edda-framework 0.5.0__tar.gz → 0.7.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 (168) hide show
  1. {edda_framework-0.5.0 → edda_framework-0.7.0}/.github/workflows/ci.yml +2 -2
  2. {edda_framework-0.5.0 → edda_framework-0.7.0}/.gitignore +1 -0
  3. {edda_framework-0.5.0 → edda_framework-0.7.0}/PKG-INFO +8 -5
  4. {edda_framework-0.5.0 → edda_framework-0.7.0}/README.md +5 -4
  5. {edda_framework-0.5.0 → edda_framework-0.7.0}/docs/index.md +1 -0
  6. {edda_framework-0.5.0 → edda_framework-0.7.0}/docs/integrations/mcp.md +32 -4
  7. {edda_framework-0.5.0 → edda_framework-0.7.0}/docs/integrations/opentelemetry.md +4 -0
  8. edda_framework-0.7.0/docs/viewer-ui/images/compensation-execution.png +0 -0
  9. edda_framework-0.7.0/docs/viewer-ui/images/detail-page-loan-approval.png +0 -0
  10. edda_framework-0.7.0/docs/viewer-ui/images/detail-page-match-case.png +0 -0
  11. edda_framework-0.7.0/docs/viewer-ui/images/nested-pydantic-form.png +0 -0
  12. edda_framework-0.7.0/docs/viewer-ui/images/start-workflow-form-pydantic.png +0 -0
  13. edda_framework-0.7.0/docs/viewer-ui/images/wait-event-visualization.png +0 -0
  14. edda_framework-0.7.0/docs/viewer-ui/images/workflow-list-view.png +0 -0
  15. edda_framework-0.7.0/docs/viewer-ui/images/workflow-selection-dropdown.png +0 -0
  16. {edda_framework-0.5.0 → edda_framework-0.7.0}/docs/viewer-ui/setup.md +11 -1
  17. {edda_framework-0.5.0 → edda_framework-0.7.0}/docs/viewer-ui/visualization.md +38 -33
  18. {edda_framework-0.5.0 → edda_framework-0.7.0}/edda/integrations/mcp/decorators.py +101 -5
  19. {edda_framework-0.5.0 → edda_framework-0.7.0}/edda/integrations/mcp/server.py +36 -15
  20. {edda_framework-0.5.0 → edda_framework-0.7.0}/edda/storage/protocol.py +18 -4
  21. {edda_framework-0.5.0 → edda_framework-0.7.0}/edda/storage/sqlalchemy_storage.py +105 -5
  22. {edda_framework-0.5.0 → edda_framework-0.7.0}/edda/viewer_ui/app.py +552 -126
  23. {edda_framework-0.5.0 → edda_framework-0.7.0}/edda/viewer_ui/components.py +81 -68
  24. {edda_framework-0.5.0 → edda_framework-0.7.0}/edda/viewer_ui/data_service.py +42 -3
  25. edda_framework-0.7.0/edda/viewer_ui/theme.py +200 -0
  26. {edda_framework-0.5.0 → edda_framework-0.7.0}/pyproject.toml +4 -2
  27. edda_framework-0.7.0/tests/integrations/mcp/test_cancel.py +166 -0
  28. {edda_framework-0.5.0 → edda_framework-0.7.0}/tests/integrations/mcp/test_integration.py +2 -2
  29. {edda_framework-0.5.0 → edda_framework-0.7.0}/tests/integrations/mcp/test_jsonrpc.py +1 -1
  30. {edda_framework-0.5.0 → edda_framework-0.7.0}/tests/integrations/mcp/test_prompts.py +3 -3
  31. {edda_framework-0.5.0 → edda_framework-0.7.0}/tests/integrations/mcp/test_server.py +80 -1
  32. edda_framework-0.7.0/tests/test_viewer_pagination.py +318 -0
  33. {edda_framework-0.5.0 → edda_framework-0.7.0}/uv.lock +21 -17
  34. {edda_framework-0.5.0 → edda_framework-0.7.0}/zensical.toml +8 -9
  35. edda_framework-0.5.0/docs/markdown.md +0 -98
  36. edda_framework-0.5.0/docs/viewer-ui/images/compensation-execution.png +0 -0
  37. edda_framework-0.5.0/docs/viewer-ui/images/conditional-branching-diagram.png +0 -0
  38. edda_framework-0.5.0/docs/viewer-ui/images/detail-overview-panel.png +0 -0
  39. edda_framework-0.5.0/docs/viewer-ui/images/execution-history-panel.png +0 -0
  40. edda_framework-0.5.0/docs/viewer-ui/images/form-generation-example.png +0 -0
  41. edda_framework-0.5.0/docs/viewer-ui/images/hybrid-diagram-example.png +0 -0
  42. edda_framework-0.5.0/docs/viewer-ui/images/nested-pydantic-form.png +0 -0
  43. edda_framework-0.5.0/docs/viewer-ui/images/start-workflow-dialog.png +0 -0
  44. edda_framework-0.5.0/docs/viewer-ui/images/status-badges-example.png +0 -0
  45. edda_framework-0.5.0/docs/viewer-ui/images/wait-event-visualization.png +0 -0
  46. edda_framework-0.5.0/docs/viewer-ui/images/workflow-list-view.png +0 -0
  47. {edda_framework-0.5.0 → edda_framework-0.7.0}/.github/workflows/docs.yml +0 -0
  48. {edda_framework-0.5.0 → edda_framework-0.7.0}/.github/workflows/release.yml +0 -0
  49. {edda_framework-0.5.0 → edda_framework-0.7.0}/.python-version +0 -0
  50. {edda_framework-0.5.0 → edda_framework-0.7.0}/Justfile +0 -0
  51. {edda_framework-0.5.0 → edda_framework-0.7.0}/LICENSE +0 -0
  52. {edda_framework-0.5.0 → edda_framework-0.7.0}/demo_app.py +0 -0
  53. {edda_framework-0.5.0 → edda_framework-0.7.0}/docs/core-features/durable-execution/replay.md +0 -0
  54. {edda_framework-0.5.0 → edda_framework-0.7.0}/docs/core-features/events/cloudevents-http-binding.md +0 -0
  55. {edda_framework-0.5.0 → edda_framework-0.7.0}/docs/core-features/events/wait-event.md +0 -0
  56. {edda_framework-0.5.0 → edda_framework-0.7.0}/docs/core-features/hooks.md +0 -0
  57. {edda_framework-0.5.0 → edda_framework-0.7.0}/docs/core-features/retry.md +0 -0
  58. {edda_framework-0.5.0 → edda_framework-0.7.0}/docs/core-features/saga-compensation.md +0 -0
  59. {edda_framework-0.5.0 → edda_framework-0.7.0}/docs/core-features/transactional-outbox.md +0 -0
  60. {edda_framework-0.5.0 → edda_framework-0.7.0}/docs/core-features/workflows-activities.md +0 -0
  61. {edda_framework-0.5.0 → edda_framework-0.7.0}/docs/examples/ecommerce.md +0 -0
  62. {edda_framework-0.5.0 → edda_framework-0.7.0}/docs/examples/events.md +0 -0
  63. {edda_framework-0.5.0 → edda_framework-0.7.0}/docs/examples/fastapi-integration.md +0 -0
  64. {edda_framework-0.5.0 → edda_framework-0.7.0}/docs/examples/saga.md +0 -0
  65. {edda_framework-0.5.0 → edda_framework-0.7.0}/docs/examples/simple.md +0 -0
  66. {edda_framework-0.5.0 → edda_framework-0.7.0}/docs/getting-started/concepts.md +0 -0
  67. {edda_framework-0.5.0 → edda_framework-0.7.0}/docs/getting-started/first-workflow.md +0 -0
  68. {edda_framework-0.5.0 → edda_framework-0.7.0}/docs/getting-started/installation.md +0 -0
  69. {edda_framework-0.5.0 → edda_framework-0.7.0}/docs/getting-started/quick-start.md +0 -0
  70. {edda_framework-0.5.0 → edda_framework-0.7.0}/docs/viewer-ui/images/cloudevents-cli-trigger.png +0 -0
  71. {edda_framework-0.5.0 → edda_framework-0.7.0}/edda/__init__.py +0 -0
  72. {edda_framework-0.5.0 → edda_framework-0.7.0}/edda/activity.py +0 -0
  73. {edda_framework-0.5.0 → edda_framework-0.7.0}/edda/app.py +0 -0
  74. {edda_framework-0.5.0 → edda_framework-0.7.0}/edda/compensation.py +0 -0
  75. {edda_framework-0.5.0 → edda_framework-0.7.0}/edda/context.py +0 -0
  76. {edda_framework-0.5.0 → edda_framework-0.7.0}/edda/events.py +0 -0
  77. {edda_framework-0.5.0 → edda_framework-0.7.0}/edda/exceptions.py +0 -0
  78. {edda_framework-0.5.0 → edda_framework-0.7.0}/edda/hooks.py +0 -0
  79. {edda_framework-0.5.0 → edda_framework-0.7.0}/edda/integrations/__init__.py +0 -0
  80. {edda_framework-0.5.0 → edda_framework-0.7.0}/edda/integrations/mcp/__init__.py +0 -0
  81. {edda_framework-0.5.0 → edda_framework-0.7.0}/edda/integrations/opentelemetry/__init__.py +0 -0
  82. {edda_framework-0.5.0 → edda_framework-0.7.0}/edda/integrations/opentelemetry/hooks.py +0 -0
  83. {edda_framework-0.5.0 → edda_framework-0.7.0}/edda/locking.py +0 -0
  84. {edda_framework-0.5.0 → edda_framework-0.7.0}/edda/outbox/__init__.py +0 -0
  85. {edda_framework-0.5.0 → edda_framework-0.7.0}/edda/outbox/relayer.py +0 -0
  86. {edda_framework-0.5.0 → edda_framework-0.7.0}/edda/outbox/transactional.py +0 -0
  87. {edda_framework-0.5.0 → edda_framework-0.7.0}/edda/pydantic_utils.py +0 -0
  88. {edda_framework-0.5.0 → edda_framework-0.7.0}/edda/replay.py +0 -0
  89. {edda_framework-0.5.0 → edda_framework-0.7.0}/edda/retry.py +0 -0
  90. {edda_framework-0.5.0 → edda_framework-0.7.0}/edda/serialization/__init__.py +0 -0
  91. {edda_framework-0.5.0 → edda_framework-0.7.0}/edda/serialization/base.py +0 -0
  92. {edda_framework-0.5.0 → edda_framework-0.7.0}/edda/serialization/json.py +0 -0
  93. {edda_framework-0.5.0 → edda_framework-0.7.0}/edda/storage/__init__.py +0 -0
  94. {edda_framework-0.5.0 → edda_framework-0.7.0}/edda/storage/models.py +0 -0
  95. {edda_framework-0.5.0 → edda_framework-0.7.0}/edda/viewer_ui/__init__.py +0 -0
  96. {edda_framework-0.5.0 → edda_framework-0.7.0}/edda/visualizer/__init__.py +0 -0
  97. {edda_framework-0.5.0 → edda_framework-0.7.0}/edda/visualizer/ast_analyzer.py +0 -0
  98. {edda_framework-0.5.0 → edda_framework-0.7.0}/edda/visualizer/mermaid_generator.py +0 -0
  99. {edda_framework-0.5.0 → edda_framework-0.7.0}/edda/workflow.py +0 -0
  100. {edda_framework-0.5.0 → edda_framework-0.7.0}/edda/wsgi.py +0 -0
  101. {edda_framework-0.5.0 → edda_framework-0.7.0}/examples/__init__.py +0 -0
  102. {edda_framework-0.5.0 → edda_framework-0.7.0}/examples/cancellable_workflow.py +0 -0
  103. {edda_framework-0.5.0 → edda_framework-0.7.0}/examples/compensation_workflow.py +0 -0
  104. {edda_framework-0.5.0 → edda_framework-0.7.0}/examples/event_waiting_app.py +0 -0
  105. {edda_framework-0.5.0 → edda_framework-0.7.0}/examples/event_waiting_workflow.py +0 -0
  106. {edda_framework-0.5.0 → edda_framework-0.7.0}/examples/event_waiting_workflow_complete.py +0 -0
  107. {edda_framework-0.5.0 → edda_framework-0.7.0}/examples/mcp/README.md +0 -0
  108. {edda_framework-0.5.0 → edda_framework-0.7.0}/examples/mcp/order_processing_mcp.py +0 -0
  109. {edda_framework-0.5.0 → edda_framework-0.7.0}/examples/mcp/prompts_example.py +0 -0
  110. {edda_framework-0.5.0 → edda_framework-0.7.0}/examples/mcp/remote_server_example.py +0 -0
  111. {edda_framework-0.5.0 → edda_framework-0.7.0}/examples/mcp/simple_mcp_server.py +0 -0
  112. {edda_framework-0.5.0 → edda_framework-0.7.0}/examples/observability_with_logfire.py +0 -0
  113. {edda_framework-0.5.0 → edda_framework-0.7.0}/examples/observability_with_opentelemetry.py +0 -0
  114. {edda_framework-0.5.0 → edda_framework-0.7.0}/examples/pydantic_saga.py +0 -0
  115. {edda_framework-0.5.0 → edda_framework-0.7.0}/examples/retry_example.py +0 -0
  116. {edda_framework-0.5.0 → edda_framework-0.7.0}/examples/retry_with_compensation.py +0 -0
  117. {edda_framework-0.5.0 → edda_framework-0.7.0}/examples/simple_workflow.py +0 -0
  118. {edda_framework-0.5.0 → edda_framework-0.7.0}/examples/typeddict_example.py +0 -0
  119. {edda_framework-0.5.0 → edda_framework-0.7.0}/examples/with_outbox.py +0 -0
  120. {edda_framework-0.5.0 → edda_framework-0.7.0}/tests/__init__.py +0 -0
  121. {edda_framework-0.5.0 → edda_framework-0.7.0}/tests/conftest.py +0 -0
  122. {edda_framework-0.5.0 → edda_framework-0.7.0}/tests/integrations/__init__.py +0 -0
  123. {edda_framework-0.5.0 → edda_framework-0.7.0}/tests/integrations/mcp/__init__.py +0 -0
  124. {edda_framework-0.5.0 → edda_framework-0.7.0}/tests/integrations/opentelemetry/__init__.py +0 -0
  125. {edda_framework-0.5.0 → edda_framework-0.7.0}/tests/integrations/opentelemetry/test_hooks.py +0 -0
  126. {edda_framework-0.5.0 → edda_framework-0.7.0}/tests/test_activity.py +0 -0
  127. {edda_framework-0.5.0 → edda_framework-0.7.0}/tests/test_activity_retry.py +0 -0
  128. {edda_framework-0.5.0 → edda_framework-0.7.0}/tests/test_activity_sync.py +0 -0
  129. {edda_framework-0.5.0 → edda_framework-0.7.0}/tests/test_app.py +0 -0
  130. {edda_framework-0.5.0 → edda_framework-0.7.0}/tests/test_ast_analyzer.py +0 -0
  131. {edda_framework-0.5.0 → edda_framework-0.7.0}/tests/test_atomic_wait_event.py +0 -0
  132. {edda_framework-0.5.0 → edda_framework-0.7.0}/tests/test_binary_data.py +0 -0
  133. {edda_framework-0.5.0 → edda_framework-0.7.0}/tests/test_cloudevents_http_binding.py +0 -0
  134. {edda_framework-0.5.0 → edda_framework-0.7.0}/tests/test_compensation.py +0 -0
  135. {edda_framework-0.5.0 → edda_framework-0.7.0}/tests/test_compensation_crash_recovery.py.wip +0 -0
  136. {edda_framework-0.5.0 → edda_framework-0.7.0}/tests/test_concurrent_outbox.py +0 -0
  137. {edda_framework-0.5.0 → edda_framework-0.7.0}/tests/test_context.py +0 -0
  138. {edda_framework-0.5.0 → edda_framework-0.7.0}/tests/test_ctx_session.py +0 -0
  139. {edda_framework-0.5.0 → edda_framework-0.7.0}/tests/test_distributed_event_delivery.py +0 -0
  140. {edda_framework-0.5.0 → edda_framework-0.7.0}/tests/test_events.py +0 -0
  141. {edda_framework-0.5.0 → edda_framework-0.7.0}/tests/test_lock_race_condition.py +0 -0
  142. {edda_framework-0.5.0 → edda_framework-0.7.0}/tests/test_lock_timeout_customization.py +0 -0
  143. {edda_framework-0.5.0 → edda_framework-0.7.0}/tests/test_locking.py +0 -0
  144. {edda_framework-0.5.0 → edda_framework-0.7.0}/tests/test_multidb_storage.py +0 -0
  145. {edda_framework-0.5.0 → edda_framework-0.7.0}/tests/test_outbox.py +0 -0
  146. {edda_framework-0.5.0 → edda_framework-0.7.0}/tests/test_pydantic_activity.py +0 -0
  147. {edda_framework-0.5.0 → edda_framework-0.7.0}/tests/test_pydantic_enum.py +0 -0
  148. {edda_framework-0.5.0 → edda_framework-0.7.0}/tests/test_pydantic_events.py +0 -0
  149. {edda_framework-0.5.0 → edda_framework-0.7.0}/tests/test_pydantic_saga.py +0 -0
  150. {edda_framework-0.5.0 → edda_framework-0.7.0}/tests/test_pydantic_utils.py +0 -0
  151. {edda_framework-0.5.0 → edda_framework-0.7.0}/tests/test_received_event.py +0 -0
  152. {edda_framework-0.5.0 → edda_framework-0.7.0}/tests/test_replay.py +0 -0
  153. {edda_framework-0.5.0 → edda_framework-0.7.0}/tests/test_retry_policy.py +0 -0
  154. {edda_framework-0.5.0 → edda_framework-0.7.0}/tests/test_saga_parameter_extraction.py +0 -0
  155. {edda_framework-0.5.0 → edda_framework-0.7.0}/tests/test_serialization.py +0 -0
  156. {edda_framework-0.5.0 → edda_framework-0.7.0}/tests/test_skip_locked.py +0 -0
  157. {edda_framework-0.5.0 → edda_framework-0.7.0}/tests/test_stale_workflow_recovery.py +0 -0
  158. {edda_framework-0.5.0 → edda_framework-0.7.0}/tests/test_storage.py +0 -0
  159. {edda_framework-0.5.0 → edda_framework-0.7.0}/tests/test_storage_mysql.py +0 -0
  160. {edda_framework-0.5.0 → edda_framework-0.7.0}/tests/test_storage_postgresql.py +0 -0
  161. {edda_framework-0.5.0 → edda_framework-0.7.0}/tests/test_transactions.py +0 -0
  162. {edda_framework-0.5.0 → edda_framework-0.7.0}/tests/test_viewer_pydantic_form.py +0 -0
  163. {edda_framework-0.5.0 → edda_framework-0.7.0}/tests/test_viewer_start_saga.py +0 -0
  164. {edda_framework-0.5.0 → edda_framework-0.7.0}/tests/test_wait_timer.py +0 -0
  165. {edda_framework-0.5.0 → edda_framework-0.7.0}/tests/test_workflow.py +0 -0
  166. {edda_framework-0.5.0 → edda_framework-0.7.0}/tests/test_workflow_auto_register.py +0 -0
  167. {edda_framework-0.5.0 → edda_framework-0.7.0}/tests/test_workflow_cancellation.py +0 -0
  168. {edda_framework-0.5.0 → edda_framework-0.7.0}/viewer_app.py +0 -0
@@ -53,8 +53,8 @@ jobs:
53
53
  - name: Set up Python ${{ matrix.python-version }}
54
54
  run: uv python install ${{ matrix.python-version }}
55
55
 
56
- - name: Install dependencies (all database drivers)
57
- run: uv sync --extra dev --extra postgresql --extra mysql
56
+ - name: Install dependencies (all database drivers and opentelemetry)
57
+ run: uv sync --extra dev --extra postgresql --extra mysql --extra opentelemetry
58
58
 
59
59
  - name: Run tests (all databases)
60
60
  env:
@@ -99,3 +99,4 @@ site/
99
99
 
100
100
  # Cache directories
101
101
  .cache/
102
+ .nicegui/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: edda-framework
3
- Version: 0.5.0
3
+ Version: 0.7.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
@@ -30,11 +30,13 @@ Requires-Dist: sqlalchemy[asyncio]>=2.0.0
30
30
  Requires-Dist: uvloop>=0.22.1
31
31
  Provides-Extra: dev
32
32
  Requires-Dist: black>=25.9.0; extra == 'dev'
33
+ Requires-Dist: mcp>=1.22.0; extra == 'dev'
33
34
  Requires-Dist: mypy>=1.18.2; extra == 'dev'
34
35
  Requires-Dist: pytest-asyncio>=1.2.0; extra == 'dev'
35
36
  Requires-Dist: pytest-cov>=7.0.0; extra == 'dev'
36
37
  Requires-Dist: pytest>=8.4.2; extra == 'dev'
37
38
  Requires-Dist: ruff>=0.14.2; extra == 'dev'
39
+ Requires-Dist: starlette>=0.40.0; extra == 'dev'
38
40
  Requires-Dist: testcontainers[mysql]>=4.0.0; extra == 'dev'
39
41
  Requires-Dist: testcontainers[postgres]>=4.0.0; extra == 'dev'
40
42
  Requires-Dist: tsuno>=0.1.3; extra == 'dev'
@@ -709,7 +711,7 @@ def process_payment(ctx: WorkflowContext, amount: float) -> dict:
709
711
  @workflow
710
712
  async def payment_workflow(ctx: WorkflowContext, order_id: str) -> dict:
711
713
  # Workflows still use async (for deterministic replay)
712
- result = await process_payment(ctx, 99.99, activity_id="pay:1")
714
+ result = await process_payment(ctx, 99.99)
713
715
  return result
714
716
  ```
715
717
 
@@ -748,13 +750,14 @@ if __name__ == "__main__":
748
750
 
749
751
  ### Auto-Generated Tools
750
752
 
751
- Each `@durable_tool` automatically generates **three MCP tools**:
753
+ Each `@durable_tool` automatically generates **four MCP tools**:
752
754
 
753
755
  1. **Main tool** (`process_order`): Starts the workflow, returns instance ID
754
- 2. **Status tool** (`process_order_status`): Checks workflow progress
756
+ 2. **Status tool** (`process_order_status`): Checks workflow progress with completed activity count and suggested poll interval
755
757
  3. **Result tool** (`process_order_result`): Gets final result when completed
758
+ 4. **Cancel tool** (`process_order_cancel`): Cancels workflow if running or waiting, executes compensation handlers
756
759
 
757
- This enables AI assistants to work with workflows that take minutes, hours, or even days to complete.
760
+ This enables AI assistants to work with workflows that take minutes, hours, or even days to complete, with full control over the workflow lifecycle.
758
761
 
759
762
  ### MCP Prompts
760
763
 
@@ -653,7 +653,7 @@ def process_payment(ctx: WorkflowContext, amount: float) -> dict:
653
653
  @workflow
654
654
  async def payment_workflow(ctx: WorkflowContext, order_id: str) -> dict:
655
655
  # Workflows still use async (for deterministic replay)
656
- result = await process_payment(ctx, 99.99, activity_id="pay:1")
656
+ result = await process_payment(ctx, 99.99)
657
657
  return result
658
658
  ```
659
659
 
@@ -692,13 +692,14 @@ if __name__ == "__main__":
692
692
 
693
693
  ### Auto-Generated Tools
694
694
 
695
- Each `@durable_tool` automatically generates **three MCP tools**:
695
+ Each `@durable_tool` automatically generates **four MCP tools**:
696
696
 
697
697
  1. **Main tool** (`process_order`): Starts the workflow, returns instance ID
698
- 2. **Status tool** (`process_order_status`): Checks workflow progress
698
+ 2. **Status tool** (`process_order_status`): Checks workflow progress with completed activity count and suggested poll interval
699
699
  3. **Result tool** (`process_order_result`): Gets final result when completed
700
+ 4. **Cancel tool** (`process_order_cancel`): Cancels workflow if running or waiting, executes compensation handlers
700
701
 
701
- This enables AI assistants to work with workflows that take minutes, hours, or even days to complete.
702
+ This enables AI assistants to work with workflows that take minutes, hours, or even days to complete, with full control over the workflow lifecycle.
702
703
 
703
704
  ### MCP Prompts
704
705
 
@@ -46,6 +46,7 @@ Edda's waiting functions make it ideal for time-based and event-driven business
46
46
  - **📦 Scheduled Notifications**: Shipping updates, subscription renewals, appointment reminders
47
47
 
48
48
  **Waiting functions**:
49
+
49
50
  - `wait_timer(duration_seconds)`: Wait for a relative duration
50
51
  - `wait_until(until_time)`: Wait until an absolute datetime (e.g., campaign end date)
51
52
  - `wait_event(event_type)`: Wait for external events (near real-time response)
@@ -7,10 +7,11 @@ Edda provides seamless integration with the [Model Context Protocol (MCP)](https
7
7
  MCP is a standardized protocol for AI tool integration. Edda's MCP integration automatically converts your durable workflows into MCP-compliant tools that:
8
8
 
9
9
  - **Start workflows** and return instance IDs immediately
10
- - **Check workflow status** to monitor progress
10
+ - **Check workflow status** to monitor progress with completed activity count and suggested poll interval
11
11
  - **Retrieve results** when workflows complete
12
+ - **Cancel workflows** if running or waiting, with automatic compensation execution
12
13
 
13
- This enables AI assistants to work with long-running processes that may take minutes, hours, or even days to complete.
14
+ This enables AI assistants to work with long-running processes that may take minutes, hours, or even days to complete, with full control over the workflow lifecycle.
14
15
 
15
16
  ## Installation
16
17
 
@@ -97,7 +98,7 @@ Add to your MCP client configuration (e.g., Claude Desktop: `~/Library/Applicati
97
98
 
98
99
  ## Auto-Generated Tools
99
100
 
100
- Each `@durable_tool` automatically generates **three MCP tools**:
101
+ Each `@durable_tool` automatically generates **four MCP tools**:
101
102
 
102
103
  ### 1. Main Tool: Start Workflow
103
104
 
@@ -125,12 +126,17 @@ Input: {"instance_id": "abc123..."}
125
126
  Output: {
126
127
  "content": [{
127
128
  "type": "text",
128
- "text": "Workflow Status: running\nCurrent Activity: payment:1\nInstance ID: abc123..."
129
+ "text": "Workflow Status: running\nCurrent Activity: payment:1\nCompleted Activities: 1\nSuggested Poll Interval: 5000ms\nInstance ID: abc123..."
129
130
  }],
130
131
  "isError": false
131
132
  }
132
133
  ```
133
134
 
135
+ The status tool provides progress metadata for efficient polling:
136
+
137
+ - **Completed Activities**: Number of activities that have finished
138
+ - **Suggested Poll Interval**: Recommended wait time before checking again (5000ms for running, 10000ms for waiting)
139
+
134
140
  ### 3. Result Tool: Get Final Result
135
141
 
136
142
  ```
@@ -147,6 +153,28 @@ Output: {
147
153
  }
148
154
  ```
149
155
 
156
+ ### 4. Cancel Tool: Stop Workflow
157
+
158
+ ```
159
+ Tool Name: process_order_cancel
160
+ Description: Cancel process_order workflow (if running or waiting)
161
+
162
+ Input: {"instance_id": "abc123..."}
163
+ Output: {
164
+ "content": [{
165
+ "type": "text",
166
+ "text": "Workflow 'process_order' cancelled successfully.\nInstance ID: abc123...\nCompensations executed.\n\nThe workflow has been stopped and any side effects have been rolled back."
167
+ }],
168
+ "isError": false
169
+ }
170
+ ```
171
+
172
+ The cancel tool:
173
+
174
+ - Only works on workflows with status `running`, `waiting_for_event`, or `waiting_for_timer`
175
+ - Automatically executes SAGA compensation transactions to roll back side effects
176
+ - Returns an error for already completed, failed, or cancelled workflows
177
+
150
178
  ## Advanced Configuration
151
179
 
152
180
  ### Authentication
@@ -76,11 +76,13 @@ workflow:order_workflow (parent)
76
76
  ## Span Attributes
77
77
 
78
78
  **Workflow Spans**:
79
+
79
80
  - `edda.workflow.instance_id`
80
81
  - `edda.workflow.name`
81
82
  - `edda.workflow.cancelled` (when cancelled)
82
83
 
83
84
  **Activity Spans**:
85
+
84
86
  - `edda.activity.id` (e.g., "reserve_inventory:1")
85
87
  - `edda.activity.name`
86
88
  - `edda.activity.is_replaying`
@@ -107,10 +109,12 @@ When `enable_metrics=True`:
107
109
  OpenTelemetryHooks automatically inherits trace context from multiple sources, with the following priority:
108
110
 
109
111
  1. **Explicit `_trace_context` in input_data** (highest priority)
112
+
110
113
  - Extracted from CloudEvents extension attributes
111
114
  - Useful for cross-service trace propagation
112
115
 
113
116
  2. **Current active span** (e.g., from ASGI/WSGI middleware)
117
+
114
118
  - Automatically detected using `trace.get_current_span()`
115
119
  - Works with OpenTelemetry instrumentation middleware
116
120
 
@@ -44,6 +44,16 @@ Then open http://localhost:8080 in your browser.
44
44
 
45
45
  *The Viewer UI shows all workflow instances with status badges and action buttons*
46
46
 
47
+ **Pydantic Form Generation:**
48
+
49
+ ![Pydantic Form Generation](images/start-workflow-form-pydantic.png)
50
+
51
+ *Auto-generated form fields based on Pydantic model type hints (str, float, int)*
52
+
53
+ ![Workflow Selection](images/workflow-selection-dropdown.png)
54
+
55
+ *Dropdown showing all registered workflows with event_handler=True*
56
+
47
57
  ## Three Ways to Run the Viewer
48
58
 
49
59
  ### Method 1: Command Line (Recommended)
@@ -295,7 +305,7 @@ lsof -ti:8080 | xargs kill -9
295
305
 
296
306
  Once workflows appear, you'll see them with color-coded status badges:
297
307
 
298
- ![Status Badges Example](images/status-badges-example.png)
308
+ ![Status Badges Example](images/workflow-list-view.png)
299
309
 
300
310
  *Workflow instances displayed with status badges (Completed ✅, Running ⏳, Failed ❌, Waiting ⏸️, Cancelled 🚫, etc.)*
301
311
 
@@ -34,15 +34,22 @@ The Edda Viewer UI provides workflow visualization and monitoring.
34
34
 
35
35
  Workflows with conditional logic (if/else, match/case) are visualized with labeled branches:
36
36
 
37
- ![Conditional Branching Diagram](images/conditional-branching-diagram.png)
37
+ ![Workflow Detail Page](images/detail-page-loan-approval.png)
38
38
 
39
- *Example: `loan_approval_workflow` showing conditional branches based on credit score ("sufficient" vs "insufficient" paths)*
39
+ *Example: `loan_approval_workflow` detail page. Note the **Execution Flow diagram** (bottom left) showing conditional branches with "sufficient" vs "insufficient" paths based on credit score.*
40
40
 
41
41
  The diagram clearly shows:
42
+
42
43
  - Decision points (diamond shapes in some diagrams)
43
44
  - Branch labels indicating conditions
44
45
  - Different execution paths based on runtime data
45
46
 
47
+ **Match/Case Pattern:**
48
+
49
+ ![Match Case Workflow](images/detail-page-match-case.png)
50
+
51
+ *Example: `match_case_workflow` showing multiple case branches with different handlers*
52
+
46
53
  ## Viewing Workflows
47
54
 
48
55
  ### Workflow List
@@ -68,42 +75,46 @@ The main page shows all workflow instances:
68
75
  - 🚫 **Cancelled**: Manually cancelled (orange)
69
76
  - 🔄 **Compensating**: Executing compensations (purple)
70
77
 
71
- ![Status Badges Example](images/status-badges-example.png)
78
+ ![Status Badges Example](images/workflow-list-view.png)
72
79
 
73
80
  ### Workflow List View
74
81
 
75
- The main page displays workflow instances as interactive cards:
82
+ The main page displays workflow instances as interactive cards with search and filter capabilities:
76
83
 
77
84
  ![Workflow List View](images/workflow-list-view.png)
78
85
 
86
+ *Workflow list with search filter bar, status filter, date range picker, and pagination controls*
87
+
79
88
  ### Workflow Detail Page
80
89
 
81
- Click on a workflow instance to see:
90
+ Click on a workflow instance to see the detail page:
91
+
92
+ ![Workflow Detail Page](images/detail-page-loan-approval.png)
82
93
 
83
- 1. **Overview Panel**:
84
- - Instance ID, status, timestamps
85
- - Input parameters (JSON)
86
- - Output result (if completed)
94
+ *Workflow detail page showing the Overview Panel (top), Execution Flow diagram (bottom left), and Activity Details panel (bottom right).*
87
95
 
88
- ![Workflow Detail - Overview Panel](images/detail-overview-panel.png)
96
+ 1. **Overview Panel** (top):
89
97
 
90
- 2. **Hybrid Diagram**:
91
- - Visual graph of workflow structure
92
- - Color-coded execution status
93
- - Compensation flow (if applicable)
98
+ - Instance ID, status, timestamps
99
+ - Input parameters (JSON)
100
+ - Output result (if completed)
94
101
 
95
- ![Hybrid Diagram Example](images/hybrid-diagram-example.png)
102
+ 2. **Execution Flow** (bottom left):
96
103
 
97
- 3. **Execution History**:
98
- - Step-by-step execution log
99
- - Activity results
100
- - Event data (for wait_event)
101
- - Error messages (if failed)
104
+ - Visual graph of workflow structure
105
+ - Color-coded execution status
106
+ - Compensation flow (if applicable)
102
107
 
103
- ![Execution History Panel](images/execution-history-panel.png)
108
+ 3. **Activity Details** (bottom right):
109
+
110
+ - Step-by-step execution log
111
+ - Activity results
112
+ - Event data (for wait_event)
113
+ - Error messages (if failed)
104
114
 
105
115
  4. **Actions**:
106
- - **Cancel**: Stop running workflow
116
+
117
+ - **Cancel**: Stop running workflow
107
118
 
108
119
  ## Starting Workflows from Viewer
109
120
 
@@ -113,21 +124,15 @@ The Viewer UI can start workflows with automatic form generation:
113
124
 
114
125
  For Pydantic-based workflows, the Viewer generates input forms automatically.
115
126
 
116
- ![Start Workflow Dialog](images/start-workflow-dialog.png)
127
+ ![Start Workflow Dialog](images/workflow-selection-dropdown.png)
117
128
 
118
- *Example: Workflow selection with auto-generated form*
129
+ *Example: Workflow selection dropdown showing all registered workflows*
119
130
 
120
131
  The form generator supports various field types based on Pydantic model definitions:
121
132
 
122
- ![Form Generation Example](images/form-generation-example.png)
123
-
124
- *Example: Auto-generated form showing different field types (text inputs, number inputs, checkboxes, etc.)*
125
-
126
- For complex workflows with nested models:
127
-
128
133
  ![Nested Pydantic Form](images/nested-pydantic-form.png)
129
134
 
130
- *Example: Auto-generated form with nested Pydantic models (OrderItem list, ShippingAddress)*
135
+ *Auto-generated form with nested Pydantic models (items list, shipping_address) showing text inputs, number inputs, and nested model containers*
131
136
 
132
137
  ### Starting Workflow
133
138
 
@@ -166,7 +171,7 @@ When a workflow is cancelled or fails, and compensations run, the diagram shows:
166
171
 
167
172
  ![Compensation Execution](images/compensation-execution.png)
168
173
 
169
- *Example: `cancellable_booking` workflow after cancellation*
174
+ *Saga compensation flow showing rollback activities (cancel_flight_ticket, cancel_hotel_room)*
170
175
 
171
176
  ## Event Waiting Visualization
172
177
 
@@ -174,7 +179,7 @@ Workflows can wait for external events using `wait_event()`. The Viewer displays
174
179
 
175
180
  ![Event Waiting Visualization](images/wait-event-visualization.png)
176
181
 
177
- *Example: `payment_workflow` waiting for `payment.completed` event*
182
+ *Workflow with wait_event showing event type, timeout, and activity details panel*
178
183
 
179
184
  The diagram shows:
180
185
  - Hexagon node: Event wait point
@@ -19,14 +19,15 @@ def create_durable_tool(
19
19
  description: str = "",
20
20
  ) -> Workflow:
21
21
  """
22
- Create a durable workflow tool with auto-generated status/result tools.
22
+ Create a durable workflow tool with auto-generated status/result/cancel tools.
23
23
 
24
24
  This function:
25
25
  1. Wraps the function as an Edda @workflow
26
- 2. Registers three MCP tools:
26
+ 2. Registers four MCP tools:
27
27
  - {name}: Start workflow, return instance_id
28
28
  - {name}_status: Check workflow status
29
29
  - {name}_result: Get workflow result
30
+ - {name}_cancel: Cancel workflow (if running or waiting)
30
31
 
31
32
  Args:
32
33
  server: EddaMCPServer instance
@@ -93,9 +94,9 @@ def create_durable_tool(
93
94
  status_tool_name = f"{workflow_name}_status"
94
95
  status_tool_description = f"Check status of {workflow_name} workflow"
95
96
 
96
- @server._mcp.tool(name=status_tool_name, description=status_tool_description) # type: ignore[misc]
97
+ @server._mcp.tool(name=status_tool_name, description=status_tool_description)
97
98
  async def status_tool(instance_id: str) -> dict[str, Any]:
98
- """Check workflow status."""
99
+ """Check workflow status with progress metadata."""
99
100
  try:
100
101
  instance = await server.storage.get_instance(instance_id)
101
102
  if instance is None:
@@ -112,9 +113,22 @@ def create_durable_tool(
112
113
  status = instance["status"]
113
114
  current_activity_id = instance.get("current_activity_id", "N/A")
114
115
 
116
+ # Get history to count completed activities
117
+ history = await server.storage.get_history(instance_id)
118
+ completed_activities = len(
119
+ [h for h in history if h["event_type"] == "ActivityCompleted"]
120
+ )
121
+
122
+ # Suggest poll interval based on status
123
+ # Running workflows need more frequent polling (5s)
124
+ # Waiting workflows need less frequent polling (10s)
125
+ suggested_poll_interval_ms = 5000 if status == "running" else 10000
126
+
115
127
  status_text = (
116
128
  f"Workflow Status: {status}\n"
117
129
  f"Current Activity: {current_activity_id}\n"
130
+ f"Completed Activities: {completed_activities}\n"
131
+ f"Suggested Poll Interval: {suggested_poll_interval_ms}ms\n"
118
132
  f"Instance ID: {instance_id}"
119
133
  )
120
134
 
@@ -137,7 +151,7 @@ def create_durable_tool(
137
151
  result_tool_name = f"{workflow_name}_result"
138
152
  result_tool_description = f"Get result of {workflow_name} workflow (if completed)"
139
153
 
140
- @server._mcp.tool(name=result_tool_name, description=result_tool_description) # type: ignore[misc]
154
+ @server._mcp.tool(name=result_tool_name, description=result_tool_description)
141
155
  async def result_tool(instance_id: str) -> dict[str, Any]:
142
156
  """Get workflow result (if completed)."""
143
157
  try:
@@ -184,4 +198,86 @@ def create_durable_tool(
184
198
  "isError": True,
185
199
  }
186
200
 
201
+ # 5. Generate cancel tool
202
+ cancel_tool_name = f"{workflow_name}_cancel"
203
+ cancel_tool_description = f"Cancel {workflow_name} workflow (if running or waiting)"
204
+
205
+ @server._mcp.tool(name=cancel_tool_name, description=cancel_tool_description)
206
+ async def cancel_tool(instance_id: str) -> dict[str, Any]:
207
+ """Cancel a running or waiting workflow."""
208
+ try:
209
+ # Check if instance exists
210
+ instance = await server.storage.get_instance(instance_id)
211
+ if instance is None:
212
+ return {
213
+ "content": [
214
+ {
215
+ "type": "text",
216
+ "text": f"Workflow instance not found: {instance_id}",
217
+ }
218
+ ],
219
+ "isError": True,
220
+ }
221
+
222
+ current_status = instance["status"]
223
+
224
+ # Check if replay_engine is available
225
+ if server.replay_engine is None:
226
+ return {
227
+ "content": [
228
+ {
229
+ "type": "text",
230
+ "text": "Server not initialized. Call server.initialize() first.",
231
+ }
232
+ ],
233
+ "isError": True,
234
+ }
235
+
236
+ # Try to cancel
237
+ success = await server.replay_engine.cancel_workflow(
238
+ instance_id=instance_id,
239
+ cancelled_by="mcp_user",
240
+ )
241
+
242
+ if success:
243
+ return {
244
+ "content": [
245
+ {
246
+ "type": "text",
247
+ "text": (
248
+ f"Workflow '{workflow_name}' cancelled successfully.\n"
249
+ f"Instance ID: {instance_id}\n"
250
+ f"Compensations executed.\n\n"
251
+ f"The workflow has been stopped and any side effects "
252
+ f"have been rolled back."
253
+ ),
254
+ }
255
+ ],
256
+ "isError": False,
257
+ }
258
+ else:
259
+ return {
260
+ "content": [
261
+ {
262
+ "type": "text",
263
+ "text": (
264
+ f"Cannot cancel workflow: {instance_id}\n"
265
+ f"Current status: {current_status}\n"
266
+ f"Only running or waiting workflows can be cancelled."
267
+ ),
268
+ }
269
+ ],
270
+ "isError": True,
271
+ }
272
+ except Exception as e:
273
+ return {
274
+ "content": [
275
+ {
276
+ "type": "text",
277
+ "text": f"Error cancelling workflow: {str(e)}",
278
+ }
279
+ ],
280
+ "isError": True,
281
+ }
282
+
187
283
  return workflow_instance
@@ -9,10 +9,11 @@ from edda.app import EddaApp
9
9
  from edda.workflow import Workflow
10
10
 
11
11
  if TYPE_CHECKING:
12
+ from edda.replay import ReplayEngine
12
13
  from edda.storage.protocol import StorageProtocol
13
14
 
14
15
  try:
15
- from mcp.server.fastmcp import FastMCP # type: ignore[import-not-found]
16
+ from mcp.server.fastmcp import FastMCP
16
17
  except ImportError as e:
17
18
  raise ImportError(
18
19
  "MCP Python SDK is required for MCP integration. "
@@ -68,10 +69,11 @@ class EddaMCPServer:
68
69
  asyncio.run(main())
69
70
  ```
70
71
 
71
- The server automatically generates three MCP tools for each @durable_tool:
72
+ The server automatically generates four MCP tools for each @durable_tool:
72
73
  - `tool_name`: Start the workflow, returns instance_id
73
74
  - `tool_name_status`: Check workflow status
74
75
  - `tool_name_result`: Get workflow result (if completed)
76
+ - `tool_name_cancel`: Cancel workflow (if running or waiting)
75
77
  """
76
78
 
77
79
  def __init__(
@@ -122,6 +124,24 @@ class EddaMCPServer:
122
124
  """
123
125
  return self._edda_app.storage
124
126
 
127
+ @property
128
+ def replay_engine(self) -> ReplayEngine | None:
129
+ """
130
+ Access replay engine for workflow operations (cancel, resume, etc.).
131
+
132
+ Returns:
133
+ ReplayEngine or None if not initialized
134
+
135
+ Example:
136
+ ```python
137
+ # Cancel a running workflow
138
+ success = await server.replay_engine.cancel_workflow(
139
+ instance_id, "mcp_user"
140
+ )
141
+ ```
142
+ """
143
+ return self._edda_app.replay_engine
144
+
125
145
  def durable_tool(
126
146
  self,
127
147
  func: Callable[..., Any] | None = None,
@@ -131,10 +151,11 @@ class EddaMCPServer:
131
151
  """
132
152
  Decorator to define a durable workflow tool.
133
153
 
134
- Automatically generates three MCP tools:
154
+ Automatically generates four MCP tools:
135
155
  1. Main tool: Starts the workflow, returns instance_id
136
156
  2. Status tool: Checks workflow status
137
157
  3. Result tool: Gets workflow result (if completed)
158
+ 4. Cancel tool: Cancels workflow (if running or waiting)
138
159
 
139
160
  Args:
140
161
  func: Workflow function (async)
@@ -207,7 +228,7 @@ class EddaMCPServer:
207
228
  def decorator(f: Callable[..., Any]) -> Callable[..., Any]:
208
229
  # Use FastMCP's native prompt decorator
209
230
  prompt_desc = description or f.__doc__ or f"Prompt: {f.__name__}"
210
- return cast(Callable[..., Any], self._mcp.prompt(description=prompt_desc)(f))
231
+ return self._mcp.prompt(description=prompt_desc)(f)
211
232
 
212
233
  if func is None:
213
234
  return decorator
@@ -228,8 +249,8 @@ class EddaMCPServer:
228
249
  Returns:
229
250
  ASGI callable (Starlette app)
230
251
  """
231
- from starlette.requests import Request # type: ignore[import-not-found]
232
- from starlette.responses import Response # type: ignore[import-not-found]
252
+ from starlette.requests import Request
253
+ from starlette.responses import Response
233
254
 
234
255
  # Get MCP's Starlette app (Issue #1367 workaround: use directly)
235
256
  app = self._mcp.streamable_http_app()
@@ -270,14 +291,13 @@ class EddaMCPServer:
270
291
  app.router.add_route("/cancel/{instance_id}", edda_cancel_handler, methods=["POST"])
271
292
 
272
293
  # Add authentication middleware if token_verifier provided (AFTER adding routes)
294
+ result_app: Any = app
273
295
  if self._token_verifier is not None:
274
- from starlette.middleware.base import ( # type: ignore[import-not-found]
275
- BaseHTTPMiddleware,
276
- )
296
+ from starlette.middleware.base import BaseHTTPMiddleware
277
297
 
278
- class AuthMiddleware(BaseHTTPMiddleware): # type: ignore[misc]
279
- def __init__(self, app: Any, token_verifier: Callable[[str], bool]):
280
- super().__init__(app)
298
+ class AuthMiddleware(BaseHTTPMiddleware):
299
+ def __init__(self, app_inner: Any, token_verifier: Callable[[str], bool]) -> None:
300
+ super().__init__(app_inner)
281
301
  self.token_verifier = token_verifier
282
302
 
283
303
  async def dispatch(
@@ -288,12 +308,13 @@ class EddaMCPServer:
288
308
  token = auth_header[7:]
289
309
  if not self.token_verifier(token):
290
310
  return Response("Unauthorized", status_code=401)
291
- return await call_next(request)
311
+ response: Response = await call_next(request)
312
+ return response
292
313
 
293
314
  # Wrap app with auth middleware
294
- app = AuthMiddleware(app, self._token_verifier)
315
+ result_app = AuthMiddleware(app, self._token_verifier)
295
316
 
296
- return cast(Callable[..., Any], app)
317
+ return cast(Callable[..., Any], result_app)
297
318
 
298
319
  async def initialize(self) -> None:
299
320
  """