edda-framework 0.3.1__tar.gz → 0.4.0__tar.gz

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 (151) hide show
  1. {edda_framework-0.3.1 → edda_framework-0.4.0}/PKG-INFO +27 -1
  2. {edda_framework-0.3.1 → edda_framework-0.4.0}/README.md +26 -0
  3. {edda_framework-0.3.1 → edda_framework-0.4.0}/demo_app.py +4 -4
  4. {edda_framework-0.3.1 → edda_framework-0.4.0}/docs/core-features/durable-execution/replay.md +8 -8
  5. {edda_framework-0.3.1 → edda_framework-0.4.0}/docs/core-features/workflows-activities.md +4 -4
  6. {edda_framework-0.3.1 → edda_framework-0.4.0}/docs/getting-started/first-workflow.md +32 -28
  7. {edda_framework-0.3.1 → edda_framework-0.4.0}/docs/index.md +8 -7
  8. {edda_framework-0.3.1 → edda_framework-0.4.0}/edda/integrations/mcp/decorators.py +3 -4
  9. {edda_framework-0.3.1 → edda_framework-0.4.0}/edda/integrations/mcp/server.py +157 -5
  10. edda_framework-0.4.0/examples/mcp/prompts_example.py +281 -0
  11. {edda_framework-0.3.1 → edda_framework-0.4.0}/examples/observability_with_logfire.py +1 -1
  12. {edda_framework-0.3.1 → edda_framework-0.4.0}/pyproject.toml +1 -1
  13. {edda_framework-0.3.1 → edda_framework-0.4.0}/tests/integrations/mcp/test_integration.py +6 -6
  14. {edda_framework-0.3.1 → edda_framework-0.4.0}/tests/integrations/mcp/test_jsonrpc.py +3 -3
  15. edda_framework-0.4.0/tests/integrations/mcp/test_prompts.py +203 -0
  16. {edda_framework-0.3.1 → edda_framework-0.4.0}/tests/integrations/mcp/test_server.py +2 -2
  17. {edda_framework-0.3.1 → edda_framework-0.4.0}/uv.lock +1 -1
  18. {edda_framework-0.3.1 → edda_framework-0.4.0}/.github/workflows/ci.yml +0 -0
  19. {edda_framework-0.3.1 → edda_framework-0.4.0}/.github/workflows/docs.yml +0 -0
  20. {edda_framework-0.3.1 → edda_framework-0.4.0}/.github/workflows/release.yml +0 -0
  21. {edda_framework-0.3.1 → edda_framework-0.4.0}/.gitignore +0 -0
  22. {edda_framework-0.3.1 → edda_framework-0.4.0}/.python-version +0 -0
  23. {edda_framework-0.3.1 → edda_framework-0.4.0}/Justfile +0 -0
  24. {edda_framework-0.3.1 → edda_framework-0.4.0}/LICENSE +0 -0
  25. {edda_framework-0.3.1 → edda_framework-0.4.0}/docs/core-features/events/cloudevents-http-binding.md +0 -0
  26. {edda_framework-0.3.1 → edda_framework-0.4.0}/docs/core-features/events/wait-event.md +0 -0
  27. {edda_framework-0.3.1 → edda_framework-0.4.0}/docs/core-features/hooks.md +0 -0
  28. {edda_framework-0.3.1 → edda_framework-0.4.0}/docs/core-features/retry.md +0 -0
  29. {edda_framework-0.3.1 → edda_framework-0.4.0}/docs/core-features/saga-compensation.md +0 -0
  30. {edda_framework-0.3.1 → edda_framework-0.4.0}/docs/core-features/transactional-outbox.md +0 -0
  31. {edda_framework-0.3.1 → edda_framework-0.4.0}/docs/examples/ecommerce.md +0 -0
  32. {edda_framework-0.3.1 → edda_framework-0.4.0}/docs/examples/events.md +0 -0
  33. {edda_framework-0.3.1 → edda_framework-0.4.0}/docs/examples/fastapi-integration.md +0 -0
  34. {edda_framework-0.3.1 → edda_framework-0.4.0}/docs/examples/saga.md +0 -0
  35. {edda_framework-0.3.1 → edda_framework-0.4.0}/docs/examples/simple.md +0 -0
  36. {edda_framework-0.3.1 → edda_framework-0.4.0}/docs/getting-started/concepts.md +0 -0
  37. {edda_framework-0.3.1 → edda_framework-0.4.0}/docs/getting-started/installation.md +0 -0
  38. {edda_framework-0.3.1 → edda_framework-0.4.0}/docs/getting-started/quick-start.md +0 -0
  39. {edda_framework-0.3.1 → edda_framework-0.4.0}/docs/integrations/mcp.md +0 -0
  40. {edda_framework-0.3.1 → edda_framework-0.4.0}/docs/markdown.md +0 -0
  41. {edda_framework-0.3.1 → edda_framework-0.4.0}/docs/viewer-ui/images/cloudevents-cli-trigger.png +0 -0
  42. {edda_framework-0.3.1 → edda_framework-0.4.0}/docs/viewer-ui/images/compensation-execution.png +0 -0
  43. {edda_framework-0.3.1 → edda_framework-0.4.0}/docs/viewer-ui/images/conditional-branching-diagram.png +0 -0
  44. {edda_framework-0.3.1 → edda_framework-0.4.0}/docs/viewer-ui/images/detail-overview-panel.png +0 -0
  45. {edda_framework-0.3.1 → edda_framework-0.4.0}/docs/viewer-ui/images/execution-history-panel.png +0 -0
  46. {edda_framework-0.3.1 → edda_framework-0.4.0}/docs/viewer-ui/images/form-generation-example.png +0 -0
  47. {edda_framework-0.3.1 → edda_framework-0.4.0}/docs/viewer-ui/images/hybrid-diagram-example.png +0 -0
  48. {edda_framework-0.3.1 → edda_framework-0.4.0}/docs/viewer-ui/images/nested-pydantic-form.png +0 -0
  49. {edda_framework-0.3.1 → edda_framework-0.4.0}/docs/viewer-ui/images/start-workflow-dialog.png +0 -0
  50. {edda_framework-0.3.1 → edda_framework-0.4.0}/docs/viewer-ui/images/status-badges-example.png +0 -0
  51. {edda_framework-0.3.1 → edda_framework-0.4.0}/docs/viewer-ui/images/wait-event-visualization.png +0 -0
  52. {edda_framework-0.3.1 → edda_framework-0.4.0}/docs/viewer-ui/images/workflow-list-view.png +0 -0
  53. {edda_framework-0.3.1 → edda_framework-0.4.0}/docs/viewer-ui/setup.md +0 -0
  54. {edda_framework-0.3.1 → edda_framework-0.4.0}/docs/viewer-ui/visualization.md +0 -0
  55. {edda_framework-0.3.1 → edda_framework-0.4.0}/edda/__init__.py +0 -0
  56. {edda_framework-0.3.1 → edda_framework-0.4.0}/edda/activity.py +0 -0
  57. {edda_framework-0.3.1 → edda_framework-0.4.0}/edda/app.py +0 -0
  58. {edda_framework-0.3.1 → edda_framework-0.4.0}/edda/compensation.py +0 -0
  59. {edda_framework-0.3.1 → edda_framework-0.4.0}/edda/context.py +0 -0
  60. {edda_framework-0.3.1 → edda_framework-0.4.0}/edda/events.py +0 -0
  61. {edda_framework-0.3.1 → edda_framework-0.4.0}/edda/exceptions.py +0 -0
  62. {edda_framework-0.3.1 → edda_framework-0.4.0}/edda/hooks.py +0 -0
  63. {edda_framework-0.3.1 → edda_framework-0.4.0}/edda/integrations/__init__.py +0 -0
  64. {edda_framework-0.3.1 → edda_framework-0.4.0}/edda/integrations/mcp/__init__.py +0 -0
  65. {edda_framework-0.3.1 → edda_framework-0.4.0}/edda/locking.py +0 -0
  66. {edda_framework-0.3.1 → edda_framework-0.4.0}/edda/outbox/__init__.py +0 -0
  67. {edda_framework-0.3.1 → edda_framework-0.4.0}/edda/outbox/relayer.py +0 -0
  68. {edda_framework-0.3.1 → edda_framework-0.4.0}/edda/outbox/transactional.py +0 -0
  69. {edda_framework-0.3.1 → edda_framework-0.4.0}/edda/pydantic_utils.py +0 -0
  70. {edda_framework-0.3.1 → edda_framework-0.4.0}/edda/replay.py +0 -0
  71. {edda_framework-0.3.1 → edda_framework-0.4.0}/edda/retry.py +0 -0
  72. {edda_framework-0.3.1 → edda_framework-0.4.0}/edda/serialization/__init__.py +0 -0
  73. {edda_framework-0.3.1 → edda_framework-0.4.0}/edda/serialization/base.py +0 -0
  74. {edda_framework-0.3.1 → edda_framework-0.4.0}/edda/serialization/json.py +0 -0
  75. {edda_framework-0.3.1 → edda_framework-0.4.0}/edda/storage/__init__.py +0 -0
  76. {edda_framework-0.3.1 → edda_framework-0.4.0}/edda/storage/models.py +0 -0
  77. {edda_framework-0.3.1 → edda_framework-0.4.0}/edda/storage/protocol.py +0 -0
  78. {edda_framework-0.3.1 → edda_framework-0.4.0}/edda/storage/sqlalchemy_storage.py +0 -0
  79. {edda_framework-0.3.1 → edda_framework-0.4.0}/edda/viewer_ui/__init__.py +0 -0
  80. {edda_framework-0.3.1 → edda_framework-0.4.0}/edda/viewer_ui/app.py +0 -0
  81. {edda_framework-0.3.1 → edda_framework-0.4.0}/edda/viewer_ui/components.py +0 -0
  82. {edda_framework-0.3.1 → edda_framework-0.4.0}/edda/viewer_ui/data_service.py +0 -0
  83. {edda_framework-0.3.1 → edda_framework-0.4.0}/edda/visualizer/__init__.py +0 -0
  84. {edda_framework-0.3.1 → edda_framework-0.4.0}/edda/visualizer/ast_analyzer.py +0 -0
  85. {edda_framework-0.3.1 → edda_framework-0.4.0}/edda/visualizer/mermaid_generator.py +0 -0
  86. {edda_framework-0.3.1 → edda_framework-0.4.0}/edda/workflow.py +0 -0
  87. {edda_framework-0.3.1 → edda_framework-0.4.0}/edda/wsgi.py +0 -0
  88. {edda_framework-0.3.1 → edda_framework-0.4.0}/examples/__init__.py +0 -0
  89. {edda_framework-0.3.1 → edda_framework-0.4.0}/examples/cancellable_workflow.py +0 -0
  90. {edda_framework-0.3.1 → edda_framework-0.4.0}/examples/compensation_workflow.py +0 -0
  91. {edda_framework-0.3.1 → edda_framework-0.4.0}/examples/event_waiting_app.py +0 -0
  92. {edda_framework-0.3.1 → edda_framework-0.4.0}/examples/event_waiting_workflow.py +0 -0
  93. {edda_framework-0.3.1 → edda_framework-0.4.0}/examples/event_waiting_workflow_complete.py +0 -0
  94. {edda_framework-0.3.1 → edda_framework-0.4.0}/examples/mcp/README.md +0 -0
  95. {edda_framework-0.3.1 → edda_framework-0.4.0}/examples/mcp/order_processing_mcp.py +0 -0
  96. {edda_framework-0.3.1 → edda_framework-0.4.0}/examples/mcp/remote_server_example.py +0 -0
  97. {edda_framework-0.3.1 → edda_framework-0.4.0}/examples/mcp/simple_mcp_server.py +0 -0
  98. {edda_framework-0.3.1 → edda_framework-0.4.0}/examples/pydantic_saga.py +0 -0
  99. {edda_framework-0.3.1 → edda_framework-0.4.0}/examples/retry_example.py +0 -0
  100. {edda_framework-0.3.1 → edda_framework-0.4.0}/examples/retry_with_compensation.py +0 -0
  101. {edda_framework-0.3.1 → edda_framework-0.4.0}/examples/simple_workflow.py +0 -0
  102. {edda_framework-0.3.1 → edda_framework-0.4.0}/examples/typeddict_example.py +0 -0
  103. {edda_framework-0.3.1 → edda_framework-0.4.0}/examples/with_outbox.py +0 -0
  104. {edda_framework-0.3.1 → edda_framework-0.4.0}/tests/__init__.py +0 -0
  105. {edda_framework-0.3.1 → edda_framework-0.4.0}/tests/conftest.py +0 -0
  106. {edda_framework-0.3.1 → edda_framework-0.4.0}/tests/integrations/__init__.py +0 -0
  107. {edda_framework-0.3.1 → edda_framework-0.4.0}/tests/integrations/mcp/__init__.py +0 -0
  108. {edda_framework-0.3.1 → edda_framework-0.4.0}/tests/test_activity.py +0 -0
  109. {edda_framework-0.3.1 → edda_framework-0.4.0}/tests/test_activity_retry.py +0 -0
  110. {edda_framework-0.3.1 → edda_framework-0.4.0}/tests/test_activity_sync.py +0 -0
  111. {edda_framework-0.3.1 → edda_framework-0.4.0}/tests/test_app.py +0 -0
  112. {edda_framework-0.3.1 → edda_framework-0.4.0}/tests/test_ast_analyzer.py +0 -0
  113. {edda_framework-0.3.1 → edda_framework-0.4.0}/tests/test_atomic_wait_event.py +0 -0
  114. {edda_framework-0.3.1 → edda_framework-0.4.0}/tests/test_binary_data.py +0 -0
  115. {edda_framework-0.3.1 → edda_framework-0.4.0}/tests/test_cloudevents_http_binding.py +0 -0
  116. {edda_framework-0.3.1 → edda_framework-0.4.0}/tests/test_compensation.py +0 -0
  117. {edda_framework-0.3.1 → edda_framework-0.4.0}/tests/test_compensation_crash_recovery.py.wip +0 -0
  118. {edda_framework-0.3.1 → edda_framework-0.4.0}/tests/test_concurrent_outbox.py +0 -0
  119. {edda_framework-0.3.1 → edda_framework-0.4.0}/tests/test_context.py +0 -0
  120. {edda_framework-0.3.1 → edda_framework-0.4.0}/tests/test_ctx_session.py +0 -0
  121. {edda_framework-0.3.1 → edda_framework-0.4.0}/tests/test_distributed_event_delivery.py +0 -0
  122. {edda_framework-0.3.1 → edda_framework-0.4.0}/tests/test_events.py +0 -0
  123. {edda_framework-0.3.1 → edda_framework-0.4.0}/tests/test_lock_race_condition.py +0 -0
  124. {edda_framework-0.3.1 → edda_framework-0.4.0}/tests/test_lock_timeout_customization.py +0 -0
  125. {edda_framework-0.3.1 → edda_framework-0.4.0}/tests/test_locking.py +0 -0
  126. {edda_framework-0.3.1 → edda_framework-0.4.0}/tests/test_multidb_storage.py +0 -0
  127. {edda_framework-0.3.1 → edda_framework-0.4.0}/tests/test_outbox.py +0 -0
  128. {edda_framework-0.3.1 → edda_framework-0.4.0}/tests/test_pydantic_activity.py +0 -0
  129. {edda_framework-0.3.1 → edda_framework-0.4.0}/tests/test_pydantic_enum.py +0 -0
  130. {edda_framework-0.3.1 → edda_framework-0.4.0}/tests/test_pydantic_events.py +0 -0
  131. {edda_framework-0.3.1 → edda_framework-0.4.0}/tests/test_pydantic_saga.py +0 -0
  132. {edda_framework-0.3.1 → edda_framework-0.4.0}/tests/test_pydantic_utils.py +0 -0
  133. {edda_framework-0.3.1 → edda_framework-0.4.0}/tests/test_received_event.py +0 -0
  134. {edda_framework-0.3.1 → edda_framework-0.4.0}/tests/test_replay.py +0 -0
  135. {edda_framework-0.3.1 → edda_framework-0.4.0}/tests/test_retry_policy.py +0 -0
  136. {edda_framework-0.3.1 → edda_framework-0.4.0}/tests/test_saga_parameter_extraction.py +0 -0
  137. {edda_framework-0.3.1 → edda_framework-0.4.0}/tests/test_serialization.py +0 -0
  138. {edda_framework-0.3.1 → edda_framework-0.4.0}/tests/test_skip_locked.py +0 -0
  139. {edda_framework-0.3.1 → edda_framework-0.4.0}/tests/test_stale_workflow_recovery.py +0 -0
  140. {edda_framework-0.3.1 → edda_framework-0.4.0}/tests/test_storage.py +0 -0
  141. {edda_framework-0.3.1 → edda_framework-0.4.0}/tests/test_storage_mysql.py +0 -0
  142. {edda_framework-0.3.1 → edda_framework-0.4.0}/tests/test_storage_postgresql.py +0 -0
  143. {edda_framework-0.3.1 → edda_framework-0.4.0}/tests/test_transactions.py +0 -0
  144. {edda_framework-0.3.1 → edda_framework-0.4.0}/tests/test_viewer_pydantic_form.py +0 -0
  145. {edda_framework-0.3.1 → edda_framework-0.4.0}/tests/test_viewer_start_saga.py +0 -0
  146. {edda_framework-0.3.1 → edda_framework-0.4.0}/tests/test_wait_timer.py +0 -0
  147. {edda_framework-0.3.1 → edda_framework-0.4.0}/tests/test_workflow.py +0 -0
  148. {edda_framework-0.3.1 → edda_framework-0.4.0}/tests/test_workflow_auto_register.py +0 -0
  149. {edda_framework-0.3.1 → edda_framework-0.4.0}/tests/test_workflow_cancellation.py +0 -0
  150. {edda_framework-0.3.1 → edda_framework-0.4.0}/viewer_app.py +0 -0
  151. {edda_framework-0.3.1 → edda_framework-0.4.0}/zensical.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: edda-framework
3
- Version: 0.3.1
3
+ Version: 0.4.0
4
4
  Summary: Lightweight Durable Execution Framework
5
5
  Project-URL: Homepage, https://github.com/i2y/edda
6
6
  Project-URL: Documentation, https://github.com/i2y/edda#readme
@@ -730,6 +730,32 @@ Each `@durable_tool` automatically generates **three MCP tools**:
730
730
 
731
731
  This enables AI assistants to work with workflows that take minutes, hours, or even days to complete.
732
732
 
733
+ ### MCP Prompts
734
+
735
+ Define reusable prompt templates that can access workflow state:
736
+
737
+ ```python
738
+ from mcp.server.fastmcp.prompts.base import UserMessage
739
+ from mcp.types import TextContent
740
+
741
+ @server.prompt(description="Analyze a workflow execution")
742
+ async def analyze_workflow(instance_id: str) -> UserMessage:
743
+ """Generate analysis prompt for a specific workflow."""
744
+ instance = await server.storage.get_instance(instance_id)
745
+ history = await server.storage.get_history(instance_id)
746
+
747
+ text = f"""Analyze this workflow:
748
+ **Status**: {instance['status']}
749
+ **Activities**: {len(history)}
750
+ **Result**: {instance.get('output_data')}
751
+
752
+ Please provide insights and optimization suggestions."""
753
+
754
+ return UserMessage(content=TextContent(type="text", text=text))
755
+ ```
756
+
757
+ AI clients can use these prompts to generate context-aware analysis of your workflows.
758
+
733
759
  **For detailed documentation**, see [MCP Integration Guide](docs/integrations/mcp.md).
734
760
 
735
761
  ## Observability Hooks
@@ -678,6 +678,32 @@ Each `@durable_tool` automatically generates **three MCP tools**:
678
678
 
679
679
  This enables AI assistants to work with workflows that take minutes, hours, or even days to complete.
680
680
 
681
+ ### MCP Prompts
682
+
683
+ Define reusable prompt templates that can access workflow state:
684
+
685
+ ```python
686
+ from mcp.server.fastmcp.prompts.base import UserMessage
687
+ from mcp.types import TextContent
688
+
689
+ @server.prompt(description="Analyze a workflow execution")
690
+ async def analyze_workflow(instance_id: str) -> UserMessage:
691
+ """Generate analysis prompt for a specific workflow."""
692
+ instance = await server.storage.get_instance(instance_id)
693
+ history = await server.storage.get_history(instance_id)
694
+
695
+ text = f"""Analyze this workflow:
696
+ **Status**: {instance['status']}
697
+ **Activities**: {len(history)}
698
+ **Result**: {instance.get('output_data')}
699
+
700
+ Please provide insights and optimization suggestions."""
701
+
702
+ return UserMessage(content=TextContent(type="text", text=text))
703
+ ```
704
+
705
+ AI clients can use these prompts to generate context-aware analysis of your workflows.
706
+
681
707
  **For detailed documentation**, see [MCP Integration Guide](docs/integrations/mcp.md).
682
708
 
683
709
  ## Observability Hooks
@@ -492,10 +492,10 @@ async def order_processing_workflow(
492
492
  {"item_id": "ITEM-2", "name": "Product B", "price": 49.99, "quantity": 1}
493
493
  ],
494
494
  "shipping_address": {
495
- "street": "123 Main St",
496
- "city": "San Francisco",
497
- "state": "CA",
498
- "zip_code": "94102"
495
+ "street": "221B Baker Street",
496
+ "city": "London",
497
+ "state": "Greater London",
498
+ "zip_code": "NW1 6XE"
499
499
  }
500
500
  }
501
501
 
@@ -100,9 +100,9 @@ async def arrange_shipping(ctx: WorkflowContext, order_id: str):
100
100
  @workflow
101
101
  async def order_workflow(ctx: WorkflowContext, order_id: str):
102
102
  # Activity IDs are auto-generated for sequential calls
103
- inventory = await reserve_inventory(ctx, order_id, activity_id="reserve_inventory:1")
104
- payment = await process_payment(ctx, order_id, activity_id="process_payment:1")
105
- shipping = await arrange_shipping(ctx, order_id, activity_id="arrange_shipping:1")
103
+ inventory = await reserve_inventory(ctx, order_id)
104
+ payment = await process_payment(ctx, order_id)
105
+ shipping = await arrange_shipping(ctx, order_id)
106
106
 
107
107
  return {"status": "completed"}
108
108
  ```
@@ -497,16 +497,16 @@ async with workflow_lock(storage, instance_id, worker_id):
497
497
  ```python
498
498
  @workflow
499
499
  async def order_workflow(ctx: WorkflowContext, order_id: str):
500
- # Activity 1
501
- inventory = await reserve_inventory(ctx, order_id, activity_id="reserve_inventory:1")
500
+ # Activity 1 (auto-generated ID: "reserve_inventory:1")
501
+ inventory = await reserve_inventory(ctx, order_id)
502
502
  # → DB saved: activity_id="reserve_inventory:1", result={"reservation_id": "R123"}
503
503
 
504
- # Activity 2
505
- payment = await process_payment(ctx, order_id, activity_id="process_payment:1")
504
+ # Activity 2 (auto-generated ID: "process_payment:1")
505
+ payment = await process_payment(ctx, order_id)
506
506
  # → DB saved: activity_id="process_payment:1", result={"transaction_id": "T456"}
507
507
 
508
508
  # Activity 3: Exception occurs (e.g., network error)
509
- shipping = await arrange_shipping(ctx, order_id, activity_id="arrange_shipping:1")
509
+ shipping = await arrange_shipping(ctx, order_id)
510
510
  # → Exception thrown, workflow interrupted
511
511
  ```
512
512
 
@@ -147,8 +147,8 @@ async def async_activity(ctx: WorkflowContext, data: str) -> dict:
147
147
  async def mixed_workflow(ctx: WorkflowContext, user_id: str) -> dict:
148
148
  # Workflows are always async (for deterministic replay)
149
149
  # But can call both sync and async activities
150
- user = await create_user_record(ctx, user_id, "user@example.com", activity_id="create:1")
151
- data = await async_activity(ctx, user_id, activity_id="fetch:1")
150
+ user = await create_user_record(ctx, user_id, "user@example.com")
151
+ data = await async_activity(ctx, user_id)
152
152
  return {"user": user, "data": data}
153
153
  ```
154
154
 
@@ -823,8 +823,8 @@ def process_legacy_data(ctx: WorkflowContext, data: str) -> dict:
823
823
  @workflow
824
824
  async def order_workflow(ctx: WorkflowContext, order_id: str) -> dict:
825
825
  # Both sync and async activities work fine
826
- user = await create_user_record(ctx, order_id, activity_id="user:1") # Sync
827
- payment = await process_payment(ctx, 99.99, activity_id="pay:1") # Async
826
+ user = await create_user_record(ctx, order_id) # Sync
827
+ payment = await process_payment(ctx, 99.99) # Async
828
828
  return {"user": user, "payment": payment}
829
829
  ```
830
830
 
@@ -272,36 +272,40 @@ async def main():
272
272
  # Initialize the app (required before starting workflows)
273
273
  await app.initialize()
274
274
 
275
- # Create order input
276
- order = OrderInput(
277
- order_id="ORD-12345",
278
- customer_email="customer@example.com",
279
- items=[
280
- OrderItem(product_id="PROD-1", quantity=2, unit_price=29.99),
281
- OrderItem(product_id="PROD-2", quantity=1, unit_price=49.99),
282
- ],
283
- shipping_address=ShippingAddress(
284
- street="123 Main St",
285
- city="San Francisco",
286
- postal_code="94105",
287
- country="USA"
275
+ try:
276
+ # Create order input
277
+ order = OrderInput(
278
+ order_id="ORD-12345",
279
+ customer_email="customer@example.com",
280
+ items=[
281
+ OrderItem(product_id="PROD-1", quantity=2, unit_price=29.99),
282
+ OrderItem(product_id="PROD-2", quantity=1, unit_price=49.99),
283
+ ],
284
+ shipping_address=ShippingAddress(
285
+ street="1-2-3 Dogenzaka",
286
+ city="Shibuya",
287
+ postal_code="150-0001",
288
+ country="Japan"
289
+ )
288
290
  )
289
- )
290
291
 
291
- # Start workflow
292
- print("Starting order processing workflow...")
293
- instance_id = await order_processing_workflow.start(input=order)
292
+ # Start workflow
293
+ print("Starting order processing workflow...")
294
+ instance_id = await order_processing_workflow.start(input=order)
295
+
296
+ print(f"\n✅ Workflow started: {instance_id}")
294
297
 
295
- print(f"\n✅ Workflow started: {instance_id}")
298
+ # Get result
299
+ instance = await app.storage.get_instance(instance_id)
300
+ if instance["status"] == "completed":
301
+ result = instance["output_data"]
302
+ print(f"📊 Order completed:")
303
+ print(f" - Order ID: {result['order_id']}")
304
+ print(f" - Total: ${result['total_amount']:.2f}")
305
+ print(f" - Tracking: {result['confirmation_number']}")
296
306
 
297
- # Get result
298
- instance = await app.storage.get_instance(instance_id)
299
- if instance["status"] == "completed":
300
- result = instance["output_data"]
301
- print(f"📊 Order completed:")
302
- print(f" - Order ID: {result['order_id']}")
303
- print(f" - Total: ${result['total_amount']:.2f}")
304
- print(f" - Tracking: {result['confirmation_number']}")
307
+ finally:
308
+ await app.shutdown()
305
309
 
306
310
  if __name__ == "__main__":
307
311
  asyncio.run(main())
@@ -322,7 +326,7 @@ Starting order processing workflow...
322
326
 
323
327
  📦 Reserving inventory for ORD-12345: $109.97
324
328
  💳 Processing payment for ORD-12345: $109.97
325
- 🚚 Shipping ORD-12345 to San Francisco, USA
329
+ 🚚 Shipping ORD-12345 to Shibuya, Japan
326
330
 
327
331
  ✅ Workflow started: <instance_id>
328
332
  📊 Order completed:
@@ -364,7 +368,7 @@ uv run python order_workflow.py
364
368
  ```
365
369
  📦 Reserving inventory for ORD-12345: $109.97
366
370
  💳 Processing payment for ORD-12345: $109.97
367
- 🚚 Shipping ORD-12345 to San Francisco, USA
371
+ 🚚 Shipping ORD-12345 to Shibuya, Japan
368
372
  💥 Exception: Shipping service unavailable!
369
373
 
370
374
  ❌ Refunding payment for ORD-12345: $109.97
@@ -93,21 +93,22 @@ from edda import EddaApp, workflow, activity, WorkflowContext
93
93
 
94
94
  @activity
95
95
  async def process_payment(ctx: WorkflowContext, amount: float):
96
- # Durable execution - automatically recorded in history
97
96
  print(f"Processing payment: ${amount}")
98
97
  return {"status": "paid", "amount": amount}
99
98
 
100
99
  @workflow
101
100
  async def order_workflow(ctx: WorkflowContext, order_id: str, amount: float):
102
- # Workflow orchestrates activities with automatic retry on crash
103
101
  result = await process_payment(ctx, amount)
104
102
  return {"order_id": order_id, **result}
105
103
 
106
- # Simplified example - production code needs:
107
- # 1. await app.initialize() before starting workflows
108
- # 2. try-finally with await app.shutdown() for cleanup
109
- app = EddaApp(service_name="demo-service", db_url="sqlite:///workflow.db")
110
- instance_id = await order_workflow.start(order_id="ORD-123", amount=99.99)
104
+ async def main():
105
+ app = EddaApp(service_name="demo-service", db_url="sqlite:///workflow.db")
106
+ await app.initialize()
107
+ try:
108
+ instance_id = await order_workflow.start(order_id="ORD-123", amount=99.99)
109
+ print(f"Started workflow: {instance_id}")
110
+ finally:
111
+ await app.shutdown()
111
112
  ```
112
113
 
113
114
  **What happens on crash?**
@@ -6,11 +6,10 @@ import inspect
6
6
  from collections.abc import Callable
7
7
  from typing import TYPE_CHECKING, Any, cast
8
8
 
9
- from edda.workflow import workflow
9
+ from edda.workflow import Workflow, workflow
10
10
 
11
11
  if TYPE_CHECKING:
12
12
  from edda.integrations.mcp.server import EddaMCPServer
13
- from edda.workflow import Workflow
14
13
 
15
14
 
16
15
  def create_durable_tool(
@@ -98,7 +97,7 @@ def create_durable_tool(
98
97
  async def status_tool(instance_id: str) -> dict[str, Any]:
99
98
  """Check workflow status."""
100
99
  try:
101
- instance = await server._edda_app.storage.get_instance(instance_id)
100
+ instance = await server.storage.get_instance(instance_id)
102
101
  if instance is None:
103
102
  return {
104
103
  "content": [
@@ -142,7 +141,7 @@ def create_durable_tool(
142
141
  async def result_tool(instance_id: str) -> dict[str, Any]:
143
142
  """Get workflow result (if completed)."""
144
143
  try:
145
- instance = await server._edda_app.storage.get_instance(instance_id)
144
+ instance = await server.storage.get_instance(instance_id)
146
145
  if instance is None:
147
146
  return {
148
147
  "content": [
@@ -3,11 +3,14 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from collections.abc import Callable
6
- from typing import Any, cast
6
+ from typing import TYPE_CHECKING, Any, cast
7
7
 
8
8
  from edda.app import EddaApp
9
9
  from edda.workflow import Workflow
10
10
 
11
+ if TYPE_CHECKING:
12
+ from edda.storage.protocol import StorageProtocol
13
+
11
14
  try:
12
15
  from mcp.server.fastmcp import FastMCP # type: ignore[import-not-found]
13
16
  except ImportError as e:
@@ -45,7 +48,13 @@ class EddaMCPServer:
45
48
 
46
49
  # Deploy with uvicorn (HTTP transport)
47
50
  if __name__ == "__main__":
51
+ import asyncio
48
52
  import uvicorn
53
+
54
+ async def startup():
55
+ await server.initialize()
56
+
57
+ asyncio.run(startup())
49
58
  uvicorn.run(server.asgi_app(), host="0.0.0.0", port=8000)
50
59
 
51
60
  # Or deploy with stdio (for MCP clients, e.g., Claude Desktop)
@@ -97,6 +106,22 @@ class EddaMCPServer:
97
106
  # Registry of durable tools (workflow_name -> Workflow instance)
98
107
  self._workflows: dict[str, Workflow] = {}
99
108
 
109
+ @property
110
+ def storage(self) -> StorageProtocol:
111
+ """
112
+ Access workflow storage for querying instances and history.
113
+
114
+ Returns:
115
+ StorageProtocol: Storage backend for workflow state
116
+
117
+ Example:
118
+ ```python
119
+ instance = await server.storage.get_instance(instance_id)
120
+ history = await server.storage.get_history(instance_id)
121
+ ```
122
+ """
123
+ return self._edda_app.storage
124
+
100
125
  def durable_tool(
101
126
  self,
102
127
  func: Callable[..., Any] | None = None,
@@ -135,6 +160,59 @@ class EddaMCPServer:
135
160
  return decorator
136
161
  return decorator(func)
137
162
 
163
+ def prompt(
164
+ self,
165
+ func: Callable[..., Any] | None = None,
166
+ *,
167
+ description: str = "",
168
+ ) -> Callable[..., Any]:
169
+ """
170
+ Decorator to define a prompt template.
171
+
172
+ Prompts can access workflow state to generate dynamic, context-aware
173
+ prompts for AI clients (Claude Desktop, etc.).
174
+
175
+ Args:
176
+ func: Prompt function (async or sync)
177
+ description: Prompt description for MCP clients
178
+
179
+ Returns:
180
+ Decorated function
181
+
182
+ Example:
183
+ ```python
184
+ from fastmcp.prompts.prompt import PromptMessage, TextContent
185
+
186
+ @server.prompt(description="Analyze workflow results")
187
+ async def analyze_workflow(instance_id: str) -> PromptMessage:
188
+ '''Generate a prompt to analyze a specific workflow execution.'''
189
+ instance = await server.storage.get_instance(instance_id)
190
+ history = await server.storage.get_history(instance_id)
191
+
192
+ text = f'''Analyze this workflow:
193
+
194
+ Instance ID: {instance_id}
195
+ Status: {instance['status']}
196
+ Activities: {len(history)}
197
+
198
+ Please identify any issues or optimization opportunities.'''
199
+
200
+ return PromptMessage(
201
+ role="user",
202
+ content=TextContent(type="text", text=text)
203
+ )
204
+ ```
205
+ """
206
+
207
+ def decorator(f: Callable[..., Any]) -> Callable[..., Any]:
208
+ # Use FastMCP's native prompt decorator
209
+ prompt_desc = description or f.__doc__ or f"Prompt: {f.__name__}"
210
+ return cast(Callable[..., Any], self._mcp.prompt(description=prompt_desc)(f))
211
+
212
+ if func is None:
213
+ return decorator
214
+ return decorator(func)
215
+
138
216
  def asgi_app(self) -> Callable[..., Any]:
139
217
  """
140
218
  Create ASGI application with MCP + CloudEvents support.
@@ -221,11 +299,9 @@ class EddaMCPServer:
221
299
  """
222
300
  Initialize the EddaApp (setup replay engine, storage, etc.).
223
301
 
224
- This method must be called before running the server in stdio mode.
225
- For HTTP mode (asgi_app()), initialization happens automatically
226
- when the ASGI app is deployed.
302
+ This method must be called before running the server in either stdio or HTTP mode.
227
303
 
228
- Example:
304
+ Example (stdio mode):
229
305
  ```python
230
306
  async def main():
231
307
  await server.initialize()
@@ -235,9 +311,85 @@ class EddaMCPServer:
235
311
  import asyncio
236
312
  asyncio.run(main())
237
313
  ```
314
+
315
+ Example (HTTP mode):
316
+ ```python
317
+ import asyncio
318
+ import uvicorn
319
+
320
+ async def startup():
321
+ await server.initialize()
322
+
323
+ asyncio.run(startup())
324
+ uvicorn.run(server.asgi_app(), host="0.0.0.0", port=8000)
325
+ ```
238
326
  """
239
327
  await self._edda_app.initialize()
240
328
 
329
+ async def shutdown(self) -> None:
330
+ """
331
+ Shutdown the server and cleanup resources.
332
+
333
+ Stops background tasks (auto-resume, timer checks, event timeouts),
334
+ closes storage connections, and performs graceful shutdown.
335
+
336
+ This method should be called when the server is shutting down.
337
+
338
+ Example (stdio mode):
339
+ ```python
340
+ import signal
341
+ import asyncio
342
+
343
+ async def main():
344
+ server = EddaMCPServer(...)
345
+ await server.initialize()
346
+
347
+ # Setup signal handlers for graceful shutdown
348
+ loop = asyncio.get_running_loop()
349
+ shutdown_event = asyncio.Event()
350
+
351
+ def signal_handler():
352
+ shutdown_event.set()
353
+
354
+ for sig in (signal.SIGTERM, signal.SIGINT):
355
+ loop.add_signal_handler(sig, signal_handler)
356
+
357
+ # Run server
358
+ try:
359
+ await server.run_stdio()
360
+ finally:
361
+ await server.shutdown()
362
+
363
+ if __name__ == "__main__":
364
+ asyncio.run(main())
365
+ ```
366
+
367
+ Example (HTTP mode with uvicorn):
368
+ ```python
369
+ import asyncio
370
+ import uvicorn
371
+
372
+ async def startup():
373
+ await server.initialize()
374
+
375
+ async def shutdown_handler():
376
+ await server.shutdown()
377
+
378
+ # Use uvicorn lifecycle events
379
+ config = uvicorn.Config(
380
+ server.asgi_app(),
381
+ host="0.0.0.0",
382
+ port=8000,
383
+ )
384
+ server_instance = uvicorn.Server(config)
385
+
386
+ # Uvicorn handles SIGTERM/SIGINT automatically
387
+ await server_instance.serve()
388
+ await shutdown_handler()
389
+ ```
390
+ """
391
+ await self._edda_app.shutdown()
392
+
241
393
  async def run_stdio(self) -> None:
242
394
  """
243
395
  Run MCP server with stdio transport (for MCP clients, e.g., Claude Desktop).