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.
Files changed (49) hide show
  1. agnt5/__init__.py +307 -0
  2. agnt5/__pycache__/__init__.cpython-311.pyc +0 -0
  3. agnt5/__pycache__/agent.cpython-311.pyc +0 -0
  4. agnt5/__pycache__/context.cpython-311.pyc +0 -0
  5. agnt5/__pycache__/durable.cpython-311.pyc +0 -0
  6. agnt5/__pycache__/extraction.cpython-311.pyc +0 -0
  7. agnt5/__pycache__/memory.cpython-311.pyc +0 -0
  8. agnt5/__pycache__/reflection.cpython-311.pyc +0 -0
  9. agnt5/__pycache__/runtime.cpython-311.pyc +0 -0
  10. agnt5/__pycache__/task.cpython-311.pyc +0 -0
  11. agnt5/__pycache__/tool.cpython-311.pyc +0 -0
  12. agnt5/__pycache__/tracing.cpython-311.pyc +0 -0
  13. agnt5/__pycache__/types.cpython-311.pyc +0 -0
  14. agnt5/__pycache__/workflow.cpython-311.pyc +0 -0
  15. agnt5/_core.abi3.so +0 -0
  16. agnt5/agent.py +1086 -0
  17. agnt5/context.py +406 -0
  18. agnt5/durable.py +1050 -0
  19. agnt5/extraction.py +410 -0
  20. agnt5/llm/__init__.py +179 -0
  21. agnt5/llm/__pycache__/__init__.cpython-311.pyc +0 -0
  22. agnt5/llm/__pycache__/anthropic.cpython-311.pyc +0 -0
  23. agnt5/llm/__pycache__/azure.cpython-311.pyc +0 -0
  24. agnt5/llm/__pycache__/base.cpython-311.pyc +0 -0
  25. agnt5/llm/__pycache__/google.cpython-311.pyc +0 -0
  26. agnt5/llm/__pycache__/mistral.cpython-311.pyc +0 -0
  27. agnt5/llm/__pycache__/openai.cpython-311.pyc +0 -0
  28. agnt5/llm/__pycache__/together.cpython-311.pyc +0 -0
  29. agnt5/llm/anthropic.py +319 -0
  30. agnt5/llm/azure.py +348 -0
  31. agnt5/llm/base.py +315 -0
  32. agnt5/llm/google.py +373 -0
  33. agnt5/llm/mistral.py +330 -0
  34. agnt5/llm/model_registry.py +467 -0
  35. agnt5/llm/models.json +227 -0
  36. agnt5/llm/openai.py +334 -0
  37. agnt5/llm/together.py +377 -0
  38. agnt5/memory.py +746 -0
  39. agnt5/reflection.py +514 -0
  40. agnt5/runtime.py +699 -0
  41. agnt5/task.py +476 -0
  42. agnt5/testing.py +451 -0
  43. agnt5/tool.py +516 -0
  44. agnt5/tracing.py +624 -0
  45. agnt5/types.py +210 -0
  46. agnt5/workflow.py +897 -0
  47. agnt5-0.1.0.dist-info/METADATA +93 -0
  48. agnt5-0.1.0.dist-info/RECORD +49 -0
  49. 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()