edda-framework 0.5.0__tar.gz → 0.6.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.
- {edda_framework-0.5.0 → edda_framework-0.6.0}/.github/workflows/ci.yml +2 -2
- {edda_framework-0.5.0 → edda_framework-0.6.0}/PKG-INFO +6 -5
- {edda_framework-0.5.0 → edda_framework-0.6.0}/README.md +5 -4
- {edda_framework-0.5.0 → edda_framework-0.6.0}/docs/integrations/mcp.md +30 -4
- {edda_framework-0.5.0 → edda_framework-0.6.0}/edda/integrations/mcp/decorators.py +101 -5
- {edda_framework-0.5.0 → edda_framework-0.6.0}/edda/integrations/mcp/server.py +36 -15
- {edda_framework-0.5.0 → edda_framework-0.6.0}/pyproject.toml +1 -1
- edda_framework-0.6.0/tests/integrations/mcp/test_cancel.py +166 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/tests/integrations/mcp/test_integration.py +2 -2
- {edda_framework-0.5.0 → edda_framework-0.6.0}/tests/integrations/mcp/test_jsonrpc.py +1 -1
- {edda_framework-0.5.0 → edda_framework-0.6.0}/tests/integrations/mcp/test_prompts.py +3 -3
- {edda_framework-0.5.0 → edda_framework-0.6.0}/tests/integrations/mcp/test_server.py +80 -1
- {edda_framework-0.5.0 → edda_framework-0.6.0}/uv.lock +1 -1
- {edda_framework-0.5.0 → edda_framework-0.6.0}/.github/workflows/docs.yml +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/.github/workflows/release.yml +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/.gitignore +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/.python-version +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/Justfile +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/LICENSE +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/demo_app.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/docs/core-features/durable-execution/replay.md +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/docs/core-features/events/cloudevents-http-binding.md +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/docs/core-features/events/wait-event.md +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/docs/core-features/hooks.md +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/docs/core-features/retry.md +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/docs/core-features/saga-compensation.md +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/docs/core-features/transactional-outbox.md +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/docs/core-features/workflows-activities.md +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/docs/examples/ecommerce.md +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/docs/examples/events.md +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/docs/examples/fastapi-integration.md +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/docs/examples/saga.md +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/docs/examples/simple.md +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/docs/getting-started/concepts.md +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/docs/getting-started/first-workflow.md +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/docs/getting-started/installation.md +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/docs/getting-started/quick-start.md +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/docs/index.md +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/docs/integrations/opentelemetry.md +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/docs/markdown.md +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/docs/viewer-ui/images/cloudevents-cli-trigger.png +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/docs/viewer-ui/images/compensation-execution.png +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/docs/viewer-ui/images/conditional-branching-diagram.png +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/docs/viewer-ui/images/detail-overview-panel.png +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/docs/viewer-ui/images/execution-history-panel.png +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/docs/viewer-ui/images/form-generation-example.png +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/docs/viewer-ui/images/hybrid-diagram-example.png +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/docs/viewer-ui/images/nested-pydantic-form.png +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/docs/viewer-ui/images/start-workflow-dialog.png +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/docs/viewer-ui/images/status-badges-example.png +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/docs/viewer-ui/images/wait-event-visualization.png +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/docs/viewer-ui/images/workflow-list-view.png +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/docs/viewer-ui/setup.md +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/docs/viewer-ui/visualization.md +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/edda/__init__.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/edda/activity.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/edda/app.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/edda/compensation.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/edda/context.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/edda/events.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/edda/exceptions.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/edda/hooks.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/edda/integrations/__init__.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/edda/integrations/mcp/__init__.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/edda/integrations/opentelemetry/__init__.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/edda/integrations/opentelemetry/hooks.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/edda/locking.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/edda/outbox/__init__.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/edda/outbox/relayer.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/edda/outbox/transactional.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/edda/pydantic_utils.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/edda/replay.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/edda/retry.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/edda/serialization/__init__.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/edda/serialization/base.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/edda/serialization/json.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/edda/storage/__init__.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/edda/storage/models.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/edda/storage/protocol.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/edda/storage/sqlalchemy_storage.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/edda/viewer_ui/__init__.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/edda/viewer_ui/app.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/edda/viewer_ui/components.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/edda/viewer_ui/data_service.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/edda/visualizer/__init__.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/edda/visualizer/ast_analyzer.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/edda/visualizer/mermaid_generator.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/edda/workflow.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/edda/wsgi.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/examples/__init__.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/examples/cancellable_workflow.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/examples/compensation_workflow.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/examples/event_waiting_app.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/examples/event_waiting_workflow.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/examples/event_waiting_workflow_complete.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/examples/mcp/README.md +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/examples/mcp/order_processing_mcp.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/examples/mcp/prompts_example.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/examples/mcp/remote_server_example.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/examples/mcp/simple_mcp_server.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/examples/observability_with_logfire.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/examples/observability_with_opentelemetry.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/examples/pydantic_saga.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/examples/retry_example.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/examples/retry_with_compensation.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/examples/simple_workflow.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/examples/typeddict_example.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/examples/with_outbox.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/tests/__init__.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/tests/conftest.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/tests/integrations/__init__.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/tests/integrations/mcp/__init__.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/tests/integrations/opentelemetry/__init__.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/tests/integrations/opentelemetry/test_hooks.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/tests/test_activity.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/tests/test_activity_retry.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/tests/test_activity_sync.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/tests/test_app.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/tests/test_ast_analyzer.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/tests/test_atomic_wait_event.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/tests/test_binary_data.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/tests/test_cloudevents_http_binding.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/tests/test_compensation.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/tests/test_compensation_crash_recovery.py.wip +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/tests/test_concurrent_outbox.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/tests/test_context.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/tests/test_ctx_session.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/tests/test_distributed_event_delivery.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/tests/test_events.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/tests/test_lock_race_condition.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/tests/test_lock_timeout_customization.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/tests/test_locking.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/tests/test_multidb_storage.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/tests/test_outbox.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/tests/test_pydantic_activity.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/tests/test_pydantic_enum.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/tests/test_pydantic_events.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/tests/test_pydantic_saga.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/tests/test_pydantic_utils.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/tests/test_received_event.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/tests/test_replay.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/tests/test_retry_policy.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/tests/test_saga_parameter_extraction.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/tests/test_serialization.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/tests/test_skip_locked.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/tests/test_stale_workflow_recovery.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/tests/test_storage.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/tests/test_storage_mysql.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/tests/test_storage_postgresql.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/tests/test_transactions.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/tests/test_viewer_pydantic_form.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/tests/test_viewer_start_saga.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/tests/test_wait_timer.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/tests/test_workflow.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/tests/test_workflow_auto_register.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/tests/test_workflow_cancellation.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/viewer_app.py +0 -0
- {edda_framework-0.5.0 → edda_framework-0.6.0}/zensical.toml +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:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: edda-framework
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.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
|
|
@@ -709,7 +709,7 @@ def process_payment(ctx: WorkflowContext, amount: float) -> dict:
|
|
|
709
709
|
@workflow
|
|
710
710
|
async def payment_workflow(ctx: WorkflowContext, order_id: str) -> dict:
|
|
711
711
|
# Workflows still use async (for deterministic replay)
|
|
712
|
-
result = await process_payment(ctx, 99.99
|
|
712
|
+
result = await process_payment(ctx, 99.99)
|
|
713
713
|
return result
|
|
714
714
|
```
|
|
715
715
|
|
|
@@ -748,13 +748,14 @@ if __name__ == "__main__":
|
|
|
748
748
|
|
|
749
749
|
### Auto-Generated Tools
|
|
750
750
|
|
|
751
|
-
Each `@durable_tool` automatically generates **
|
|
751
|
+
Each `@durable_tool` automatically generates **four MCP tools**:
|
|
752
752
|
|
|
753
753
|
1. **Main tool** (`process_order`): Starts the workflow, returns instance ID
|
|
754
|
-
2. **Status tool** (`process_order_status`): Checks workflow progress
|
|
754
|
+
2. **Status tool** (`process_order_status`): Checks workflow progress with completed activity count and suggested poll interval
|
|
755
755
|
3. **Result tool** (`process_order_result`): Gets final result when completed
|
|
756
|
+
4. **Cancel tool** (`process_order_cancel`): Cancels workflow if running or waiting, executes compensation handlers
|
|
756
757
|
|
|
757
|
-
This enables AI assistants to work with workflows that take minutes, hours, or even days to complete.
|
|
758
|
+
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
759
|
|
|
759
760
|
### MCP Prompts
|
|
760
761
|
|
|
@@ -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
|
|
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 **
|
|
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
|
|
|
@@ -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 **
|
|
101
|
+
Each `@durable_tool` automatically generates **four MCP tools**:
|
|
101
102
|
|
|
102
103
|
### 1. Main Tool: Start Workflow
|
|
103
104
|
|
|
@@ -125,12 +126,16 @@ 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
|
+
- **Completed Activities**: Number of activities that have finished
|
|
137
|
+
- **Suggested Poll Interval**: Recommended wait time before checking again (5000ms for running, 10000ms for waiting)
|
|
138
|
+
|
|
134
139
|
### 3. Result Tool: Get Final Result
|
|
135
140
|
|
|
136
141
|
```
|
|
@@ -147,6 +152,27 @@ Output: {
|
|
|
147
152
|
}
|
|
148
153
|
```
|
|
149
154
|
|
|
155
|
+
### 4. Cancel Tool: Stop Workflow
|
|
156
|
+
|
|
157
|
+
```
|
|
158
|
+
Tool Name: process_order_cancel
|
|
159
|
+
Description: Cancel process_order workflow (if running or waiting)
|
|
160
|
+
|
|
161
|
+
Input: {"instance_id": "abc123..."}
|
|
162
|
+
Output: {
|
|
163
|
+
"content": [{
|
|
164
|
+
"type": "text",
|
|
165
|
+
"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."
|
|
166
|
+
}],
|
|
167
|
+
"isError": false
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
The cancel tool:
|
|
172
|
+
- Only works on workflows with status `running`, `waiting_for_event`, or `waiting_for_timer`
|
|
173
|
+
- Automatically executes SAGA compensation transactions to roll back side effects
|
|
174
|
+
- Returns an error for already completed, failed, or cancelled workflows
|
|
175
|
+
|
|
150
176
|
## Advanced Configuration
|
|
151
177
|
|
|
152
178
|
### Authentication
|
|
@@ -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
|
|
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)
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
232
|
-
from starlette.responses import Response
|
|
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
|
|
275
|
-
BaseHTTPMiddleware,
|
|
276
|
-
)
|
|
296
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
277
297
|
|
|
278
|
-
class AuthMiddleware(BaseHTTPMiddleware):
|
|
279
|
-
def __init__(self,
|
|
280
|
-
super().__init__(
|
|
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
|
-
|
|
311
|
+
response: Response = await call_next(request)
|
|
312
|
+
return response
|
|
292
313
|
|
|
293
314
|
# Wrap app with auth middleware
|
|
294
|
-
|
|
315
|
+
result_app = AuthMiddleware(app, self._token_verifier)
|
|
295
316
|
|
|
296
|
-
return cast(Callable[..., Any],
|
|
317
|
+
return cast(Callable[..., Any], result_app)
|
|
297
318
|
|
|
298
319
|
async def initialize(self) -> None:
|
|
299
320
|
"""
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""Tests for MCP cancel tool functionality."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
# Skip all tests if mcp is not installed
|
|
8
|
+
pytest.importorskip("mcp")
|
|
9
|
+
|
|
10
|
+
from edda import WorkflowContext, activity
|
|
11
|
+
from edda.compensation import register_compensation
|
|
12
|
+
from edda.events import wait_event
|
|
13
|
+
from edda.integrations.mcp import EddaMCPServer
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.fixture
|
|
17
|
+
async def mcp_server_with_cancellable_tool():
|
|
18
|
+
"""Create MCP server with a cancellable durable tool."""
|
|
19
|
+
server = EddaMCPServer(
|
|
20
|
+
name="Cancel Test Service",
|
|
21
|
+
db_url="sqlite+aiosqlite:///:memory:",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# Track compensation execution
|
|
25
|
+
compensations_executed: list[str] = []
|
|
26
|
+
|
|
27
|
+
@activity
|
|
28
|
+
async def process_step(ctx: WorkflowContext, value: str):
|
|
29
|
+
return {"processed": value.upper()}
|
|
30
|
+
|
|
31
|
+
@activity
|
|
32
|
+
async def compensate_step(ctx: WorkflowContext):
|
|
33
|
+
compensations_executed.append("step_compensated")
|
|
34
|
+
|
|
35
|
+
@server.durable_tool(description="Cancellable workflow")
|
|
36
|
+
async def cancellable_workflow(ctx: WorkflowContext, value: str):
|
|
37
|
+
result = await process_step(ctx, value)
|
|
38
|
+
await register_compensation(ctx, compensate_step)
|
|
39
|
+
# Wait for an event that won't come (workflow will be cancelled)
|
|
40
|
+
await wait_event(ctx, "test_event")
|
|
41
|
+
return result
|
|
42
|
+
|
|
43
|
+
@server.durable_tool(description="Quick workflow")
|
|
44
|
+
async def quick_workflow(ctx: WorkflowContext, value: str):
|
|
45
|
+
result = await process_step(ctx, value)
|
|
46
|
+
return result
|
|
47
|
+
|
|
48
|
+
# Initialize the EddaApp
|
|
49
|
+
await server._edda_app.initialize()
|
|
50
|
+
server._compensations_executed = compensations_executed
|
|
51
|
+
yield server
|
|
52
|
+
# Cleanup after tests
|
|
53
|
+
await server.shutdown()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@pytest.mark.asyncio
|
|
57
|
+
async def test_cancel_tool_registered(mcp_server_with_cancellable_tool):
|
|
58
|
+
"""Test that cancel tool is registered for durable_tool."""
|
|
59
|
+
server = mcp_server_with_cancellable_tool
|
|
60
|
+
|
|
61
|
+
# Check that 4 tools are registered per workflow
|
|
62
|
+
# cancellable_workflow: main, status, result, cancel
|
|
63
|
+
# quick_workflow: main, status, result, cancel
|
|
64
|
+
# We verify by checking if the workflows exist in registry
|
|
65
|
+
assert "cancellable_workflow" in server._workflows
|
|
66
|
+
assert "quick_workflow" in server._workflows
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@pytest.mark.asyncio
|
|
70
|
+
async def test_cancel_waiting_workflow(mcp_server_with_cancellable_tool):
|
|
71
|
+
"""Test cancelling a workflow waiting for an event."""
|
|
72
|
+
server = mcp_server_with_cancellable_tool
|
|
73
|
+
workflow = server._workflows["cancellable_workflow"]
|
|
74
|
+
|
|
75
|
+
# Start workflow (will start processing and wait for event)
|
|
76
|
+
instance_id = await workflow.start(value="test")
|
|
77
|
+
|
|
78
|
+
# Give it time to reach wait_event
|
|
79
|
+
await asyncio.sleep(0.2)
|
|
80
|
+
|
|
81
|
+
# Verify workflow is waiting
|
|
82
|
+
instance = await server.storage.get_instance(instance_id)
|
|
83
|
+
assert instance["status"] == "waiting_for_event"
|
|
84
|
+
|
|
85
|
+
# Cancel the workflow
|
|
86
|
+
success = await server.replay_engine.cancel_workflow(instance_id, "mcp_user")
|
|
87
|
+
|
|
88
|
+
assert success is True
|
|
89
|
+
|
|
90
|
+
# Verify workflow is cancelled
|
|
91
|
+
instance = await server.storage.get_instance(instance_id)
|
|
92
|
+
assert instance["status"] == "cancelled"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@pytest.mark.asyncio
|
|
96
|
+
async def test_cancel_nonexistent_instance(mcp_server_with_cancellable_tool):
|
|
97
|
+
"""Test cancelling a workflow that doesn't exist returns False."""
|
|
98
|
+
server = mcp_server_with_cancellable_tool
|
|
99
|
+
|
|
100
|
+
# Try to cancel non-existent workflow
|
|
101
|
+
success = await server.replay_engine.cancel_workflow("nonexistent-id", "mcp_user")
|
|
102
|
+
|
|
103
|
+
assert success is False
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@pytest.mark.asyncio
|
|
107
|
+
async def test_cancel_completed_workflow(mcp_server_with_cancellable_tool):
|
|
108
|
+
"""Test that completed workflows cannot be cancelled."""
|
|
109
|
+
server = mcp_server_with_cancellable_tool
|
|
110
|
+
workflow = server._workflows["quick_workflow"]
|
|
111
|
+
|
|
112
|
+
# Start and wait for completion
|
|
113
|
+
instance_id = await workflow.start(value="complete")
|
|
114
|
+
|
|
115
|
+
# Give it time to complete
|
|
116
|
+
await asyncio.sleep(0.2)
|
|
117
|
+
|
|
118
|
+
# Verify workflow is completed
|
|
119
|
+
instance = await server.storage.get_instance(instance_id)
|
|
120
|
+
assert instance["status"] == "completed"
|
|
121
|
+
|
|
122
|
+
# Try to cancel - should return False
|
|
123
|
+
success = await server.replay_engine.cancel_workflow(instance_id, "mcp_user")
|
|
124
|
+
|
|
125
|
+
assert success is False
|
|
126
|
+
|
|
127
|
+
# Status should still be completed
|
|
128
|
+
instance = await server.storage.get_instance(instance_id)
|
|
129
|
+
assert instance["status"] == "completed"
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@pytest.mark.asyncio
|
|
133
|
+
async def test_cancel_already_cancelled_workflow(mcp_server_with_cancellable_tool):
|
|
134
|
+
"""Test that cancelling already cancelled workflow is idempotent."""
|
|
135
|
+
server = mcp_server_with_cancellable_tool
|
|
136
|
+
workflow = server._workflows["cancellable_workflow"]
|
|
137
|
+
|
|
138
|
+
# Start workflow
|
|
139
|
+
instance_id = await workflow.start(value="idempotent")
|
|
140
|
+
|
|
141
|
+
# Give it time to reach wait_event
|
|
142
|
+
await asyncio.sleep(0.2)
|
|
143
|
+
|
|
144
|
+
# Cancel first time
|
|
145
|
+
success1 = await server.replay_engine.cancel_workflow(instance_id, "mcp_user")
|
|
146
|
+
assert success1 is True
|
|
147
|
+
|
|
148
|
+
# Cancel second time - should return False (already cancelled)
|
|
149
|
+
success2 = await server.replay_engine.cancel_workflow(instance_id, "mcp_user")
|
|
150
|
+
assert success2 is False
|
|
151
|
+
|
|
152
|
+
# Status should still be cancelled
|
|
153
|
+
instance = await server.storage.get_instance(instance_id)
|
|
154
|
+
assert instance["status"] == "cancelled"
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@pytest.mark.asyncio
|
|
158
|
+
async def test_replay_engine_property(mcp_server_with_cancellable_tool):
|
|
159
|
+
"""Test that replay_engine property is accessible on EddaMCPServer."""
|
|
160
|
+
server = mcp_server_with_cancellable_tool
|
|
161
|
+
|
|
162
|
+
# replay_engine should be accessible after initialization
|
|
163
|
+
assert server.replay_engine is not None
|
|
164
|
+
|
|
165
|
+
# Should be the same instance as EddaApp's replay_engine
|
|
166
|
+
assert server.replay_engine is server._edda_app.replay_engine
|
|
@@ -26,7 +26,7 @@ async def mcp_server_with_tool():
|
|
|
26
26
|
|
|
27
27
|
@server.durable_tool(description="Process input value")
|
|
28
28
|
async def process_value(ctx: WorkflowContext, value: str):
|
|
29
|
-
result = await process_step(ctx, value
|
|
29
|
+
result = await process_step(ctx, value)
|
|
30
30
|
return result
|
|
31
31
|
|
|
32
32
|
# Initialize the EddaApp
|
|
@@ -132,7 +132,7 @@ async def test_result_tool_before_completion(mcp_server_with_tool):
|
|
|
132
132
|
|
|
133
133
|
@server.durable_tool(description="Long workflow")
|
|
134
134
|
async def long_workflow(ctx: WorkflowContext):
|
|
135
|
-
result = await long_running_step(ctx
|
|
135
|
+
result = await long_running_step(ctx)
|
|
136
136
|
return result
|
|
137
137
|
|
|
138
138
|
workflow = server._workflows["long_workflow"]
|
|
@@ -26,7 +26,7 @@ async def mcp_server():
|
|
|
26
26
|
|
|
27
27
|
@server.durable_tool(description="Greet a user")
|
|
28
28
|
async def greet_workflow(ctx: WorkflowContext, name: str):
|
|
29
|
-
result = await greet_user(ctx, name
|
|
29
|
+
result = await greet_user(ctx, name)
|
|
30
30
|
return result
|
|
31
31
|
|
|
32
32
|
# Initialize the EddaApp
|
|
@@ -54,7 +54,7 @@ async def test_prompt_with_workflow_state(mcp_server):
|
|
|
54
54
|
|
|
55
55
|
@mcp_server.durable_tool(description="Test workflow")
|
|
56
56
|
async def test_workflow(ctx: WorkflowContext, value: str):
|
|
57
|
-
result = await test_activity(ctx, value
|
|
57
|
+
result = await test_activity(ctx, value)
|
|
58
58
|
return result
|
|
59
59
|
|
|
60
60
|
# Start workflow
|
|
@@ -182,8 +182,8 @@ async def test_prompt_accesses_workflow_history(mcp_server):
|
|
|
182
182
|
|
|
183
183
|
@mcp_server.durable_tool(description="Multi-step workflow")
|
|
184
184
|
async def multi_step(ctx: WorkflowContext):
|
|
185
|
-
await step_one(ctx
|
|
186
|
-
await step_two(ctx
|
|
185
|
+
await step_one(ctx)
|
|
186
|
+
await step_two(ctx)
|
|
187
187
|
return {"completed": True}
|
|
188
188
|
|
|
189
189
|
# Start and complete workflow
|