durable-workflow 2.5.15__tar.gz → 2.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.
Files changed (135) hide show
  1. {durable_workflow-2.5.15/src/durable_workflow.egg-info → durable_workflow-2.6.0}/PKG-INFO +9 -1
  2. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/README.md +8 -0
  3. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/pyproject.toml +3 -3
  4. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow/_cooperative_cancellation.py +9 -1
  5. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow/client.py +11 -9
  6. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow/worker.py +42 -123
  7. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow/workflow.py +164 -14
  8. {durable_workflow-2.5.15 → durable_workflow-2.6.0/src/durable_workflow.egg-info}/PKG-INFO +9 -1
  9. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow.egg-info/SOURCES.txt +2 -0
  10. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_ci_checkout.py +39 -0
  11. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_cooperative_cancellation_worker.py +138 -3
  12. durable_workflow-2.6.0/tests/test_nexus_durable_wait.py +241 -0
  13. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_nexus_namespace.py +12 -0
  14. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_replay_regression_corpus.py +27 -0
  15. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_runtime_external_payload_transport.py +3 -0
  16. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_worker.py +16 -57
  17. durable_workflow-2.6.0/tests/test_worker_observation.py +299 -0
  18. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/LICENSE +0 -0
  19. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/setup.cfg +0 -0
  20. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow/__init__.py +0 -0
  21. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow/_activity_process.py +0 -0
  22. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow/_avro.py +0 -0
  23. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow/_cancellation_scope.py +0 -0
  24. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow/_cancellation_scope_history.py +0 -0
  25. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow/_prepared_local_activity.py +0 -0
  26. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow/_query_task_authority.py +0 -0
  27. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow/_sticky_workflow_cache.py +0 -0
  28. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow/activity.py +0 -0
  29. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow/auth_composition.py +0 -0
  30. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow/cancellation.py +0 -0
  31. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow/errors.py +0 -0
  32. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow/external_storage.py +0 -0
  33. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow/external_task_input.py +0 -0
  34. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow/external_task_result.py +0 -0
  35. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow/history_bundle_verify.py +0 -0
  36. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow/interceptors.py +0 -0
  37. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow/invocable.py +0 -0
  38. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow/metrics.py +0 -0
  39. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow/nexus.py +0 -0
  40. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow/py.typed +0 -0
  41. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow/python_conformance.py +0 -0
  42. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow/replay_conformance.py +0 -0
  43. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow/replay_verify.py +0 -0
  44. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow/retry_policy.py +0 -0
  45. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow/serializer.py +0 -0
  46. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow/sync.py +0 -0
  47. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow/testing.py +0 -0
  48. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow/worker_session.py +0 -0
  49. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow/workflow_updates_conformance.py +0 -0
  50. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow.egg-info/dependency_links.txt +0 -0
  51. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow.egg-info/entry_points.txt +0 -0
  52. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow.egg-info/requires.txt +0 -0
  53. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/src/durable_workflow.egg-info/top_level.txt +0 -0
  54. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_activity_cancellation_policies.py +0 -0
  55. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_activity_context.py +0 -0
  56. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_activity_process.py +0 -0
  57. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_auth_composition.py +0 -0
  58. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_avro_value_benchmark.py +0 -0
  59. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_avro_value_protocol.py +0 -0
  60. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_cancellation_context.py +0 -0
  61. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_cancellation_remaining_time.py +0 -0
  62. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_cancellation_scope_admission.py +0 -0
  63. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_cancellation_scope_authoring.py +0 -0
  64. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_cancellation_scope_boundary.py +0 -0
  65. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_cancellation_scope_opening.py +0 -0
  66. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_child_workflow_policies.py +0 -0
  67. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_ci_integration_endpoint.py +0 -0
  68. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_client.py +0 -0
  69. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_client_timeouts.py +0 -0
  70. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_codec_regression_corpus.py +0 -0
  71. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_committed_cancellation_scope_descendants.py +0 -0
  72. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_committed_cancellation_scope_groups.py +0 -0
  73. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_committed_cancellation_scope_history.py +0 -0
  74. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_committed_cancellation_scope_replay.py +0 -0
  75. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_condition_replay_drift.py +0 -0
  76. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_control_plane_parity_fixtures.py +0 -0
  77. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_cooperative_cancellation.py +0 -0
  78. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_cooperative_cancellation_client.py +0 -0
  79. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_cooperative_remote_worker.py +0 -0
  80. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_docstring_cross_references.py +0 -0
  81. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_durable_selection.py +0 -0
  82. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_errors.py +0 -0
  83. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_external_storage.py +0 -0
  84. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_external_task_input.py +0 -0
  85. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_external_task_result.py +0 -0
  86. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_golden_history_replay.py +0 -0
  87. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_history_bundle_verify.py +0 -0
  88. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_history_event_contract.py +0 -0
  89. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_invocable.py +0 -0
  90. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_message_streams.py +0 -0
  91. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_metrics.py +0 -0
  92. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_nexus_wire.py +0 -0
  93. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_order_processing_example.py +0 -0
  94. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_parallel_completion_order_replay.py +0 -0
  95. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_parallel_saga_authoring.py +0 -0
  96. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_patch_history.py +0 -0
  97. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_pending_operation_replay.py +0 -0
  98. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_prepared_local_activity_client.py +0 -0
  99. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_prepared_local_activity_groups.py +0 -0
  100. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_prepared_local_activity_policies.py +0 -0
  101. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_prepared_local_activity_worker.py +0 -0
  102. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_public_boundary_scanner.py +0 -0
  103. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_pypi_project_surface.py +0 -0
  104. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_python_conformance.py +0 -0
  105. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_queries.py +0 -0
  106. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_query_task_authority.py +0 -0
  107. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_query_timeout_retry.py +0 -0
  108. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_readme_quickstart.py +0 -0
  109. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_redrive_replay.py +0 -0
  110. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_registration_ambiguity.py +0 -0
  111. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_release_metadata.py +0 -0
  112. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_replay.py +0 -0
  113. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_replay_clock.py +0 -0
  114. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_replay_conformance.py +0 -0
  115. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_replay_verify.py +0 -0
  116. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_retry_policy.py +0 -0
  117. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_run_cancellation_scope_replay.py +0 -0
  118. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_schedules.py +0 -0
  119. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_scoped_run_cancellation_context.py +0 -0
  120. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_serializer.py +0 -0
  121. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_signals.py +0 -0
  122. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_sleep.py +0 -0
  123. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_standalone_activity_client.py +0 -0
  124. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_sticky_execution.py +0 -0
  125. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_storage_admission.py +0 -0
  126. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_sync.py +0 -0
  127. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_testing_harness.py +0 -0
  128. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_update_signal_condition_replay.py +0 -0
  129. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_updates.py +0 -0
  130. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_wait_condition.py +0 -0
  131. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_worker_session.py +0 -0
  132. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_workflow_entrypoint_validation.py +0 -0
  133. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_workflow_result_timeout.py +0 -0
  134. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_workflow_streams.py +0 -0
  135. {durable_workflow-2.5.15 → durable_workflow-2.6.0}/tests/test_workflow_updates_conformance.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: durable-workflow
3
- Version: 2.5.15
3
+ Version: 2.6.0
4
4
  Summary: Python client and worker SDK for Durable Workflow Cloud and self-hosted Server
5
5
  Author: Durable Workflow Contributors
6
6
  License-Expression: MIT
@@ -40,6 +40,7 @@ Dynamic: license-file
40
40
 
41
41
  # Durable Workflow Python SDK
42
42
 
43
+ [![Quality Gate](https://sonarcloud.io/api/project_badges/measure?project=durable-workflow_sdk-python&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=durable-workflow_sdk-python)
43
44
  [![CI](https://github.com/durable-workflow/sdk-python/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/durable-workflow/sdk-python/actions/workflows/ci.yml)
44
45
  [![PyPI](https://img.shields.io/pypi/v/durable-workflow.svg)](https://pypi.org/project/durable-workflow/)
45
46
  [![Python](https://img.shields.io/pypi/pyversions/durable-workflow.svg)](https://pypi.org/project/durable-workflow/)
@@ -168,6 +169,13 @@ admission also needs `prepared_local_activities`, with
168
169
  `prepared_local_activity_cancellation_policies` for explicit local policies.
169
170
  Independently cancellable scopes remain disabled.
170
171
 
172
+ For synchronous Nexus completion waits, the cooperative worker also negotiates
173
+ `nexus_cooperative_cancellation` when Server advertises the installed
174
+ `workflow_run` binding. Owned target cleanup keeps the caller pending until the
175
+ Server commits delivery at the original call. Async and acceptance-only calls
176
+ keep their admission behavior. Existing terminal results and recorded legacy
177
+ Nexus histories retain their replay semantics.
178
+
171
179
  The cooperative worker supervises async and synchronous activity callbacks
172
180
  independently of application heartbeats. Existing terminal cancellation remains
173
181
  available. See the [cancellation guide](docs/cooperative-cancellation-design.md)
@@ -1,5 +1,6 @@
1
1
  # Durable Workflow Python SDK
2
2
 
3
+ [![Quality Gate](https://sonarcloud.io/api/project_badges/measure?project=durable-workflow_sdk-python&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=durable-workflow_sdk-python)
3
4
  [![CI](https://github.com/durable-workflow/sdk-python/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/durable-workflow/sdk-python/actions/workflows/ci.yml)
4
5
  [![PyPI](https://img.shields.io/pypi/v/durable-workflow.svg)](https://pypi.org/project/durable-workflow/)
5
6
  [![Python](https://img.shields.io/pypi/pyversions/durable-workflow.svg)](https://pypi.org/project/durable-workflow/)
@@ -128,6 +129,13 @@ admission also needs `prepared_local_activities`, with
128
129
  `prepared_local_activity_cancellation_policies` for explicit local policies.
129
130
  Independently cancellable scopes remain disabled.
130
131
 
132
+ For synchronous Nexus completion waits, the cooperative worker also negotiates
133
+ `nexus_cooperative_cancellation` when Server advertises the installed
134
+ `workflow_run` binding. Owned target cleanup keeps the caller pending until the
135
+ Server commits delivery at the original call. Async and acceptance-only calls
136
+ keep their admission behavior. Existing terminal results and recorded legacy
137
+ Nexus histories retain their replay semantics.
138
+
131
139
  The cooperative worker supervises async and synchronous activity callbacks
132
140
  independently of application heartbeats. Existing terminal cancellation remains
133
141
  available. See the [cancellation guide](docs/cooperative-cancellation-design.md)
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "durable-workflow"
7
- version = "2.5.15"
7
+ version = "2.6.0"
8
8
  description = "Python client and worker SDK for Durable Workflow Cloud and self-hosted Server"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -71,8 +71,8 @@ durable-workflow-replay-conformance = "durable_workflow.replay_conformance:main"
71
71
  durable-workflow-workflow-updates-conformance = "durable_workflow.workflow_updates_conformance:main"
72
72
 
73
73
  [tool.durable-workflow]
74
- product-train = "2.5.15"
75
- registry-version = "2.5.15"
74
+ product-train = "2.6.0"
75
+ registry-version = "2.6.0"
76
76
  supported-server-versions = "2.8.0"
77
77
  worker-protocol-version = "1.20"
78
78
  control-plane-version = "2"
@@ -16,7 +16,10 @@ from .errors import NonDeterministicReplayError
16
16
 
17
17
  REQUEST_EVENT = "CooperativeCancellationRequested"
18
18
  DELIVERY_EVENT = "CooperativeCancellationDelivered"
19
- _CALL_KINDS = {"activity", "local_activity", "timer", "condition", "signal", "child", "parallel", "selection_handle"}
19
+ _CALL_KINDS = {
20
+ "activity", "local_activity", "timer", "condition", "signal", "child", "parallel", "selection_handle",
21
+ "service_operation",
22
+ }
20
23
  _RESOLUTION_EVENTS = {
21
24
  "ActivityCompleted",
22
25
  "ActivityFailed",
@@ -31,6 +34,9 @@ _RESOLUTION_EVENTS = {
31
34
  "ChildRunFailed",
32
35
  "ChildRunCancelled",
33
36
  "ChildRunTerminated",
37
+ "ServiceCallCompleted",
38
+ "ServiceCallFailed",
39
+ "ServiceCallCancelled",
34
40
  }
35
41
 
36
42
 
@@ -265,6 +271,8 @@ def read_cancellation_history(
265
271
  "ChildRunFailed",
266
272
  "ChildRunCancelled",
267
273
  "ChildRunTerminated",
274
+ "ServiceCallFailed",
275
+ "ServiceCallCancelled",
268
276
  }:
269
277
  failed.add(sequence)
270
278
  state = CancellationHistory(
@@ -302,8 +302,8 @@ def _contains_inline_payload_envelope(value: object) -> bool:
302
302
  if set(value) == {"codec", "blob"} and isinstance(value.get("blob"), str):
303
303
  return True
304
304
  if (
305
- value.get("type") == "record_side_effect"
306
- and isinstance(value.get("result"), str)
305
+ (value.get("type") == "record_side_effect" and isinstance(value.get("result"), str))
306
+ or (value.get("type") == "start_service_operation" and isinstance(value.get("request_payload"), str))
307
307
  ):
308
308
  return True
309
309
  return any(_contains_inline_payload_envelope(item) for item in value.values())
@@ -2097,21 +2097,23 @@ class Client:
2097
2097
  slot: tuple[str | int, ...] = (),
2098
2098
  ) -> Any:
2099
2099
  if isinstance(value, dict):
2100
- if (
2101
- value.get("type") == "record_side_effect"
2102
- and isinstance(value.get("result"), str)
2103
- ):
2100
+ command_type = value.get("type")
2101
+ blob_key = (
2102
+ {"record_side_effect": "result", "start_service_operation": "request_payload"}.get(command_type)
2103
+ if isinstance(command_type, str) else None
2104
+ )
2105
+ if blob_key is not None and isinstance(value.get(blob_key), str):
2104
2106
  normalized_command = dict(value)
2105
2107
  externalized_result = await self._externalize_runtime_payloads(
2106
- {"codec": serializer.AVRO_CODEC, "blob": value["result"]},
2108
+ {"codec": value.get("payload_codec", serializer.AVRO_CODEC), "blob": value[blob_key]},
2107
2109
  worker=worker,
2108
2110
  transport=transport,
2109
2111
  uploaded=uploaded,
2110
2112
  completion=completion,
2111
- slot=(*slot, "result"),
2113
+ slot=(*slot, blob_key),
2112
2114
  )
2113
2115
  if "external_payload" in externalized_result:
2114
- normalized_command["result"] = externalized_result
2116
+ normalized_command[blob_key] = externalized_result
2115
2117
  value = normalized_command
2116
2118
 
2117
2119
  if set(value) == {"codec", "blob"} and isinstance(value.get("blob"), str):
@@ -101,10 +101,8 @@ from .workflow import (
101
101
  Command,
102
102
  FailWorkflow,
103
103
  LocalActivityExecutionAborted,
104
- NexusServiceCall,
105
104
  PreparedLocalActivityCall,
106
105
  RecordLocalActivity,
107
- RecordSideEffect,
108
106
  ReplayOutcome,
109
107
  ScheduleActivity,
110
108
  StartChildWorkflow,
@@ -169,7 +167,6 @@ _WORKFLOW_TASK_COMPLETION_AMBIGUOUS_REJECTION_REASONS = {
169
167
  }
170
168
  _WORKFLOW_TASK_COMPLETION_MAX_ATTEMPTS = 3
171
169
  _WORKFLOW_TASK_COMPLETION_RETRY_DELAYS = (0.05, 0.2)
172
- _WORKFLOW_TASK_NEXUS_RESOLUTION_LIMIT = 100
173
170
  _LOCAL_ACTIVITY_REPORT_LIMIT = 1000
174
171
  _WORKER_WORKFLOW_FINGERPRINTS: dict[tuple[str, str], str] = {}
175
172
  _P = ParamSpec("_P")
@@ -1072,6 +1069,7 @@ class Worker:
1072
1069
  if "sticky_execution" in self.capabilities and not self._sticky_cache.enabled:
1073
1070
  raise ValueError("sticky_execution requires a positive sticky_cache_capacity")
1074
1071
  self._cooperative_cancellation_supported = False
1072
+ self._nexus_cooperative_cancellation_supported = False
1075
1073
  self._prepared_local_activities_supported = False
1076
1074
  self._prepared_local_activity_groups_supported = False
1077
1075
  self._allow_cancellation_scope_authoring = False
@@ -1087,6 +1085,9 @@ class Worker:
1087
1085
  raise ValueError("prepared local groups require prepared_local_activities capability")
1088
1086
  if "prepared_local_activities" in self.capabilities and "cooperative_cancellation" not in self.capabilities:
1089
1087
  raise ValueError("prepared local activities require cooperative_cancellation capability")
1088
+ if ("nexus_cooperative_cancellation" in self.capabilities
1089
+ and "cooperative_cancellation" not in self.capabilities):
1090
+ raise ValueError("Nexus cooperative cancellation requires cooperative_cancellation capability")
1090
1091
  if any(not capability for capability in self.capabilities):
1091
1092
  raise ValueError("worker capabilities must be non-empty strings")
1092
1093
  self.worker_id = worker_id or f"py-worker-{uuid.uuid4().hex[:8]}"
@@ -1272,6 +1273,17 @@ class Worker:
1272
1273
  )
1273
1274
  if "cooperative_cancellation" in self.capabilities and not self._cooperative_cancellation_supported:
1274
1275
  raise RuntimeError("cooperative cancellation requires explicit compatible runtime and worker protocol 1.20")
1276
+ self._nexus_cooperative_cancellation_supported = (
1277
+ self._cooperative_cancellation_supported
1278
+ and isinstance(server_capabilities, Mapping)
1279
+ and server_capabilities.get("nexus_cooperative_cancellation") is True
1280
+ and isinstance(server_capabilities.get("nexus_cooperative_cancellation_binding_kinds"), list)
1281
+ and "workflow_run" in server_capabilities["nexus_cooperative_cancellation_binding_kinds"]
1282
+ )
1283
+ if "nexus_cooperative_cancellation" in self.capabilities and not self._nexus_cooperative_cancellation_supported:
1284
+ raise RuntimeError(
1285
+ "nexus_cooperative_cancellation_not_supported: Server must advertise its installed workflow_run bridge",
1286
+ )
1275
1287
  self._prepared_local_activities_supported = (
1276
1288
  "prepared_local_activities" in self.capabilities and self._cooperative_cancellation_supported
1277
1289
  and isinstance(server_capabilities, Mapping)
@@ -1342,6 +1354,8 @@ class Worker:
1342
1354
  capabilities.append(WORKFLOW_UPDATES_CAPABILITY)
1343
1355
  capabilities.append(MESSAGE_STREAMS_CAPABILITY)
1344
1356
  capabilities.extend(self.capabilities)
1357
+ if self._nexus_cooperative_cancellation_supported and "nexus_cooperative_cancellation" not in capabilities:
1358
+ capabilities.append("nexus_cooperative_cancellation")
1345
1359
  if (
1346
1360
  self._local_activity_cancellation_policies
1347
1361
  and "prepared_local_activity_cancellation_policies" not in capabilities
@@ -1373,6 +1387,11 @@ class Worker:
1373
1387
  capabilities=capabilities,
1374
1388
  capability_manifest={
1375
1389
  **affinity_manifest,
1390
+ **({"nexus_cooperative_cancellation": {
1391
+ "supported": True, "minimum_protocol_version": "1.20",
1392
+ "implementation": "owned_workflow_run_canonical_delivery",
1393
+ "binding_kinds": ["workflow_run"],
1394
+ }} if self._nexus_cooperative_cancellation_supported else {}),
1376
1395
  **({"prepared_local_activities": {
1377
1396
  "supported": True, "minimum_protocol_version": "1.20",
1378
1397
  "implementation": "durable_sequential_admission",
@@ -1452,86 +1471,6 @@ class Worker:
1452
1471
 
1453
1472
  return await handler(context)
1454
1473
 
1455
- async def _resolve_workflow_nexus_commands(
1456
- self,
1457
- workflow_cls: type,
1458
- history: list[dict[str, Any]],
1459
- start_input: list[Any],
1460
- task: dict[str, Any],
1461
- *,
1462
- payload_codec: str | None,
1463
- command_codec: str,
1464
- initial_commands: list[Command],
1465
- ) -> list[Command]:
1466
- events = list(history)
1467
- commands: list[Command] = list(initial_commands)
1468
- recorded_commands: list[RecordSideEffect] = []
1469
-
1470
- for _ in range(_WORKFLOW_TASK_NEXUS_RESOLUTION_LIMIT):
1471
- nexus_commands = [command for command in commands if isinstance(command, NexusServiceCall)]
1472
- if not nexus_commands:
1473
- return recorded_commands + commands
1474
- if len(nexus_commands) != len(commands):
1475
- raise RuntimeError("Nexus service calls must be yielded one at a time")
1476
-
1477
- for command in nexus_commands:
1478
- result = await self.client.execute_nexus_operation(
1479
- command.endpoint_name,
1480
- command.service_name,
1481
- command.operation_name,
1482
- command.arguments,
1483
- mode=command.mode,
1484
- wait_for=command.wait_for,
1485
- wait_timeout_seconds=command.wait_timeout_seconds,
1486
- idempotency_key=command.idempotency_key,
1487
- payload_codec=command.payload_codec,
1488
- caller_namespace=command.caller_namespace,
1489
- target_namespace=command.target_namespace,
1490
- caller_workflow_instance_id=_string_or_none(task.get("workflow_id")),
1491
- caller_workflow_run_id=_string_or_none(task.get("run_id")),
1492
- service_sdk_language=command.service_sdk_language,
1493
- artifact_tuple=command.artifact_tuple,
1494
- published_artifact_worker_execution=command.published_artifact_worker_execution,
1495
- target_workflow_instance_id=command.target_workflow_instance_id,
1496
- target_workflow_run_id=command.target_workflow_run_id,
1497
- connection=command.connection,
1498
- queue=command.queue,
1499
- business_key=command.business_key,
1500
- labels=command.labels,
1501
- memo=command.memo,
1502
- search_attributes=command.search_attributes,
1503
- duplicate_start_policy=command.duplicate_start_policy,
1504
- raise_on_failure=False,
1505
- )
1506
- recorded_payload = result.to_recorded_payload()
1507
- recorded_commands.append(RecordSideEffect(recorded_payload))
1508
- events.append({
1509
- "event_type": "SideEffectRecorded",
1510
- "payload": {
1511
- "payload_codec": command_codec,
1512
- "result": serializer.encode(recorded_payload, codec=command_codec),
1513
- },
1514
- })
1515
-
1516
- outcome = replay(
1517
- workflow_cls,
1518
- events,
1519
- start_input,
1520
- workflow_id=task.get("workflow_id"),
1521
- run_id=_string_or_none(task.get("run_id")) or "",
1522
- workflow_command_id=(
1523
- _string_or_none(task.get("workflow_command_id"))
1524
- or _string_or_none(task.get("task_id"))
1525
- ),
1526
- payload_codec=payload_codec,
1527
- external_storage=self.external_storage,
1528
- external_storage_cache=self.external_storage_cache,
1529
- cancel_requested=bool(task.get("cancel_requested", False)),
1530
- )
1531
- commands = list(outcome.commands)
1532
-
1533
- raise RuntimeError("workflow yielded too many consecutive Nexus service calls")
1534
-
1535
1474
  async def _renew_workflow_payload_lease(self, task: dict[str, Any]) -> None:
1536
1475
  if self._stop.is_set() or self._local_activity_shutdown.is_set():
1537
1476
  raise LocalActivityExecutionAborted("worker shutdown stopped its workflow payload transfer")
@@ -1852,6 +1791,11 @@ class Worker:
1852
1791
  # Earlier authored commands must commit on this claim first.
1853
1792
  # Completion releases the claim; a successor replays their durable results.
1854
1793
  return outcome, history
1794
+ if intent.call_kind == "service_operation" and not self._nexus_cooperative_cancellation_supported:
1795
+ raise LocalActivityExecutionAborted(
1796
+ "nexus_cooperative_cancellation_not_supported: "
1797
+ "delivery requires installed Server discovery and a cooperative protocol 1.20 worker",
1798
+ )
1855
1799
  delivery_attempts += 1
1856
1800
  if delivery_attempts > 3:
1857
1801
  raise LocalActivityExecutionAborted("cancellation replay did not converge on its canonical delivery")
@@ -1866,7 +1810,20 @@ class Worker:
1866
1810
  operation_sequence_span=intent.operation_sequence_span,
1867
1811
  )
1868
1812
  if delivery_reply.get("delivered") is False:
1869
- raise _WorkflowClaimDeferred("parent awaits canonical child cleanup on a new claim")
1813
+ pending_reasons = {
1814
+ "service_operation": {"cancellation_waiting_for_service_operation"},
1815
+ "child": {"cancellation_waiting_for_child"},
1816
+ "activity": {"cancellation_waiting_for_activity"},
1817
+ "local_activity": {"cancellation_waiting_for_activity"},
1818
+ "parallel": {"cancellation_waiting_for_child", "cancellation_waiting_for_activity"},
1819
+ "selection_handle": {"cancellation_waiting_for_child", "cancellation_waiting_for_activity"},
1820
+ }.get(intent.call_kind, set())
1821
+ if (delivery_reply.get("reason") not in pending_reasons
1822
+ or delivery_reply.get("claim_released") is not True
1823
+ or delivery_reply.get("task_id") != task["task_id"]
1824
+ or delivery_reply.get("workflow_run_id") != task["run_id"]):
1825
+ raise LocalActivityExecutionAborted("pending cancellation lacks this claim's release receipt")
1826
+ raise _WorkflowClaimDeferred("pending cancellation: target cleanup continues on a new claim")
1870
1827
  except _WorkflowClaimDeferred:
1871
1828
  raise
1872
1829
  except Exception as error:
@@ -2675,45 +2632,7 @@ class Worker:
2675
2632
  log.warning("failed to report replay failure: %s", fe)
2676
2633
  return None
2677
2634
 
2678
- try:
2679
- workflow_commands = await self._resolve_workflow_nexus_commands(
2680
- cls,
2681
- list(history),
2682
- start_input,
2683
- task,
2684
- payload_codec=codec,
2685
- command_codec=command_codec,
2686
- initial_commands=list(outcome.commands),
2687
- )
2688
- except Exception as e:
2689
- log.exception("workflow Nexus service call resolution failed")
2690
- last_local = max(
2691
- (index for index, command in enumerate(outcome.commands)
2692
- if isinstance(command, RecordLocalActivity) and command.outcome is not None),
2693
- default=-1,
2694
- )
2695
- if last_local >= 0:
2696
- workflow_commands = [
2697
- *outcome.commands[:last_local + 1],
2698
- FailWorkflow(
2699
- f"Nexus resolution after local activity failed: {e}",
2700
- exception_type=type(e).__name__,
2701
- non_retryable=True,
2702
- ),
2703
- ]
2704
- else:
2705
- try:
2706
- await self.client.fail_workflow_task(
2707
- task_id=task_id,
2708
- lease_owner=self.worker_id,
2709
- workflow_task_attempt=attempt,
2710
- message=f"workflow Nexus service call resolution failed: {e}",
2711
- failure_type=type(e).__name__,
2712
- stack_trace=traceback.format_exc(),
2713
- )
2714
- except Exception as fe:
2715
- log.warning("failed to report workflow Nexus resolution failure: %s", fe)
2716
- return None
2635
+ workflow_commands = list(outcome.commands)
2717
2636
 
2718
2637
  if (
2719
2638
  any(isinstance(command, UpsertMemo) for command in workflow_commands)