agnt5 0.1.0__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.
- agnt5/__init__.py +307 -0
- agnt5/__pycache__/__init__.cpython-311.pyc +0 -0
- agnt5/__pycache__/agent.cpython-311.pyc +0 -0
- agnt5/__pycache__/context.cpython-311.pyc +0 -0
- agnt5/__pycache__/durable.cpython-311.pyc +0 -0
- agnt5/__pycache__/extraction.cpython-311.pyc +0 -0
- agnt5/__pycache__/memory.cpython-311.pyc +0 -0
- agnt5/__pycache__/reflection.cpython-311.pyc +0 -0
- agnt5/__pycache__/runtime.cpython-311.pyc +0 -0
- agnt5/__pycache__/task.cpython-311.pyc +0 -0
- agnt5/__pycache__/tool.cpython-311.pyc +0 -0
- agnt5/__pycache__/tracing.cpython-311.pyc +0 -0
- agnt5/__pycache__/types.cpython-311.pyc +0 -0
- agnt5/__pycache__/workflow.cpython-311.pyc +0 -0
- agnt5/_core.abi3.so +0 -0
- agnt5/agent.py +1086 -0
- agnt5/context.py +406 -0
- agnt5/durable.py +1050 -0
- agnt5/extraction.py +410 -0
- agnt5/llm/__init__.py +179 -0
- agnt5/llm/__pycache__/__init__.cpython-311.pyc +0 -0
- agnt5/llm/__pycache__/anthropic.cpython-311.pyc +0 -0
- agnt5/llm/__pycache__/azure.cpython-311.pyc +0 -0
- agnt5/llm/__pycache__/base.cpython-311.pyc +0 -0
- agnt5/llm/__pycache__/google.cpython-311.pyc +0 -0
- agnt5/llm/__pycache__/mistral.cpython-311.pyc +0 -0
- agnt5/llm/__pycache__/openai.cpython-311.pyc +0 -0
- agnt5/llm/__pycache__/together.cpython-311.pyc +0 -0
- agnt5/llm/anthropic.py +319 -0
- agnt5/llm/azure.py +348 -0
- agnt5/llm/base.py +315 -0
- agnt5/llm/google.py +373 -0
- agnt5/llm/mistral.py +330 -0
- agnt5/llm/model_registry.py +467 -0
- agnt5/llm/models.json +227 -0
- agnt5/llm/openai.py +334 -0
- agnt5/llm/together.py +377 -0
- agnt5/memory.py +746 -0
- agnt5/reflection.py +514 -0
- agnt5/runtime.py +699 -0
- agnt5/task.py +476 -0
- agnt5/testing.py +451 -0
- agnt5/tool.py +516 -0
- agnt5/tracing.py +624 -0
- agnt5/types.py +210 -0
- agnt5/workflow.py +897 -0
- agnt5-0.1.0.dist-info/METADATA +93 -0
- agnt5-0.1.0.dist-info/RECORD +49 -0
- agnt5-0.1.0.dist-info/WHEEL +4 -0
agnt5/durable.py
ADDED
|
@@ -0,0 +1,1050 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Durable execution primitives for the AGNT5 SDK.
|
|
3
|
+
|
|
4
|
+
Provides decorators and base classes for creating durable functions,
|
|
5
|
+
flows, and objects that survive failures and maintain state.
|
|
6
|
+
This is a thin wrapper around the Rust SDK-Core for high performance.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Any, Callable, Optional, TypeVar, Union, Dict, List, Type
|
|
10
|
+
import functools
|
|
11
|
+
import inspect
|
|
12
|
+
import asyncio
|
|
13
|
+
import logging
|
|
14
|
+
from datetime import datetime, timedelta
|
|
15
|
+
import json
|
|
16
|
+
import uuid
|
|
17
|
+
from dataclasses import dataclass, asdict
|
|
18
|
+
|
|
19
|
+
from .types import DurableConfig, DurablePromise
|
|
20
|
+
from .context import get_context
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
T = TypeVar('T')
|
|
25
|
+
F = TypeVar('F', bound=Callable[..., Any])
|
|
26
|
+
|
|
27
|
+
# Global registry for durable functions, flows, and objects
|
|
28
|
+
_function_registry: Dict[str, "DurableFunction"] = {}
|
|
29
|
+
_flow_registry: Dict[str, "DurableFlow"] = {}
|
|
30
|
+
_object_registry: Dict[str, Type["DurableObject"]] = {}
|
|
31
|
+
_service_registry: Dict[str, "DurableService"] = {}
|
|
32
|
+
_runtime_client = None # Will be set by SDK-Core integration
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class InvocationRequest:
|
|
37
|
+
"""Request for function invocation from runtime."""
|
|
38
|
+
invocation_id: str
|
|
39
|
+
function_name: str
|
|
40
|
+
args: List[Any]
|
|
41
|
+
kwargs: Dict[str, Any]
|
|
42
|
+
context: Dict[str, Any]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class InvocationResponse:
|
|
47
|
+
"""Response for function invocation to runtime."""
|
|
48
|
+
invocation_id: str
|
|
49
|
+
success: bool
|
|
50
|
+
result: Optional[Any] = None
|
|
51
|
+
error: Optional[str] = None
|
|
52
|
+
state_changes: Optional[Dict[str, Any]] = None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class DurableContext:
|
|
56
|
+
"""
|
|
57
|
+
Durable execution context with runtime integration via SDK-Core.
|
|
58
|
+
|
|
59
|
+
Provides APIs for:
|
|
60
|
+
- External service calls via ctx.call()
|
|
61
|
+
- Durable sleep via ctx.sleep()
|
|
62
|
+
- State management via ctx.state
|
|
63
|
+
- Object access via ctx.get_object()
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def __init__(self, invocation_id: str, context_data: Dict[str, Any]):
|
|
67
|
+
self.invocation_id = invocation_id
|
|
68
|
+
self.execution_id = context_data.get("execution_id", str(uuid.uuid4()))
|
|
69
|
+
self.function_name = context_data.get("function_name", "")
|
|
70
|
+
self._state: Dict[str, Any] = context_data.get("state", {})
|
|
71
|
+
self._runtime_client = _runtime_client
|
|
72
|
+
|
|
73
|
+
async def call(self, service: str, method: str, *args, **kwargs) -> Any:
|
|
74
|
+
"""Make a durable external service call via SDK-Core."""
|
|
75
|
+
if self._runtime_client:
|
|
76
|
+
return await self._runtime_client.durable_call(service, method, args, kwargs)
|
|
77
|
+
else:
|
|
78
|
+
# Fallback for local testing with realistic mock responses
|
|
79
|
+
logger.info(f"Durable call: {service}.{method} with args={args}, kwargs={kwargs}")
|
|
80
|
+
return self._generate_mock_response(service, method, args, kwargs)
|
|
81
|
+
|
|
82
|
+
def _generate_mock_response(self, service: str, method: str, args: tuple, kwargs: dict) -> Any:
|
|
83
|
+
"""Generate realistic mock responses for testing."""
|
|
84
|
+
# Data extractor service
|
|
85
|
+
if service == "data_extractor" and method == "extract":
|
|
86
|
+
return {
|
|
87
|
+
"data": f"mock_data_from_{args[0].get('url', 'unknown')}" if args and isinstance(args[0], dict) else "mock_data",
|
|
88
|
+
"size": 1024,
|
|
89
|
+
"format": "json",
|
|
90
|
+
"extracted_at": "2024-01-01T00:00:00Z"
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
# Data validator service
|
|
94
|
+
elif service == "data_validator" and method == "validate":
|
|
95
|
+
return {
|
|
96
|
+
"valid": True,
|
|
97
|
+
"score": 0.95,
|
|
98
|
+
"issues": [],
|
|
99
|
+
"validated_at": "2024-01-01T00:00:00Z"
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
# Data cleaner service
|
|
103
|
+
elif service == "data_cleaner" and method == "clean":
|
|
104
|
+
return {
|
|
105
|
+
"cleaned_data": "cleaned_" + str(args[0]) if args else "cleaned_data",
|
|
106
|
+
"transformations_applied": ["trim_whitespace", "normalize_encoding"],
|
|
107
|
+
"cleaned_at": "2024-01-01T00:00:00Z"
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
# Data transformer service
|
|
111
|
+
elif service == "data_transformer":
|
|
112
|
+
transform_type = args[0].get("type", method) if args and isinstance(args[0], dict) else method
|
|
113
|
+
return {
|
|
114
|
+
"transformed_data": f"transformed_data_{transform_type}",
|
|
115
|
+
"transformation_type": transform_type,
|
|
116
|
+
"records_processed": 100,
|
|
117
|
+
"transformed_at": "2024-01-01T00:00:00Z"
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
# Data aggregator service
|
|
121
|
+
elif service == "data_aggregator" and method == "aggregate":
|
|
122
|
+
return {
|
|
123
|
+
"aggregated_data": "final_aggregated_result",
|
|
124
|
+
"size": 5120,
|
|
125
|
+
"records_count": 300,
|
|
126
|
+
"aggregation_method": "merge_and_dedupe",
|
|
127
|
+
"aggregated_at": "2024-01-01T00:00:00Z"
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
# Data storage service
|
|
131
|
+
elif service == "data_storage" and method == "store":
|
|
132
|
+
return {
|
|
133
|
+
"success": True,
|
|
134
|
+
"location": "database://processed_data/table_123",
|
|
135
|
+
"record_id": "REC-789",
|
|
136
|
+
"stored_at": "2024-01-01T00:00:00Z"
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
# Search service
|
|
140
|
+
elif service == "search_service" and method == "search":
|
|
141
|
+
query = args[0].get("query", "default") if args and isinstance(args[0], dict) else "default"
|
|
142
|
+
max_results = args[0].get("max_results", 10) if args and isinstance(args[0], dict) else 10
|
|
143
|
+
|
|
144
|
+
sources = []
|
|
145
|
+
for i in range(max_results):
|
|
146
|
+
sources.append({
|
|
147
|
+
"title": f"Source {i+1} for {query}",
|
|
148
|
+
"url": f"https://example.com/source_{i+1}",
|
|
149
|
+
"content": f"Content related to {query} from source {i+1}",
|
|
150
|
+
"metadata": {
|
|
151
|
+
"author": f"Author {i+1}",
|
|
152
|
+
"published_date": "2024-01-01",
|
|
153
|
+
"source_type": "academic" if i % 2 == 0 else "general"
|
|
154
|
+
}
|
|
155
|
+
})
|
|
156
|
+
return sources
|
|
157
|
+
|
|
158
|
+
# Analysis service
|
|
159
|
+
elif service == "analysis_service" and method == "analyze":
|
|
160
|
+
content = args[0].get("content", "") if args and isinstance(args[0], dict) else ""
|
|
161
|
+
return {
|
|
162
|
+
"summary": f"Analysis summary of content (length: {len(content)})",
|
|
163
|
+
"key_points": [
|
|
164
|
+
"Key finding 1",
|
|
165
|
+
"Key finding 2",
|
|
166
|
+
"Key finding 3"
|
|
167
|
+
],
|
|
168
|
+
"relevance_score": 0.85,
|
|
169
|
+
"sentiment": "neutral",
|
|
170
|
+
"analyzed_at": "2024-01-01T00:00:00Z"
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
# Synthesis service
|
|
174
|
+
elif service == "synthesis_service" and method == "synthesize":
|
|
175
|
+
return {
|
|
176
|
+
"key_findings": [
|
|
177
|
+
"Finding 1: Important insight discovered",
|
|
178
|
+
"Finding 2: Significant pattern identified",
|
|
179
|
+
"Finding 3: Notable correlation found"
|
|
180
|
+
],
|
|
181
|
+
"conclusions": [
|
|
182
|
+
"Conclusion 1: Evidence supports hypothesis",
|
|
183
|
+
"Conclusion 2: Further research recommended"
|
|
184
|
+
],
|
|
185
|
+
"confidence_score": 0.88,
|
|
186
|
+
"synthesized_at": "2024-01-01T00:00:00Z"
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
# Report generator service
|
|
190
|
+
elif service == "report_generator" and method == "generate":
|
|
191
|
+
topic = args[0].get("topic", "Unknown") if args and isinstance(args[0], dict) else "Unknown"
|
|
192
|
+
return {
|
|
193
|
+
"report_id": "RPT-12345",
|
|
194
|
+
"title": f"Research Report: {topic}",
|
|
195
|
+
"content": f"Comprehensive report on {topic} with detailed findings and analysis.",
|
|
196
|
+
"format": "markdown",
|
|
197
|
+
"pages": 15,
|
|
198
|
+
"generated_at": "2024-01-01T00:00:00Z"
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
# Order service
|
|
202
|
+
elif service == "order_service" and method == "validate":
|
|
203
|
+
return {
|
|
204
|
+
"valid": True,
|
|
205
|
+
"validation_id": "VAL-123",
|
|
206
|
+
"validated_at": "2024-01-01T00:00:00Z"
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
# Inventory service
|
|
210
|
+
elif service == "inventory_service":
|
|
211
|
+
if method == "reserve":
|
|
212
|
+
return {
|
|
213
|
+
"success": True,
|
|
214
|
+
"reservation_id": f"RES-{args[0].get('product_id', 'UNKNOWN')}-123" if args and isinstance(args[0], dict) else "RES-123",
|
|
215
|
+
"quantity_reserved": args[0].get('quantity', 1) if args and isinstance(args[0], dict) else 1,
|
|
216
|
+
"reserved_at": "2024-01-01T00:00:00Z"
|
|
217
|
+
}
|
|
218
|
+
elif method in ["release", "confirm"]:
|
|
219
|
+
return {
|
|
220
|
+
"success": True,
|
|
221
|
+
"processed_at": "2024-01-01T00:00:00Z"
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
# Payment service
|
|
225
|
+
elif service == "payment_service" and method == "charge":
|
|
226
|
+
return {
|
|
227
|
+
"success": True,
|
|
228
|
+
"payment_id": "PAY-78901",
|
|
229
|
+
"transaction_id": "TXN-45678",
|
|
230
|
+
"amount_charged": args[0].get('amount', 0) if args and isinstance(args[0], dict) else 0,
|
|
231
|
+
"charged_at": "2024-01-01T00:00:00Z"
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
# Fulfillment service
|
|
235
|
+
elif service == "fulfillment_service" and method == "create_shipment":
|
|
236
|
+
return {
|
|
237
|
+
"success": True,
|
|
238
|
+
"shipment_id": "SHIP-56789",
|
|
239
|
+
"tracking_number": "TRK-ABCDEF123456",
|
|
240
|
+
"estimated_delivery": "2024-01-08T00:00:00Z",
|
|
241
|
+
"carrier": "FedEx",
|
|
242
|
+
"created_at": "2024-01-01T00:00:00Z"
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
# Notification service
|
|
246
|
+
elif service == "notification_service" and method == "send_confirmation":
|
|
247
|
+
return {
|
|
248
|
+
"success": True,
|
|
249
|
+
"notification_id": "NOT-12345",
|
|
250
|
+
"sent_at": "2024-01-01T00:00:00Z"
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
# Default fallback
|
|
254
|
+
else:
|
|
255
|
+
return {
|
|
256
|
+
"service": service,
|
|
257
|
+
"method": method,
|
|
258
|
+
"mock": True,
|
|
259
|
+
"response": f"mock_response_from_{service}_{method}",
|
|
260
|
+
"timestamp": "2024-01-01T00:00:00Z"
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async def sleep(self, seconds: float) -> None:
|
|
264
|
+
"""Durable sleep that survives restarts via SDK-Core."""
|
|
265
|
+
if self._runtime_client:
|
|
266
|
+
await self._runtime_client.durable_sleep(seconds)
|
|
267
|
+
else:
|
|
268
|
+
# Fallback for local testing
|
|
269
|
+
logger.info(f"Durable sleep: {seconds} seconds")
|
|
270
|
+
await asyncio.sleep(seconds)
|
|
271
|
+
|
|
272
|
+
@property
|
|
273
|
+
def state(self) -> "StateManager":
|
|
274
|
+
"""Access execution-scoped state."""
|
|
275
|
+
return StateManager(self._state, self._runtime_client)
|
|
276
|
+
|
|
277
|
+
async def get_object(self, object_class: Type[T], object_id: str) -> T:
|
|
278
|
+
"""Get or create a durable object via SDK-Core."""
|
|
279
|
+
if self._runtime_client:
|
|
280
|
+
return await self._runtime_client.get_object(object_class.__name__, object_id)
|
|
281
|
+
else:
|
|
282
|
+
# Fallback for local testing
|
|
283
|
+
return await object_class.get_or_create(object_id)
|
|
284
|
+
|
|
285
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
286
|
+
"""Serialize context for persistence."""
|
|
287
|
+
return {
|
|
288
|
+
"invocation_id": self.invocation_id,
|
|
289
|
+
"execution_id": self.execution_id,
|
|
290
|
+
"function_name": self.function_name,
|
|
291
|
+
"state": self._state,
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
class StateManager:
|
|
296
|
+
"""Manages execution-scoped state via SDK-Core."""
|
|
297
|
+
|
|
298
|
+
def __init__(self, state_dict: Dict[str, Any], runtime_client=None):
|
|
299
|
+
self._state = state_dict
|
|
300
|
+
self._runtime_client = runtime_client
|
|
301
|
+
|
|
302
|
+
async def get(self, key: str, default: Any = None) -> Any:
|
|
303
|
+
"""Get a state value."""
|
|
304
|
+
if self._runtime_client:
|
|
305
|
+
return await self._runtime_client.get_state(key, default)
|
|
306
|
+
return self._state.get(key, default)
|
|
307
|
+
|
|
308
|
+
async def set(self, key: str, value: Any) -> None:
|
|
309
|
+
"""Set a state value."""
|
|
310
|
+
if self._runtime_client:
|
|
311
|
+
await self._runtime_client.set_state(key, value)
|
|
312
|
+
else:
|
|
313
|
+
self._state[key] = value
|
|
314
|
+
|
|
315
|
+
async def delete(self, key: str) -> None:
|
|
316
|
+
"""Delete a state value."""
|
|
317
|
+
if self._runtime_client:
|
|
318
|
+
await self._runtime_client.delete_state(key)
|
|
319
|
+
else:
|
|
320
|
+
self._state.pop(key, None)
|
|
321
|
+
|
|
322
|
+
def keys(self) -> List[str]:
|
|
323
|
+
"""Get all state keys."""
|
|
324
|
+
return list(self._state.keys())
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
class DurableFunction:
|
|
328
|
+
"""
|
|
329
|
+
A durable function that maintains state across failures.
|
|
330
|
+
Delegates execution to SDK-Core for high performance.
|
|
331
|
+
"""
|
|
332
|
+
|
|
333
|
+
def __init__(self, func: Callable, config: Optional[DurableConfig] = None):
|
|
334
|
+
"""Initialize a durable function."""
|
|
335
|
+
self.func = func
|
|
336
|
+
self.is_async = inspect.iscoroutinefunction(func)
|
|
337
|
+
|
|
338
|
+
# Configuration
|
|
339
|
+
if config:
|
|
340
|
+
self.config = config
|
|
341
|
+
else:
|
|
342
|
+
self.config = DurableConfig(
|
|
343
|
+
name=func.__name__,
|
|
344
|
+
version="1.0.0",
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
# Register the function
|
|
348
|
+
_function_registry[self.config.name] = self
|
|
349
|
+
|
|
350
|
+
# Preserve function metadata
|
|
351
|
+
functools.update_wrapper(self, func)
|
|
352
|
+
|
|
353
|
+
async def invoke(self, request: InvocationRequest) -> InvocationResponse:
|
|
354
|
+
"""
|
|
355
|
+
Invoke the durable function with runtime request.
|
|
356
|
+
Called by SDK-Core when runtime sends invocation.
|
|
357
|
+
"""
|
|
358
|
+
try:
|
|
359
|
+
# Create durable context from request
|
|
360
|
+
ctx = DurableContext(request.invocation_id, request.context)
|
|
361
|
+
|
|
362
|
+
# Prepare arguments - inject context as first parameter
|
|
363
|
+
args = [ctx] + request.args
|
|
364
|
+
|
|
365
|
+
# Execute the function
|
|
366
|
+
if self.is_async:
|
|
367
|
+
result = await self.func(*args, **request.kwargs)
|
|
368
|
+
else:
|
|
369
|
+
# Run sync function in thread pool
|
|
370
|
+
loop = asyncio.get_event_loop()
|
|
371
|
+
result = await loop.run_in_executor(None, self.func, *args, **request.kwargs)
|
|
372
|
+
|
|
373
|
+
# Return successful response
|
|
374
|
+
return InvocationResponse(
|
|
375
|
+
invocation_id=request.invocation_id,
|
|
376
|
+
success=True,
|
|
377
|
+
result=result,
|
|
378
|
+
state_changes=ctx.to_dict(),
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
except Exception as e:
|
|
382
|
+
logger.error(f"Error executing durable function '{self.config.name}': {e}")
|
|
383
|
+
return InvocationResponse(
|
|
384
|
+
invocation_id=request.invocation_id,
|
|
385
|
+
success=False,
|
|
386
|
+
error=str(e),
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
async def __call__(self, *args, **kwargs) -> Any:
|
|
390
|
+
"""Direct call for local testing (bypasses runtime)."""
|
|
391
|
+
# Create mock request for local execution
|
|
392
|
+
request = InvocationRequest(
|
|
393
|
+
invocation_id=str(uuid.uuid4()),
|
|
394
|
+
function_name=self.config.name,
|
|
395
|
+
args=list(args),
|
|
396
|
+
kwargs=kwargs,
|
|
397
|
+
context={
|
|
398
|
+
"execution_id": str(uuid.uuid4()),
|
|
399
|
+
"function_name": self.config.name,
|
|
400
|
+
}
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
response = await self.invoke(request)
|
|
404
|
+
if response.success:
|
|
405
|
+
return response.result
|
|
406
|
+
else:
|
|
407
|
+
raise RuntimeError(response.error)
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
class DurableFlow:
|
|
411
|
+
"""
|
|
412
|
+
A durable flow that orchestrates multiple steps via SDK-Core.
|
|
413
|
+
|
|
414
|
+
Durable flows provide:
|
|
415
|
+
- Multi-step process coordination with shared state
|
|
416
|
+
- Automatic checkpointing and recovery
|
|
417
|
+
- Parallel execution of independent steps
|
|
418
|
+
- Error handling and retry logic
|
|
419
|
+
- Integration with the Rust flow engine
|
|
420
|
+
|
|
421
|
+
Example:
|
|
422
|
+
```python
|
|
423
|
+
@durable.flow
|
|
424
|
+
async def research_workflow(ctx, topic):
|
|
425
|
+
# Step 1: Search for sources
|
|
426
|
+
sources = await ctx.call(search_sources, topic)
|
|
427
|
+
await ctx.state.set("sources", sources)
|
|
428
|
+
|
|
429
|
+
# Step 2: Analyze each source (can run in parallel)
|
|
430
|
+
analyses = []
|
|
431
|
+
for source in sources:
|
|
432
|
+
analysis = await ctx.call(analyze_source, source)
|
|
433
|
+
analyses.append(analysis)
|
|
434
|
+
|
|
435
|
+
# Step 3: Synthesize results
|
|
436
|
+
await ctx.state.set("analyses", analyses)
|
|
437
|
+
report = await ctx.call(generate_report, analyses)
|
|
438
|
+
|
|
439
|
+
return report
|
|
440
|
+
```
|
|
441
|
+
"""
|
|
442
|
+
|
|
443
|
+
def __init__(self, func: Callable, config: Optional[DurableConfig] = None):
|
|
444
|
+
"""Initialize a durable flow."""
|
|
445
|
+
self.func = func
|
|
446
|
+
self.is_async = inspect.iscoroutinefunction(func)
|
|
447
|
+
|
|
448
|
+
# Configuration with flow-specific defaults
|
|
449
|
+
if config:
|
|
450
|
+
self.config = config
|
|
451
|
+
else:
|
|
452
|
+
self.config = DurableConfig(
|
|
453
|
+
name=func.__name__,
|
|
454
|
+
version="1.0.0",
|
|
455
|
+
checkpoint_interval=1, # Checkpoint after each step by default
|
|
456
|
+
deterministic=True, # Flows should be deterministic
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
# Register the flow as a function but track it as a flow
|
|
460
|
+
self._durable_func = DurableFunction(self._flow_wrapper, self.config)
|
|
461
|
+
|
|
462
|
+
# Register in flow registry
|
|
463
|
+
_flow_registry[self.config.name] = self
|
|
464
|
+
|
|
465
|
+
# Preserve function metadata
|
|
466
|
+
functools.update_wrapper(self, func)
|
|
467
|
+
|
|
468
|
+
async def _flow_wrapper(self, ctx: DurableContext, *args, **kwargs) -> Any:
|
|
469
|
+
"""
|
|
470
|
+
Wrapper that executes the flow function with enhanced context.
|
|
471
|
+
|
|
472
|
+
This wrapper provides flow-specific functionality like:
|
|
473
|
+
- Step tracking and checkpointing
|
|
474
|
+
- Shared state management
|
|
475
|
+
- Parallel execution coordination
|
|
476
|
+
"""
|
|
477
|
+
# Initialize flow execution context in shared state
|
|
478
|
+
flow_state = {
|
|
479
|
+
"flow_name": self.config.name,
|
|
480
|
+
"execution_id": ctx.execution_id,
|
|
481
|
+
"current_step": 0,
|
|
482
|
+
"completed_steps": [],
|
|
483
|
+
"step_results": {},
|
|
484
|
+
"flow_config": {
|
|
485
|
+
"checkpoint_interval": getattr(self.config, 'checkpoint_interval', 1),
|
|
486
|
+
"max_retries": self.config.max_retries,
|
|
487
|
+
"deterministic": self.config.deterministic,
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
# Store initial flow state
|
|
492
|
+
await ctx.state.set("__flow_state__", flow_state)
|
|
493
|
+
|
|
494
|
+
try:
|
|
495
|
+
# Execute the actual flow function
|
|
496
|
+
if self.is_async:
|
|
497
|
+
result = await self.func(ctx, *args, **kwargs)
|
|
498
|
+
else:
|
|
499
|
+
# Run sync function in thread pool
|
|
500
|
+
loop = asyncio.get_event_loop()
|
|
501
|
+
result = await loop.run_in_executor(None, self.func, ctx, *args, **kwargs)
|
|
502
|
+
|
|
503
|
+
# Mark flow as completed
|
|
504
|
+
flow_state["status"] = "completed"
|
|
505
|
+
flow_state["result"] = result
|
|
506
|
+
await ctx.state.set("__flow_state__", flow_state)
|
|
507
|
+
|
|
508
|
+
return result
|
|
509
|
+
|
|
510
|
+
except Exception as e:
|
|
511
|
+
# Mark flow as failed
|
|
512
|
+
flow_state["status"] = "failed"
|
|
513
|
+
flow_state["error"] = str(e)
|
|
514
|
+
await ctx.state.set("__flow_state__", flow_state)
|
|
515
|
+
raise
|
|
516
|
+
|
|
517
|
+
async def invoke(self, request: InvocationRequest) -> InvocationResponse:
|
|
518
|
+
"""Invoke the durable flow via the function wrapper."""
|
|
519
|
+
return await self._durable_func.invoke(request)
|
|
520
|
+
|
|
521
|
+
async def __call__(self, *args, **kwargs) -> Any:
|
|
522
|
+
"""Execute the durable flow."""
|
|
523
|
+
return await self._durable_func(*args, **kwargs)
|
|
524
|
+
|
|
525
|
+
async def get_execution_status(self, execution_id: str) -> Dict[str, Any]:
|
|
526
|
+
"""Get the current status of a flow execution."""
|
|
527
|
+
if _runtime_client:
|
|
528
|
+
return await _runtime_client.get_flow_status(self.config.name, execution_id)
|
|
529
|
+
else:
|
|
530
|
+
# Fallback for local testing
|
|
531
|
+
return {
|
|
532
|
+
"execution_id": execution_id,
|
|
533
|
+
"flow_name": self.config.name,
|
|
534
|
+
"status": "unknown",
|
|
535
|
+
"current_step": 0,
|
|
536
|
+
"completed_steps": [],
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
async def resume_execution(self, execution_id: str) -> Any:
|
|
540
|
+
"""Resume a suspended flow execution."""
|
|
541
|
+
if _runtime_client:
|
|
542
|
+
return await _runtime_client.resume_flow(self.config.name, execution_id)
|
|
543
|
+
else:
|
|
544
|
+
raise RuntimeError("Flow resumption requires runtime client integration")
|
|
545
|
+
|
|
546
|
+
async def cancel_execution(self, execution_id: str) -> bool:
|
|
547
|
+
"""Cancel a running flow execution."""
|
|
548
|
+
if _runtime_client:
|
|
549
|
+
return await _runtime_client.cancel_flow(self.config.name, execution_id)
|
|
550
|
+
else:
|
|
551
|
+
# Fallback for local testing
|
|
552
|
+
logger.info(f"Mock cancellation of flow {self.config.name} execution {execution_id}")
|
|
553
|
+
return True
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
class DurableObject:
|
|
557
|
+
"""
|
|
558
|
+
Base class for durable objects that maintain state.
|
|
559
|
+
|
|
560
|
+
Durable objects provide:
|
|
561
|
+
- Automatic state persistence via SDK-Core
|
|
562
|
+
- Serialized access per object instance
|
|
563
|
+
- Method invocation routing
|
|
564
|
+
- Virtual object management
|
|
565
|
+
|
|
566
|
+
Example:
|
|
567
|
+
```python
|
|
568
|
+
@durable.object
|
|
569
|
+
class ShoppingCart:
|
|
570
|
+
def __init__(self, user_id: str):
|
|
571
|
+
self.user_id = user_id
|
|
572
|
+
self.items = []
|
|
573
|
+
|
|
574
|
+
async def add_item(self, item_id: str, quantity: int):
|
|
575
|
+
self.items.append({"id": item_id, "qty": quantity})
|
|
576
|
+
await self.save()
|
|
577
|
+
|
|
578
|
+
async def checkout(self, ctx: DurableContext):
|
|
579
|
+
total = await ctx.call("pricing_service", "calculate_total", self.items)
|
|
580
|
+
order = await ctx.call("order_service", "create_order", {
|
|
581
|
+
"user_id": self.user_id,
|
|
582
|
+
"items": self.items,
|
|
583
|
+
"total": total
|
|
584
|
+
})
|
|
585
|
+
self.items = []
|
|
586
|
+
await self.save()
|
|
587
|
+
return order
|
|
588
|
+
```
|
|
589
|
+
"""
|
|
590
|
+
|
|
591
|
+
def __init__(self, object_id: str):
|
|
592
|
+
"""Initialize a durable object."""
|
|
593
|
+
self.object_id = object_id
|
|
594
|
+
self._version = 0
|
|
595
|
+
from datetime import timezone
|
|
596
|
+
self._last_saved = datetime.now(timezone.utc)
|
|
597
|
+
self._methods: Dict[str, Callable] = {}
|
|
598
|
+
|
|
599
|
+
# Register all public methods as invokable
|
|
600
|
+
for name, method in inspect.getmembers(self, predicate=inspect.ismethod):
|
|
601
|
+
if not name.startswith('_') and name not in ['save', 'get_or_create']:
|
|
602
|
+
self._methods[name] = method
|
|
603
|
+
|
|
604
|
+
@classmethod
|
|
605
|
+
async def get_or_create(cls, object_id: str) -> "DurableObject":
|
|
606
|
+
"""Get an existing object or create a new one via SDK-Core."""
|
|
607
|
+
if _runtime_client:
|
|
608
|
+
return await _runtime_client.get_or_create_object(cls.__name__, object_id)
|
|
609
|
+
|
|
610
|
+
# Fallback for local testing
|
|
611
|
+
state = await cls._load_state(object_id)
|
|
612
|
+
if state:
|
|
613
|
+
obj = cls(object_id)
|
|
614
|
+
await obj._restore_state(state)
|
|
615
|
+
return obj
|
|
616
|
+
else:
|
|
617
|
+
obj = cls(object_id)
|
|
618
|
+
await obj.save()
|
|
619
|
+
return obj
|
|
620
|
+
|
|
621
|
+
async def invoke_method(self, method_name: str, args: List[Any], kwargs: Dict[str, Any]) -> Any:
|
|
622
|
+
"""Invoke a method on this object."""
|
|
623
|
+
if method_name not in self._methods:
|
|
624
|
+
raise ValueError(f"Method '{method_name}' not found on object {self.object_id}")
|
|
625
|
+
|
|
626
|
+
method = self._methods[method_name]
|
|
627
|
+
if inspect.iscoroutinefunction(method):
|
|
628
|
+
result = await method(*args, **kwargs)
|
|
629
|
+
else:
|
|
630
|
+
result = method(*args, **kwargs)
|
|
631
|
+
|
|
632
|
+
# Auto-save after method execution
|
|
633
|
+
await self.save()
|
|
634
|
+
return result
|
|
635
|
+
|
|
636
|
+
async def save(self) -> None:
|
|
637
|
+
"""Save the object state via SDK-Core."""
|
|
638
|
+
if _runtime_client:
|
|
639
|
+
state = await self._get_state()
|
|
640
|
+
await _runtime_client.save_object_state(self.__class__.__name__, self.object_id, state)
|
|
641
|
+
else:
|
|
642
|
+
# Fallback for local testing
|
|
643
|
+
state = await self._get_state()
|
|
644
|
+
await self._save_state(self.object_id, state)
|
|
645
|
+
|
|
646
|
+
self._version += 1
|
|
647
|
+
from datetime import timezone
|
|
648
|
+
self._last_saved = datetime.now(timezone.utc)
|
|
649
|
+
|
|
650
|
+
async def _get_state(self) -> Dict[str, Any]:
|
|
651
|
+
"""Get the current state for serialization."""
|
|
652
|
+
state = {}
|
|
653
|
+
for key, value in self.__dict__.items():
|
|
654
|
+
if not key.startswith('_') and not callable(value):
|
|
655
|
+
try:
|
|
656
|
+
# Try to serialize the value
|
|
657
|
+
json.dumps(value, default=str)
|
|
658
|
+
state[key] = value
|
|
659
|
+
except (TypeError, ValueError):
|
|
660
|
+
# Skip non-serializable values
|
|
661
|
+
logger.warning(f"Skipping non-serializable attribute '{key}' in object {self.object_id}")
|
|
662
|
+
|
|
663
|
+
state['_version'] = self._version
|
|
664
|
+
state['_last_saved'] = self._last_saved.isoformat()
|
|
665
|
+
return state
|
|
666
|
+
|
|
667
|
+
async def _restore_state(self, state: Dict[str, Any]) -> None:
|
|
668
|
+
"""Restore state from serialization."""
|
|
669
|
+
for key, value in state.items():
|
|
670
|
+
if key == '_last_saved':
|
|
671
|
+
self._last_saved = datetime.fromisoformat(value)
|
|
672
|
+
else:
|
|
673
|
+
setattr(self, key, value)
|
|
674
|
+
|
|
675
|
+
@classmethod
|
|
676
|
+
async def _load_state(cls, object_id: str) -> Optional[Dict[str, Any]]:
|
|
677
|
+
"""Load state from storage via SDK-Core (fallback)."""
|
|
678
|
+
if _runtime_client:
|
|
679
|
+
return await _runtime_client.load_object_state(cls.__name__, object_id)
|
|
680
|
+
return None
|
|
681
|
+
|
|
682
|
+
@classmethod
|
|
683
|
+
async def _save_state(cls, object_id: str, state: Dict[str, Any]) -> None:
|
|
684
|
+
"""Save state to storage via SDK-Core (fallback)."""
|
|
685
|
+
# This is a fallback - normally save() method calls SDK-Core directly
|
|
686
|
+
pass
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
class DurableService:
|
|
690
|
+
"""Service that registers and manages durable functions and objects."""
|
|
691
|
+
|
|
692
|
+
def __init__(self, name: str, version: str = "1.0.0"):
|
|
693
|
+
self.name = name
|
|
694
|
+
self.version = version
|
|
695
|
+
self.functions: Dict[str, DurableFunction] = {}
|
|
696
|
+
self.objects: Dict[str, Type[DurableObject]] = {}
|
|
697
|
+
|
|
698
|
+
# Register in global registry
|
|
699
|
+
_service_registry[name] = self
|
|
700
|
+
|
|
701
|
+
def add_function(self, func: DurableFunction) -> None:
|
|
702
|
+
"""Add a function to this service."""
|
|
703
|
+
self.functions[func.config.name] = func
|
|
704
|
+
|
|
705
|
+
def add_object(self, object_class: Type[DurableObject]) -> None:
|
|
706
|
+
"""Add an object class to this service."""
|
|
707
|
+
self.objects[object_class.__name__] = object_class
|
|
708
|
+
|
|
709
|
+
async def handle_invocation(self, request: InvocationRequest) -> InvocationResponse:
|
|
710
|
+
"""Handle function invocation from runtime."""
|
|
711
|
+
if request.function_name in self.functions:
|
|
712
|
+
func = self.functions[request.function_name]
|
|
713
|
+
return await func.invoke(request)
|
|
714
|
+
else:
|
|
715
|
+
return InvocationResponse(
|
|
716
|
+
invocation_id=request.invocation_id,
|
|
717
|
+
success=False,
|
|
718
|
+
error=f"Function '{request.function_name}' not found in service '{self.name}'",
|
|
719
|
+
)
|
|
720
|
+
|
|
721
|
+
def get_service_config(self) -> Dict[str, Any]:
|
|
722
|
+
"""Get service configuration for registration with SDK-Core."""
|
|
723
|
+
return {
|
|
724
|
+
"name": self.name,
|
|
725
|
+
"version": self.version,
|
|
726
|
+
"functions": [
|
|
727
|
+
{
|
|
728
|
+
"name": func.config.name,
|
|
729
|
+
"version": func.config.version,
|
|
730
|
+
"deterministic": func.config.deterministic,
|
|
731
|
+
"idempotent": func.config.idempotent,
|
|
732
|
+
"max_retries": func.config.max_retries,
|
|
733
|
+
"timeout": func.config.timeout,
|
|
734
|
+
}
|
|
735
|
+
for func in self.functions.values()
|
|
736
|
+
],
|
|
737
|
+
"objects": [
|
|
738
|
+
{
|
|
739
|
+
"name": obj.__name__,
|
|
740
|
+
"methods": [
|
|
741
|
+
name for name, _ in inspect.getmembers(obj, predicate=inspect.isfunction)
|
|
742
|
+
if not name.startswith('_') and name not in ['save', 'get_or_create']
|
|
743
|
+
]
|
|
744
|
+
}
|
|
745
|
+
for obj in self.objects.values()
|
|
746
|
+
]
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
class DurableNamespace:
|
|
751
|
+
"""Namespace for durable primitives."""
|
|
752
|
+
|
|
753
|
+
def __init__(self):
|
|
754
|
+
self._current_service: Optional[DurableService] = None
|
|
755
|
+
|
|
756
|
+
def service(self, name: str, version: str = "1.0.0") -> DurableService:
|
|
757
|
+
"""Create or get a durable service."""
|
|
758
|
+
if name in _service_registry:
|
|
759
|
+
return _service_registry[name]
|
|
760
|
+
return DurableService(name, version)
|
|
761
|
+
|
|
762
|
+
@staticmethod
|
|
763
|
+
def function(
|
|
764
|
+
func: Optional[F] = None,
|
|
765
|
+
*,
|
|
766
|
+
name: Optional[str] = None,
|
|
767
|
+
version: str = "1.0.0",
|
|
768
|
+
deterministic: bool = True,
|
|
769
|
+
idempotent: bool = True,
|
|
770
|
+
max_retries: int = 3,
|
|
771
|
+
retry_delay: float = 1.0,
|
|
772
|
+
timeout: Optional[float] = None,
|
|
773
|
+
service: Optional[str] = None,
|
|
774
|
+
) -> Union[DurableFunction, Callable[[F], DurableFunction]]:
|
|
775
|
+
"""Decorator to create a durable function."""
|
|
776
|
+
def decorator(f: F) -> DurableFunction:
|
|
777
|
+
config = DurableConfig(
|
|
778
|
+
name=name or f.__name__,
|
|
779
|
+
version=version,
|
|
780
|
+
deterministic=deterministic,
|
|
781
|
+
idempotent=idempotent,
|
|
782
|
+
max_retries=max_retries,
|
|
783
|
+
retry_delay=retry_delay,
|
|
784
|
+
timeout=timeout,
|
|
785
|
+
)
|
|
786
|
+
|
|
787
|
+
durable_func = DurableFunction(f, config)
|
|
788
|
+
|
|
789
|
+
# Add to service if specified
|
|
790
|
+
if service:
|
|
791
|
+
svc = durable.service(service)
|
|
792
|
+
svc.add_function(durable_func)
|
|
793
|
+
|
|
794
|
+
return durable_func
|
|
795
|
+
|
|
796
|
+
if func is None:
|
|
797
|
+
return decorator
|
|
798
|
+
else:
|
|
799
|
+
return decorator(func)
|
|
800
|
+
|
|
801
|
+
@staticmethod
|
|
802
|
+
def flow(
|
|
803
|
+
func: Optional[F] = None,
|
|
804
|
+
*,
|
|
805
|
+
name: Optional[str] = None,
|
|
806
|
+
version: str = "1.0.0",
|
|
807
|
+
checkpoint_interval: int = 1,
|
|
808
|
+
max_retries: int = 3,
|
|
809
|
+
max_concurrent_steps: int = 10,
|
|
810
|
+
deterministic: bool = True,
|
|
811
|
+
timeout: Optional[float] = None,
|
|
812
|
+
service: Optional[str] = None,
|
|
813
|
+
) -> Union[DurableFlow, Callable[[F], DurableFlow]]:
|
|
814
|
+
"""
|
|
815
|
+
Decorator to create a durable flow.
|
|
816
|
+
|
|
817
|
+
Args:
|
|
818
|
+
func: The function to wrap as a durable flow
|
|
819
|
+
name: Flow name (defaults to function name)
|
|
820
|
+
version: Flow version
|
|
821
|
+
checkpoint_interval: Steps between checkpoints
|
|
822
|
+
max_retries: Maximum retry attempts per step
|
|
823
|
+
max_concurrent_steps: Maximum parallel steps
|
|
824
|
+
deterministic: Whether execution should be deterministic
|
|
825
|
+
timeout: Timeout per step in seconds
|
|
826
|
+
service: Service to register the flow with
|
|
827
|
+
|
|
828
|
+
Returns:
|
|
829
|
+
DurableFlow instance or decorator
|
|
830
|
+
|
|
831
|
+
Example:
|
|
832
|
+
```python
|
|
833
|
+
@durable.flow(checkpoint_interval=2, max_concurrent_steps=5)
|
|
834
|
+
async def data_pipeline(ctx, input_data):
|
|
835
|
+
# Step 1: Extract data
|
|
836
|
+
extracted = await ctx.call(extract_service, "extract", input_data)
|
|
837
|
+
await ctx.state.set("extracted", extracted)
|
|
838
|
+
|
|
839
|
+
# Step 2: Transform data (checkpointed here due to interval=2)
|
|
840
|
+
transformed = await ctx.call(transform_service, "transform", extracted)
|
|
841
|
+
await ctx.state.set("transformed", transformed)
|
|
842
|
+
|
|
843
|
+
# Step 3: Load data
|
|
844
|
+
result = await ctx.call(load_service, "load", transformed)
|
|
845
|
+
return result
|
|
846
|
+
```
|
|
847
|
+
"""
|
|
848
|
+
def decorator(f: F) -> DurableFlow:
|
|
849
|
+
config = DurableConfig(
|
|
850
|
+
name=name or f.__name__,
|
|
851
|
+
version=version,
|
|
852
|
+
checkpoint_interval=checkpoint_interval,
|
|
853
|
+
max_retries=max_retries,
|
|
854
|
+
deterministic=deterministic,
|
|
855
|
+
timeout=timeout,
|
|
856
|
+
max_concurrent_executions=max_concurrent_steps,
|
|
857
|
+
)
|
|
858
|
+
|
|
859
|
+
durable_flow = DurableFlow(f, config)
|
|
860
|
+
|
|
861
|
+
# Add to service if specified
|
|
862
|
+
if service:
|
|
863
|
+
svc = durable.service(service)
|
|
864
|
+
svc.add_function(durable_flow._durable_func)
|
|
865
|
+
|
|
866
|
+
return durable_flow
|
|
867
|
+
|
|
868
|
+
if func is None:
|
|
869
|
+
return decorator
|
|
870
|
+
else:
|
|
871
|
+
return decorator(func)
|
|
872
|
+
|
|
873
|
+
@staticmethod
|
|
874
|
+
def object(cls: Optional[Type[T]] = None, *, service: Optional[str] = None) -> Union[Type[T], Callable[[Type[T]], Type[T]]]:
|
|
875
|
+
"""Decorator to create a durable object class."""
|
|
876
|
+
def decorator(c: Type[T]) -> Type[T]:
|
|
877
|
+
# Ensure class inherits from DurableObject
|
|
878
|
+
if not issubclass(c, DurableObject):
|
|
879
|
+
# Create a new class that inherits from both
|
|
880
|
+
class DurableClass(c, DurableObject):
|
|
881
|
+
pass
|
|
882
|
+
|
|
883
|
+
# Copy class attributes
|
|
884
|
+
for attr, value in c.__dict__.items():
|
|
885
|
+
if not attr.startswith('__'):
|
|
886
|
+
setattr(DurableClass, attr, value)
|
|
887
|
+
|
|
888
|
+
# Update class name and module
|
|
889
|
+
DurableClass.__name__ = c.__name__
|
|
890
|
+
DurableClass.__module__ = c.__module__
|
|
891
|
+
|
|
892
|
+
final_class = DurableClass
|
|
893
|
+
else:
|
|
894
|
+
final_class = c
|
|
895
|
+
|
|
896
|
+
# Register in global registry
|
|
897
|
+
_object_registry[final_class.__name__] = final_class
|
|
898
|
+
|
|
899
|
+
# Add to service if specified
|
|
900
|
+
if service:
|
|
901
|
+
svc = durable.service(service)
|
|
902
|
+
svc.add_object(final_class)
|
|
903
|
+
|
|
904
|
+
return final_class
|
|
905
|
+
|
|
906
|
+
if cls is None:
|
|
907
|
+
return decorator
|
|
908
|
+
else:
|
|
909
|
+
return decorator(cls)
|
|
910
|
+
|
|
911
|
+
@staticmethod
|
|
912
|
+
async def sleep(seconds: float) -> None:
|
|
913
|
+
"""Durable sleep that survives restarts via SDK-Core."""
|
|
914
|
+
if _runtime_client:
|
|
915
|
+
await _runtime_client.durable_sleep(seconds)
|
|
916
|
+
else:
|
|
917
|
+
logger.info(f"Durable sleep: {seconds} seconds")
|
|
918
|
+
await asyncio.sleep(seconds)
|
|
919
|
+
|
|
920
|
+
@staticmethod
|
|
921
|
+
def promise(result_type: Type[T]) -> DurablePromise[T]:
|
|
922
|
+
"""Create a durable promise."""
|
|
923
|
+
ctx = get_context()
|
|
924
|
+
return DurablePromise(ctx.execution_id, result_type)
|
|
925
|
+
|
|
926
|
+
def get_all_services(self) -> List[DurableService]:
|
|
927
|
+
"""Get all registered services."""
|
|
928
|
+
return list(_service_registry.values())
|
|
929
|
+
|
|
930
|
+
def get_all_functions(self) -> List[DurableFunction]:
|
|
931
|
+
"""Get all registered functions."""
|
|
932
|
+
return list(_function_registry.values())
|
|
933
|
+
|
|
934
|
+
def get_all_objects(self) -> List[Type[DurableObject]]:
|
|
935
|
+
"""Get all registered object classes."""
|
|
936
|
+
return list(_object_registry.values())
|
|
937
|
+
|
|
938
|
+
def get_all_flows(self) -> List[DurableFlow]:
|
|
939
|
+
"""Get all registered flows."""
|
|
940
|
+
return list(_flow_registry.values())
|
|
941
|
+
|
|
942
|
+
def get_flow(self, name: str) -> Optional[DurableFlow]:
|
|
943
|
+
"""Get a specific flow by name."""
|
|
944
|
+
return _flow_registry.get(name)
|
|
945
|
+
|
|
946
|
+
async def list_active_flows(self) -> List[Dict[str, Any]]:
|
|
947
|
+
"""List all currently active flow executions."""
|
|
948
|
+
if _runtime_client:
|
|
949
|
+
return await _runtime_client.list_active_flows()
|
|
950
|
+
else:
|
|
951
|
+
# Fallback for local testing
|
|
952
|
+
return [
|
|
953
|
+
{
|
|
954
|
+
"flow_name": flow.config.name,
|
|
955
|
+
"execution_id": "mock_execution",
|
|
956
|
+
"status": "running",
|
|
957
|
+
"started_at": datetime.now().isoformat(),
|
|
958
|
+
}
|
|
959
|
+
for flow in _flow_registry.values()
|
|
960
|
+
]
|
|
961
|
+
|
|
962
|
+
|
|
963
|
+
# Integration functions for SDK-Core
|
|
964
|
+
async def handle_invocation_from_runtime(request_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
965
|
+
"""Handle function invocation from runtime via SDK-Core."""
|
|
966
|
+
try:
|
|
967
|
+
request = InvocationRequest(
|
|
968
|
+
invocation_id=request_data["invocation_id"],
|
|
969
|
+
function_name=request_data["function_name"],
|
|
970
|
+
args=request_data.get("args", []),
|
|
971
|
+
kwargs=request_data.get("kwargs", {}),
|
|
972
|
+
context=request_data.get("context", {}),
|
|
973
|
+
)
|
|
974
|
+
|
|
975
|
+
# Find the function or flow
|
|
976
|
+
if request.function_name in _function_registry:
|
|
977
|
+
func = _function_registry[request.function_name]
|
|
978
|
+
response = await func.invoke(request)
|
|
979
|
+
elif request.function_name in _flow_registry:
|
|
980
|
+
flow = _flow_registry[request.function_name]
|
|
981
|
+
response = await flow.invoke(request)
|
|
982
|
+
else:
|
|
983
|
+
response = InvocationResponse(
|
|
984
|
+
invocation_id=request.invocation_id,
|
|
985
|
+
success=False,
|
|
986
|
+
error=f"Function or flow '{request.function_name}' not found",
|
|
987
|
+
)
|
|
988
|
+
|
|
989
|
+
return asdict(response)
|
|
990
|
+
|
|
991
|
+
except Exception as e:
|
|
992
|
+
logger.error(f"Error handling invocation: {e}")
|
|
993
|
+
return {
|
|
994
|
+
"invocation_id": request_data.get("invocation_id", "unknown"),
|
|
995
|
+
"success": False,
|
|
996
|
+
"error": str(e),
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
|
|
1000
|
+
def get_service_registration_data() -> Dict[str, Any]:
|
|
1001
|
+
"""Get service registration data for SDK-Core."""
|
|
1002
|
+
return {
|
|
1003
|
+
"services": [svc.get_service_config() for svc in _service_registry.values()],
|
|
1004
|
+
"functions": [
|
|
1005
|
+
{
|
|
1006
|
+
"name": func.config.name,
|
|
1007
|
+
"version": func.config.version,
|
|
1008
|
+
"type": "function",
|
|
1009
|
+
"deterministic": func.config.deterministic,
|
|
1010
|
+
"idempotent": func.config.idempotent,
|
|
1011
|
+
"max_retries": func.config.max_retries,
|
|
1012
|
+
"timeout": func.config.timeout,
|
|
1013
|
+
}
|
|
1014
|
+
for func in _function_registry.values()
|
|
1015
|
+
],
|
|
1016
|
+
"flows": [
|
|
1017
|
+
{
|
|
1018
|
+
"name": flow.config.name,
|
|
1019
|
+
"version": flow.config.version,
|
|
1020
|
+
"type": "flow",
|
|
1021
|
+
"deterministic": flow.config.deterministic,
|
|
1022
|
+
"checkpoint_interval": flow.config.checkpoint_interval,
|
|
1023
|
+
"max_retries": flow.config.max_retries,
|
|
1024
|
+
"timeout": flow.config.timeout,
|
|
1025
|
+
"max_concurrent_steps": flow.config.max_concurrent_executions,
|
|
1026
|
+
}
|
|
1027
|
+
for flow in _flow_registry.values()
|
|
1028
|
+
],
|
|
1029
|
+
"objects": [
|
|
1030
|
+
{
|
|
1031
|
+
"name": obj.__name__,
|
|
1032
|
+
"type": "object",
|
|
1033
|
+
"methods": [
|
|
1034
|
+
name for name, _ in inspect.getmembers(obj, predicate=inspect.isfunction)
|
|
1035
|
+
if not name.startswith('_') and name not in ['save', 'get_or_create']
|
|
1036
|
+
]
|
|
1037
|
+
}
|
|
1038
|
+
for obj in _object_registry.values()
|
|
1039
|
+
]
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
|
|
1043
|
+
def set_runtime_client(client):
|
|
1044
|
+
"""Set the runtime client for SDK-Core integration."""
|
|
1045
|
+
global _runtime_client
|
|
1046
|
+
_runtime_client = client
|
|
1047
|
+
|
|
1048
|
+
|
|
1049
|
+
# Create the durable namespace instance
|
|
1050
|
+
durable = DurableNamespace()
|