openbox-temporal-sdk-python 1.2.1__tar.gz → 1.3.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.
- {openbox_temporal_sdk_python-1.2.1 → openbox_temporal_sdk_python-1.3.0}/CHANGELOG.md +12 -0
- {openbox_temporal_sdk_python-1.2.1 → openbox_temporal_sdk_python-1.3.0}/PKG-INFO +5 -3
- {openbox_temporal_sdk_python-1.2.1 → openbox_temporal_sdk_python-1.3.0}/README.md +3 -1
- {openbox_temporal_sdk_python-1.2.1 → openbox_temporal_sdk_python-1.3.0}/docs/configuration.md +49 -0
- {openbox_temporal_sdk_python-1.2.1 → openbox_temporal_sdk_python-1.3.0}/docs/system-architecture.md +70 -20
- {openbox_temporal_sdk_python-1.2.1 → openbox_temporal_sdk_python-1.3.0}/openbox/__init__.py +28 -30
- {openbox_temporal_sdk_python-1.2.1 → openbox_temporal_sdk_python-1.3.0}/openbox/activities.py +24 -12
- {openbox_temporal_sdk_python-1.2.1 → openbox_temporal_sdk_python-1.3.0}/openbox/activity_interceptor.py +108 -35
- {openbox_temporal_sdk_python-1.2.1 → openbox_temporal_sdk_python-1.3.0}/openbox/config.py +41 -13
- {openbox_temporal_sdk_python-1.2.1 → openbox_temporal_sdk_python-1.3.0}/openbox/core_adapter.py +55 -5
- {openbox_temporal_sdk_python-1.2.1 → openbox_temporal_sdk_python-1.3.0}/openbox/errors.py +12 -1
- {openbox_temporal_sdk_python-1.2.1 → openbox_temporal_sdk_python-1.3.0}/openbox/governance_state.py +46 -12
- {openbox_temporal_sdk_python-1.2.1 → openbox_temporal_sdk_python-1.3.0}/openbox/hitl.py +25 -0
- {openbox_temporal_sdk_python-1.2.1 → openbox_temporal_sdk_python-1.3.0}/openbox/multi_agent.py +38 -8
- {openbox_temporal_sdk_python-1.2.1 → openbox_temporal_sdk_python-1.3.0}/openbox/plugin.py +5 -2
- openbox_temporal_sdk_python-1.3.0/openbox/retry_coordinator.py +136 -0
- openbox_temporal_sdk_python-1.3.0/openbox/retryable_block.py +179 -0
- {openbox_temporal_sdk_python-1.2.1 → openbox_temporal_sdk_python-1.3.0}/openbox/worker.py +8 -13
- openbox_temporal_sdk_python-1.3.0/openbox/workflow_interceptor.py +719 -0
- {openbox_temporal_sdk_python-1.2.1 → openbox_temporal_sdk_python-1.3.0}/pyproject.toml +3 -2
- {openbox_temporal_sdk_python-1.2.1 → openbox_temporal_sdk_python-1.3.0}/tests/test_activities.py +162 -5
- {openbox_temporal_sdk_python-1.2.1 → openbox_temporal_sdk_python-1.3.0}/tests/test_activity_interceptor.py +390 -38
- {openbox_temporal_sdk_python-1.2.1 → openbox_temporal_sdk_python-1.3.0}/tests/test_config.py +41 -8
- {openbox_temporal_sdk_python-1.2.1 → openbox_temporal_sdk_python-1.3.0}/tests/test_multi_agent.py +83 -12
- {openbox_temporal_sdk_python-1.2.1 → openbox_temporal_sdk_python-1.3.0}/tests/test_plugin.py +26 -6
- {openbox_temporal_sdk_python-1.2.1 → openbox_temporal_sdk_python-1.3.0}/tests/test_public_api_compatibility.py +9 -2
- openbox_temporal_sdk_python-1.3.0/tests/test_retry_coordinator_and_budget.py +151 -0
- openbox_temporal_sdk_python-1.3.0/tests/test_retryable_block_e2e.py +292 -0
- openbox_temporal_sdk_python-1.3.0/tests/test_retryable_block_envelope.py +298 -0
- openbox_temporal_sdk_python-1.3.0/tests/test_retryable_block_replay.py +227 -0
- openbox_temporal_sdk_python-1.3.0/tests/test_temporal_hook_parity.py +236 -0
- {openbox_temporal_sdk_python-1.2.1 → openbox_temporal_sdk_python-1.3.0}/tests/test_worker.py +27 -4
- {openbox_temporal_sdk_python-1.2.1 → openbox_temporal_sdk_python-1.3.0}/tests/test_workflow_interceptor.py +744 -41
- {openbox_temporal_sdk_python-1.2.1 → openbox_temporal_sdk_python-1.3.0}/tests/test_workflow_sandbox_import_safety.py +2 -0
- {openbox_temporal_sdk_python-1.2.1 → openbox_temporal_sdk_python-1.3.0}/uv.lock +5 -5
- openbox_temporal_sdk_python-1.2.1/openbox/workflow_interceptor.py +0 -372
- openbox_temporal_sdk_python-1.2.1/tests/test_temporal_hook_parity.py +0 -129
- {openbox_temporal_sdk_python-1.2.1 → openbox_temporal_sdk_python-1.3.0}/.github/workflows/publish.yml +0 -0
- {openbox_temporal_sdk_python-1.2.1 → openbox_temporal_sdk_python-1.3.0}/.github/workflows/sonarqube.yaml +0 -0
- {openbox_temporal_sdk_python-1.2.1 → openbox_temporal_sdk_python-1.3.0}/.gitignore +0 -0
- {openbox_temporal_sdk_python-1.2.1 → openbox_temporal_sdk_python-1.3.0}/.python-version +0 -0
- {openbox_temporal_sdk_python-1.2.1 → openbox_temporal_sdk_python-1.3.0}/.repomixignore +0 -0
- {openbox_temporal_sdk_python-1.2.1 → openbox_temporal_sdk_python-1.3.0}/LICENSE +0 -0
- {openbox_temporal_sdk_python-1.2.1 → openbox_temporal_sdk_python-1.3.0}/docs/changelog-hook-level-governance.md +0 -0
- {openbox_temporal_sdk_python-1.2.1 → openbox_temporal_sdk_python-1.3.0}/docs/code-standards.md +0 -0
- {openbox_temporal_sdk_python-1.2.1 → openbox_temporal_sdk_python-1.3.0}/docs/codebase-summary.md +0 -0
- {openbox_temporal_sdk_python-1.2.1 → openbox_temporal_sdk_python-1.3.0}/docs/project-overview-pdr.md +0 -0
- {openbox_temporal_sdk_python-1.2.1 → openbox_temporal_sdk_python-1.3.0}/docs/temporal-plugin-integration-guide.md +0 -0
- {openbox_temporal_sdk_python-1.2.1 → openbox_temporal_sdk_python-1.3.0}/openbox/client.py +0 -0
- {openbox_temporal_sdk_python-1.2.1 → openbox_temporal_sdk_python-1.3.0}/openbox/py.typed +0 -0
- {openbox_temporal_sdk_python-1.2.1 → openbox_temporal_sdk_python-1.3.0}/openbox/request_signing.py +0 -0
- {openbox_temporal_sdk_python-1.2.1 → openbox_temporal_sdk_python-1.3.0}/openbox/tracing.py +0 -0
- {openbox_temporal_sdk_python-1.2.1 → openbox_temporal_sdk_python-1.3.0}/openbox/types.py +0 -0
- {openbox_temporal_sdk_python-1.2.1 → openbox_temporal_sdk_python-1.3.0}/openbox/verdict_handler.py +0 -0
- {openbox_temporal_sdk_python-1.2.1 → openbox_temporal_sdk_python-1.3.0}/release-manifest.json +0 -0
- {openbox_temporal_sdk_python-1.2.1 → openbox_temporal_sdk_python-1.3.0}/repomix-output.xml +0 -0
- {openbox_temporal_sdk_python-1.2.1 → openbox_temporal_sdk_python-1.3.0}/sonar-project.properties +0 -0
- {openbox_temporal_sdk_python-1.2.1 → openbox_temporal_sdk_python-1.3.0}/tests/__init__.py +0 -0
- {openbox_temporal_sdk_python-1.2.1 → openbox_temporal_sdk_python-1.3.0}/tests/conftest.py +0 -0
- {openbox_temporal_sdk_python-1.2.1 → openbox_temporal_sdk_python-1.3.0}/tests/test_approval_action_precedence.py +0 -0
- {openbox_temporal_sdk_python-1.2.1 → openbox_temporal_sdk_python-1.3.0}/tests/test_base_sdk_signing_parity.py +0 -0
- {openbox_temporal_sdk_python-1.2.1 → openbox_temporal_sdk_python-1.3.0}/tests/test_core_conformance_suite.py +0 -0
- {openbox_temporal_sdk_python-1.2.1 → openbox_temporal_sdk_python-1.3.0}/tests/test_no_import_machinery_in_governed_open_paths.py +0 -0
- {openbox_temporal_sdk_python-1.2.1 → openbox_temporal_sdk_python-1.3.0}/tests/test_plugin_integration.py +0 -0
- {openbox_temporal_sdk_python-1.2.1 → openbox_temporal_sdk_python-1.3.0}/tests/test_request_signing.py +0 -0
- {openbox_temporal_sdk_python-1.2.1 → openbox_temporal_sdk_python-1.3.0}/tests/test_signing_config.py +0 -0
- {openbox_temporal_sdk_python-1.2.1 → openbox_temporal_sdk_python-1.3.0}/tests/test_types.py +0 -0
|
@@ -4,6 +4,18 @@ All notable changes to OpenBox SDK for Temporal Workflows.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [1.3.0] - 2026-07-21
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **Retryable BLOCK workflow restart:** A governance response carrying `BLOCK` with a valid `retry_plan` (containing `new_input`) now triggers a Continue-As-New restart of the workflow using the replacement input. The restart is event-agnostic (supports WorkflowStarted, WorkflowCompleted, WorkflowFailed, SignalReceived, ActivityStarted, ActivityCompleted, Handoff, hook evaluations, and approval polling) and is bounded by the `max_retryable_block_restarts` configuration (default: 3, minimum: 1). When the bound is exceeded, the workflow fails with a non-retryable `GovernanceRetryLimitExceeded` error. The restart count is tracked across the entire workflow chain via a durable memo counter. Plain BLOCK (without a retry plan), HALT, expired approvals, and malformed plans preserve existing behavior and do not restart.
|
|
12
|
+
|
|
13
|
+
- **Workflow input capture in `WorkflowStarted`:** The `WorkflowStarted` governance event now carries the Temporal workflow arguments in the `activity_input` field, so OpenBox stores and displays workflow input the same way as activity input. The outer argument list is preserved verbatim (`workflow(a, b)` → `[a, b]`; a single list argument stays nested as `[[...]]`; no arguments → `[]`). Because this changes the `send_governance_event` activity input, it is version-gated by a dedicated patch marker (`openbox-workflow-start-input-v1`): new workflow histories include `activity_input`, while histories created before this change replay their original payload unchanged.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- **`openbox-sdk-python` dependency raised to `>=1.1.0`, installed from PyPI.** The retryable-BLOCK restart requires `handle_retryable_block` from base SDK 1.1.0; now that 1.1.0 is published, the dependency resolves from PyPI and the temporary local editable source is removed.
|
|
18
|
+
|
|
7
19
|
## [1.2.1] - 2026-07-21
|
|
8
20
|
|
|
9
21
|
### Changed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openbox-temporal-sdk-python
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
4
4
|
Summary: OpenBox SDK - Governance and observability for Temporal workflows
|
|
5
5
|
Project-URL: Homepage, https://github.com/OpenBox-AI/temporal-sdk-python
|
|
6
6
|
Project-URL: Documentation, https://github.com/OpenBox-AI/temporal-sdk-python#readme
|
|
@@ -24,7 +24,7 @@ Requires-Python: >=3.11
|
|
|
24
24
|
Requires-Dist: asyncpg>=0.29.0
|
|
25
25
|
Requires-Dist: httpx<1,>=0.28.0
|
|
26
26
|
Requires-Dist: mysql-connector-python>=8.0.0
|
|
27
|
-
Requires-Dist: openbox-sdk-python>=1.0
|
|
27
|
+
Requires-Dist: openbox-sdk-python>=1.1.0
|
|
28
28
|
Requires-Dist: opentelemetry-api<1.40.0,>=1.38.0
|
|
29
29
|
Requires-Dist: opentelemetry-instrumentation-asyncpg<0.61b0,>=0.59b0
|
|
30
30
|
Requires-Dist: opentelemetry-instrumentation-httpx<0.61b0,>=0.59b0
|
|
@@ -227,9 +227,11 @@ OpenBox Core returns a verdict indicating what action the SDK should take.
|
|
|
227
227
|
| `ALLOW` | Continue execution normally |
|
|
228
228
|
| `CONSTRAIN` | Log constraints, continue |
|
|
229
229
|
| `REQUIRE_APPROVAL` | Pause, poll for human approval |
|
|
230
|
-
| `BLOCK` | Raise error, stop activity |
|
|
230
|
+
| `BLOCK` | Raise error, stop activity; if carrying a `retry_plan`, restart the workflow with replacement input via Continue-As-New (see "Retryable BLOCK" below) |
|
|
231
231
|
| `HALT` | Raise error, terminate workflow |
|
|
232
232
|
|
|
233
|
+
**Retryable BLOCK:** When a `BLOCK` verdict includes a `retry_plan` with `new_input`, the SDK restarts the workflow using the corrected input instead of failing permanently. The restart chain is bounded by `max_retryable_block_restarts` (default: 3). See [Retryable BLOCK Restart Configuration](./docs/configuration.md#retryable-block-restart) for semantics and **critical idempotency requirements**.
|
|
234
|
+
|
|
233
235
|
**v1.0 Backward Compatibility:**
|
|
234
236
|
- `"continue"` → `ALLOW`
|
|
235
237
|
- `"stop"` → `HALT`
|
|
@@ -178,9 +178,11 @@ OpenBox Core returns a verdict indicating what action the SDK should take.
|
|
|
178
178
|
| `ALLOW` | Continue execution normally |
|
|
179
179
|
| `CONSTRAIN` | Log constraints, continue |
|
|
180
180
|
| `REQUIRE_APPROVAL` | Pause, poll for human approval |
|
|
181
|
-
| `BLOCK` | Raise error, stop activity |
|
|
181
|
+
| `BLOCK` | Raise error, stop activity; if carrying a `retry_plan`, restart the workflow with replacement input via Continue-As-New (see "Retryable BLOCK" below) |
|
|
182
182
|
| `HALT` | Raise error, terminate workflow |
|
|
183
183
|
|
|
184
|
+
**Retryable BLOCK:** When a `BLOCK` verdict includes a `retry_plan` with `new_input`, the SDK restarts the workflow using the corrected input instead of failing permanently. The restart chain is bounded by `max_retryable_block_restarts` (default: 3). See [Retryable BLOCK Restart Configuration](./docs/configuration.md#retryable-block-restart) for semantics and **critical idempotency requirements**.
|
|
185
|
+
|
|
184
186
|
**v1.0 Backward Compatibility:**
|
|
185
187
|
- `"continue"` → `ALLOW`
|
|
186
188
|
- `"stop"` → `HALT`
|
{openbox_temporal_sdk_python-1.2.1 → openbox_temporal_sdk_python-1.3.0}/docs/configuration.md
RENAMED
|
@@ -31,6 +31,55 @@
|
|
|
31
31
|
| `hitl_enabled` | `bool` | `True` | Enable approval polling for `REQUIRE_APPROVAL` |
|
|
32
32
|
| `hitl_poll_interval_ms` | `int` | `5000` | Polling interval in milliseconds for approval status |
|
|
33
33
|
|
|
34
|
+
## Retryable BLOCK Restart
|
|
35
|
+
|
|
36
|
+
| Parameter | Type | Default | Description |
|
|
37
|
+
|-----------|------|---------|-------------|
|
|
38
|
+
| `max_retryable_block_restarts` | `int` | `3` | Maximum Continue-As-New restarts triggered by a BLOCK with `retry_plan`. Minimum value: `1`. Applies uniformly across all governance event origins. When the count would exceed this limit, the workflow fails with a non-retryable `GovernanceRetryLimitExceeded` error. |
|
|
39
|
+
|
|
40
|
+
### Semantics
|
|
41
|
+
|
|
42
|
+
A governance response carrying `BLOCK` with a valid `retry_plan` (containing `new_input`) will:
|
|
43
|
+
|
|
44
|
+
1. **Stop the current execution path** with a non-retryable error signal
|
|
45
|
+
2. **Not retry** the blocked activity with the same input (Temporal activity retry is bypassed)
|
|
46
|
+
3. **Restart the workflow via Continue-As-New** using `retry_plan.new_input` as replacement input
|
|
47
|
+
- If `new_input` is `null`, all arguments of the current run are reused exactly
|
|
48
|
+
- If `new_input` is a non-null value, it is passed as one workflow argument
|
|
49
|
+
4. **Keep the same Workflow ID** — the previous run closes as `ContinuedAsNew`; a new Run ID is issued for the restarted execution
|
|
50
|
+
5. **Track restarts across the chain** — a global counter (per workflow ID) increments with each restart and is bounded by `max_retryable_block_restarts` to prevent infinite loops
|
|
51
|
+
|
|
52
|
+
This behavior is **event-agnostic**. The following governance event types all support retryable BLOCK:
|
|
53
|
+
|
|
54
|
+
- `WorkflowStarted`
|
|
55
|
+
- `WorkflowCompleted`
|
|
56
|
+
- `WorkflowFailed`
|
|
57
|
+
- `SignalReceived`
|
|
58
|
+
- `ActivityStarted`
|
|
59
|
+
- `ActivityCompleted`
|
|
60
|
+
- `Handoff`
|
|
61
|
+
- Started and completed hooks
|
|
62
|
+
- Approval polling responses
|
|
63
|
+
|
|
64
|
+
**Origins that do NOT restart:** `HALT`, expired approvals, malformed plans, and plain BLOCK (without a valid `retry_plan`) use existing behavior.
|
|
65
|
+
|
|
66
|
+
### Critical: Idempotency and Side Effects
|
|
67
|
+
|
|
68
|
+
**This is the most important user-facing hazard.** A retry plan may be produced *after* side effects have already occurred:
|
|
69
|
+
|
|
70
|
+
- A policy may return a retry plan in response to `ActivityCompleted` — after your activity already ran and may have modified external state
|
|
71
|
+
- A completed hook may return a retry plan — after the operation has occurred
|
|
72
|
+
- `WorkflowCompleted` or `WorkflowFailed` may return a retry plan — after your workflow has executed and potentially caused side effects
|
|
73
|
+
|
|
74
|
+
**You are responsible for ensuring safety:**
|
|
75
|
+
|
|
76
|
+
- **Activities must remain idempotent.** If an activity is executed, rolls back or completes, and then the workflow restarts with corrected input, the activity must be safe to re-execute (or be designed to avoid re-execution for the same business key).
|
|
77
|
+
- **External writes should use stable business idempotency keys**, not the Temporal Run ID (which changes across Continue-As-New). For example, if writing to an external system, use your application's stable resource identifier, not `workflow.get_info().run_id`.
|
|
78
|
+
- **Policy authors and governance integrators own emitting safe replacement input.** The SDK does not validate that `new_input` is safe; it assumes the governance policy has vetted the input and the consequences of re-execution.
|
|
79
|
+
- **The SDK adds no source-specific restrictions.** You have full control and full responsibility.
|
|
80
|
+
|
|
81
|
+
Log any external state changes with identifiers your team can use to trace and reconcile retries. Monitor governance restart counts to detect policy bugs that cause repeated retries.
|
|
82
|
+
|
|
34
83
|
## Identity & Signing (AIP DID)
|
|
35
84
|
|
|
36
85
|
When provided, every request to OpenBox Core is signed locally with Ed25519 and
|
{openbox_temporal_sdk_python-1.2.1 → openbox_temporal_sdk_python-1.3.0}/docs/system-architecture.md
RENAMED
|
@@ -289,32 +289,17 @@ OpenBoxError (base)
|
|
|
289
289
|
|
|
290
290
|
#### TemporalGovernanceState
|
|
291
291
|
|
|
292
|
-
**Responsibility:** Hold the small amount of Temporal semantics that must survive past a base-SDK hook callback — signal verdicts, HITL pending markers,
|
|
292
|
+
**Responsibility:** Hold the small amount of Temporal semantics that must survive past a base-SDK hook callback — signal verdicts, HITL pending markers, completed-hook stop bridge, and retryable-BLOCK requests. Workflow-safe, thread-safe, and shared by both interceptors.
|
|
293
293
|
|
|
294
294
|
The base SDK (`openbox_core`) owns hook context, hook payload building, hook evaluation, and the within-activity abort short-circuit (its `ContextStore`). This object holds only the effects the base runtime cannot express itself. All keys are **run-scoped**: state from a prior run with the same `workflow_id` is ignored and cleared.
|
|
295
295
|
|
|
296
|
-
**
|
|
297
|
-
```python
|
|
298
|
-
class TemporalGovernanceState:
|
|
299
|
-
# Signal verdicts (workflow interceptor → next activity)
|
|
300
|
-
def set_signal_verdict(self, workflow_id, run_id, verdict, reason=None) -> None
|
|
301
|
-
def get_signal_verdict(self, workflow_id, run_id) -> Optional[(Verdict, reason)] # clears stale run
|
|
302
|
-
def clear_signal_verdict(self, workflow_id) -> None
|
|
303
|
-
|
|
304
|
-
# HITL pending-approval markers
|
|
305
|
-
def mark_pending_approval(self, workflow_id, run_id, activity_id) -> None
|
|
306
|
-
def has_pending_approval(self, workflow_id, run_id, activity_id) -> bool
|
|
307
|
-
def clear_pending_approval(self, workflow_id, run_id, activity_id) -> None
|
|
296
|
+
**Code Location:** `openbox/governance_state.py`
|
|
308
297
|
|
|
309
|
-
|
|
310
|
-
def record_completed_stop(self, workflow_id, run_id, activity_id, verdict, reason=None) -> None
|
|
311
|
-
def take_completed_stop(self, workflow_id, run_id, activity_id) -> Optional[(Verdict, reason)]
|
|
298
|
+
**Retryable BLOCK Restart Bridge:** The workflow interceptor receives retryable-BLOCK requests from governance responses (activity results, signals, hooks, approval polls) and submits them to a run-local `RetryableBlockCoordinator`. The coordinator stores the first request (first-wins semantics); the inbound workflow boundary later reads it, validates the restart budget (incrementing a memo counter bounded by `max_retryable_block_restarts`), and calls `continue_as_new(new_input, memo=...)` with the replacement input and updated counter.
|
|
312
299
|
|
|
313
|
-
|
|
314
|
-
def cleanup_run(self, workflow_id, run_id) -> None
|
|
315
|
-
```
|
|
300
|
+
**Versioned Error Transport:** Activity-side retry requests use a stable, non-retryable `ApplicationError` with type `"GovernanceRetryableBlock"` carrying a versioned request envelope. The workflow boundary extracts and validates the envelope before attempting a restart. Unknown versions fail safely as plain BLOCK; Temporal-level failures (replay, malformed detail) do not trigger a Workflow Task loop.
|
|
316
301
|
|
|
317
|
-
**
|
|
302
|
+
**Replay Safety (Patch Marker `"openbox-retryable-block-v1"`):** The Continue-As-New command is protected by a Temporal patch marker so old histories (pre-feature) skip the new branch and replay unchanged. New histories take the retryable-BLOCK path deterministically.
|
|
318
303
|
|
|
319
304
|
---
|
|
320
305
|
|
|
@@ -635,6 +620,71 @@ Authorization: Bearer {api_key}
|
|
|
635
620
|
└────────────┘ └──────────────┘ └──────────────┘
|
|
636
621
|
```
|
|
637
622
|
|
|
623
|
+
### Retryable BLOCK Restart Flow
|
|
624
|
+
|
|
625
|
+
```
|
|
626
|
+
┌──────────────────────────────────────────┐
|
|
627
|
+
│ Governance response from any event type │
|
|
628
|
+
│ (activity, signal, hook, approval, etc) │
|
|
629
|
+
└────────────┬─────────────────────────────┘
|
|
630
|
+
│
|
|
631
|
+
▼
|
|
632
|
+
┌──────────────────────────────────────────┐
|
|
633
|
+
│ Base SDK handle_retryable_block() helper │
|
|
634
|
+
│ (checks for valid BLOCK + retry_plan) │
|
|
635
|
+
└────────────┬─────────────────────────────┘
|
|
636
|
+
│
|
|
637
|
+
┌──────┴──────────┐
|
|
638
|
+
│ │
|
|
639
|
+
▼ ▼
|
|
640
|
+
┌────────┐ ┌─────────────────┐
|
|
641
|
+
│No plan │ │RetryDirective │
|
|
642
|
+
└────────┘ │(new_input) │
|
|
643
|
+
│ └────────┬────────┘
|
|
644
|
+
▼ │
|
|
645
|
+
Existing BLOCK/HALT ┌─────┴────────────────┐
|
|
646
|
+
behavior │ │
|
|
647
|
+
▼ ▼
|
|
648
|
+
Activity/hook Workflow event/signal
|
|
649
|
+
context context
|
|
650
|
+
│ │
|
|
651
|
+
▼ ▼
|
|
652
|
+
Non-retryable Submit to run-local
|
|
653
|
+
ApplicationError RetryableBlockCoordinator
|
|
654
|
+
(transport signal) (first-wins)
|
|
655
|
+
│ │
|
|
656
|
+
└──────┬───────────────┘
|
|
657
|
+
│
|
|
658
|
+
▼
|
|
659
|
+
Workflow inbound boundary
|
|
660
|
+
│
|
|
661
|
+
┌─────────┴────────────┐
|
|
662
|
+
│ │
|
|
663
|
+
┌─────▼──────┐ ┌─────────────┐
|
|
664
|
+
│Read memo │ │Read request │
|
|
665
|
+
│counter │ │from coord │
|
|
666
|
+
└─────┬──────┘ └──────┬──────┘
|
|
667
|
+
│ │
|
|
668
|
+
┌─────────┴───────────────────┤
|
|
669
|
+
│ Increment and validate cap │
|
|
670
|
+
└─────────┬───────────────────┘
|
|
671
|
+
│
|
|
672
|
+
┌──────────┴──────────┐
|
|
673
|
+
│ │
|
|
674
|
+
┌────▼────┐ ┌──────▼──────┐
|
|
675
|
+
│ Over │ │ Under limit │
|
|
676
|
+
│ limit │ │ │
|
|
677
|
+
└────┬────┘ └──────┬───────┘
|
|
678
|
+
│ │
|
|
679
|
+
▼ ▼
|
|
680
|
+
ApplicationError Continue-As-New
|
|
681
|
+
(GovernanceRetry (same Workflow ID,
|
|
682
|
+
LimitExceeded) new Run ID, fresh
|
|
683
|
+
history)
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
**Code Location:** `openbox/workflow_interceptor.py`, `openbox/retry_coordinator.py`
|
|
687
|
+
|
|
638
688
|
### Hook-Level Governance Flow
|
|
639
689
|
|
|
640
690
|
Hook detection, payload building, and evaluation are owned by the `openbox_core` base runtime. The Temporal SDK provides the activity-context bridge (`core_activity_scope`) and maps the resulting verdict onto Temporal-native effects (`TemporalFrameworkAdapter`).
|
|
@@ -6,45 +6,43 @@
|
|
|
6
6
|
# it crashes the workflow sandbox as a circular import, lazily it recurses
|
|
7
7
|
# unboundedly from build_auth_headers on every evaluate. Keep in sync with
|
|
8
8
|
# pyproject.toml on release.
|
|
9
|
-
__version__ = "1.
|
|
10
|
-
|
|
11
|
-
from .worker import create_openbox_worker
|
|
12
|
-
|
|
13
|
-
from .config import (
|
|
14
|
-
initialize,
|
|
15
|
-
get_global_config,
|
|
16
|
-
GovernanceConfig,
|
|
17
|
-
)
|
|
9
|
+
__version__ = "1.3.0"
|
|
18
10
|
|
|
11
|
+
from .config import GovernanceConfig, get_global_config, initialize
|
|
19
12
|
from .errors import (
|
|
20
|
-
OpenBoxError,
|
|
21
|
-
OpenBoxConfigError,
|
|
22
|
-
OpenBoxAuthError,
|
|
23
|
-
OpenBoxNetworkError,
|
|
24
|
-
OpenBoxInsecureURLError,
|
|
25
|
-
OpenBoxSigningError,
|
|
26
|
-
GovernanceBlockedError,
|
|
27
|
-
GovernanceHaltError,
|
|
28
|
-
GovernanceAPIError,
|
|
29
|
-
GuardrailsValidationError,
|
|
30
13
|
ApprovalExpiredError,
|
|
31
14
|
ApprovalRejectedError,
|
|
32
15
|
ApprovalTimeoutError,
|
|
16
|
+
GovernanceAPIError,
|
|
17
|
+
GovernanceBlockedError,
|
|
18
|
+
GovernanceHaltError,
|
|
19
|
+
GuardrailsValidationError,
|
|
20
|
+
OpenBoxAuthError,
|
|
21
|
+
OpenBoxConfigError,
|
|
22
|
+
OpenBoxError,
|
|
23
|
+
OpenBoxInsecureURLError,
|
|
24
|
+
OpenBoxNetworkError,
|
|
25
|
+
OpenBoxSigningError,
|
|
33
26
|
extract_governance_error,
|
|
34
27
|
map_signing_error,
|
|
35
28
|
)
|
|
36
29
|
|
|
37
|
-
from .types import (
|
|
38
|
-
Verdict,
|
|
39
|
-
WorkflowEventType,
|
|
40
|
-
GovernanceVerdictResponse,
|
|
41
|
-
GuardrailsCheckResult,
|
|
42
|
-
)
|
|
43
|
-
|
|
44
30
|
# Multi-agent primitives (sandbox-safe — only imports temporalio.workflow eagerly;
|
|
45
31
|
# signing/HTTP routed lazily through the governance activity).
|
|
46
32
|
from .multi_agent import emit_handoff
|
|
47
33
|
|
|
34
|
+
# Retryable-BLOCK restart envelope (sandbox-safe: pure stdlib + base contracts).
|
|
35
|
+
from .retryable_block import (
|
|
36
|
+
GOVERNANCE_RETRYABLE_BLOCK_SCHEMA_VERSION,
|
|
37
|
+
RetryableBlockRequest,
|
|
38
|
+
)
|
|
39
|
+
from .types import (
|
|
40
|
+
GovernanceVerdictResponse,
|
|
41
|
+
GuardrailsCheckResult,
|
|
42
|
+
Verdict,
|
|
43
|
+
WorkflowEventType,
|
|
44
|
+
)
|
|
45
|
+
from .worker import create_openbox_worker
|
|
48
46
|
from .workflow_interceptor import GovernanceInterceptor
|
|
49
47
|
|
|
50
48
|
try:
|
|
@@ -54,11 +52,9 @@ try:
|
|
|
54
52
|
except ImportError:
|
|
55
53
|
pass # temporalio < 1.23.0, plugin not available
|
|
56
54
|
|
|
57
|
-
from .verdict_handler import enforce_verdict, VerdictEnforcementResult
|
|
58
|
-
|
|
59
|
-
from .hitl import handle_approval_response, raise_approval_pending, should_skip_hitl
|
|
60
|
-
|
|
61
55
|
from .client import GovernanceClient
|
|
56
|
+
from .hitl import handle_approval_response, raise_approval_pending, should_skip_hitl
|
|
57
|
+
from .verdict_handler import VerdictEnforcementResult, enforce_verdict
|
|
62
58
|
|
|
63
59
|
# NOTE: ActivityGovernanceInterceptor is NOT imported here because it imports
|
|
64
60
|
# OpenTelemetry which uses importlib_metadata -> os.stat, causing sandbox issues.
|
|
@@ -114,6 +110,8 @@ __all__ = [
|
|
|
114
110
|
"WorkflowEventType",
|
|
115
111
|
"GovernanceVerdictResponse",
|
|
116
112
|
"GuardrailsCheckResult",
|
|
113
|
+
"RetryableBlockRequest",
|
|
114
|
+
"GOVERNANCE_RETRYABLE_BLOCK_SCHEMA_VERSION",
|
|
117
115
|
"emit_handoff",
|
|
118
116
|
"GovernanceInterceptor",
|
|
119
117
|
"enforce_verdict",
|
{openbox_temporal_sdk_python-1.2.1 → openbox_temporal_sdk_python-1.3.0}/openbox/activities.py
RENAMED
|
@@ -21,18 +21,16 @@ executes. This ensures timestamps are generated in activity context (non-determi
|
|
|
21
21
|
code allowed) rather than workflow context (must be deterministic).
|
|
22
22
|
"""
|
|
23
23
|
|
|
24
|
-
import httpx
|
|
25
24
|
import logging
|
|
26
25
|
from datetime import datetime, timezone
|
|
27
|
-
from typing import
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
from .types import rfc3339_now as _rfc3339_now
|
|
26
|
+
from typing import Any, Dict, Optional
|
|
31
27
|
|
|
28
|
+
import httpx
|
|
32
29
|
from temporalio import activity
|
|
33
30
|
from temporalio.exceptions import ApplicationError
|
|
34
31
|
|
|
35
|
-
from .types import Verdict
|
|
32
|
+
from .types import GovernanceVerdictResponse, Verdict
|
|
33
|
+
from .types import rfc3339_now as _rfc3339_now
|
|
36
34
|
|
|
37
35
|
logger = logging.getLogger(__name__)
|
|
38
36
|
|
|
@@ -201,12 +199,26 @@ class GovernanceActivities:
|
|
|
201
199
|
)
|
|
202
200
|
|
|
203
201
|
data = response.json()
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
202
|
+
parsed = GovernanceVerdictResponse.from_dict(data)
|
|
203
|
+
verdict = parsed.verdict
|
|
204
|
+
reason = parsed.reason
|
|
205
|
+
policy_id = parsed.policy_id
|
|
206
|
+
risk_score = parsed.risk_score
|
|
207
|
+
|
|
208
|
+
# A BLOCK carrying a valid retry plan restarts the workflow run
|
|
209
|
+
# instead of merely failing this activity — check for it before
|
|
210
|
+
# the plain BLOCK/HALT stop handling below ever sees the verdict.
|
|
211
|
+
from .errors import GOVERNANCE_RETRYABLE_BLOCK_ERROR_TYPE
|
|
212
|
+
from .retryable_block import retryable_block_request
|
|
213
|
+
|
|
214
|
+
retry_req = retryable_block_request(parsed, event_type=event_type)
|
|
215
|
+
if retry_req is not None:
|
|
216
|
+
raise ApplicationError(
|
|
217
|
+
"Governance requested workflow restart",
|
|
218
|
+
retry_req.to_dict(),
|
|
219
|
+
type=GOVERNANCE_RETRYABLE_BLOCK_ERROR_TYPE,
|
|
220
|
+
non_retryable=True,
|
|
221
|
+
)
|
|
210
222
|
|
|
211
223
|
if verdict.should_stop():
|
|
212
224
|
result = await _handle_stop_verdict(
|
|
@@ -11,12 +11,11 @@ IMPORTANT: Activities CAN use datetime/time and make HTTP calls directly.
|
|
|
11
11
|
This is different from workflow interceptors which must maintain determinism.
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
|
-
from typing import Optional, Any, List
|
|
15
14
|
import dataclasses
|
|
16
|
-
from dataclasses import asdict, is_dataclass, fields
|
|
17
|
-
import time
|
|
18
15
|
import json
|
|
19
|
-
|
|
16
|
+
import time
|
|
17
|
+
from dataclasses import asdict, fields, is_dataclass
|
|
18
|
+
from typing import Any, List, NoReturn, Optional
|
|
20
19
|
|
|
21
20
|
from .types import rfc3339_now as _rfc3339_now
|
|
22
21
|
|
|
@@ -63,27 +62,44 @@ def _update_list_items(current_list: list, new_list: list, _logger=None) -> None
|
|
|
63
62
|
current_list[i] = new_item
|
|
64
63
|
|
|
65
64
|
|
|
65
|
+
from opentelemetry import trace
|
|
66
66
|
from temporalio import activity
|
|
67
67
|
from temporalio.worker import (
|
|
68
|
-
Interceptor,
|
|
69
68
|
ActivityInboundInterceptor,
|
|
70
69
|
ExecuteActivityInput,
|
|
70
|
+
Interceptor,
|
|
71
71
|
)
|
|
72
|
-
from opentelemetry import trace
|
|
73
72
|
|
|
73
|
+
from .activities import _terminate_workflow_for_halt
|
|
74
|
+
from .client import GovernanceClient
|
|
74
75
|
from .config import GovernanceConfig
|
|
75
76
|
from .core_adapter import core_activity_scope, get_core_context_store
|
|
76
|
-
from .
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
77
|
+
from .errors import (
|
|
78
|
+
GOVERNANCE_RETRYABLE_BLOCK_ERROR_TYPE,
|
|
79
|
+
GovernanceBlockedError,
|
|
80
|
+
GovernanceHaltError,
|
|
81
|
+
GuardrailsValidationError,
|
|
81
82
|
)
|
|
83
|
+
from .governance_state import TemporalGovernanceState
|
|
82
84
|
from .multi_agent import read_session_from_header
|
|
83
|
-
from .
|
|
85
|
+
from .retryable_block import RetryableBlockRequest, retryable_block_request
|
|
86
|
+
from .types import GovernanceVerdictResponse, Verdict, WorkflowEventType
|
|
84
87
|
from .verdict_handler import enforce_verdict
|
|
85
|
-
|
|
86
|
-
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _raise_retryable_block(req: RetryableBlockRequest) -> NoReturn:
|
|
91
|
+
"""Raise the stable, versioned ``GovernanceRetryableBlock`` ApplicationError
|
|
92
|
+
for a valid retryable-BLOCK directive. Non-retryable — the workflow
|
|
93
|
+
interceptor catches this stable type and Continue-As-News with the
|
|
94
|
+
replacement input."""
|
|
95
|
+
from temporalio.exceptions import ApplicationError
|
|
96
|
+
|
|
97
|
+
raise ApplicationError(
|
|
98
|
+
"Governance requested workflow restart",
|
|
99
|
+
req.to_dict(),
|
|
100
|
+
type=GOVERNANCE_RETRYABLE_BLOCK_ERROR_TYPE,
|
|
101
|
+
non_retryable=True,
|
|
102
|
+
)
|
|
87
103
|
|
|
88
104
|
|
|
89
105
|
def _serialize_value(value: Any) -> Any:
|
|
@@ -94,6 +110,12 @@ def _serialize_value(value: Any) -> Any:
|
|
|
94
110
|
return value
|
|
95
111
|
if isinstance(value, bytes):
|
|
96
112
|
return _serialize_bytes(value)
|
|
113
|
+
model_dump = getattr(value, "model_dump", None)
|
|
114
|
+
if callable(model_dump):
|
|
115
|
+
try:
|
|
116
|
+
return model_dump(mode="json")
|
|
117
|
+
except Exception:
|
|
118
|
+
pass
|
|
97
119
|
if is_dataclass(value) and not isinstance(value, type):
|
|
98
120
|
return asdict(value)
|
|
99
121
|
if isinstance(value, (list, tuple)):
|
|
@@ -111,6 +133,7 @@ def _serialize_bytes(value: bytes) -> str:
|
|
|
111
133
|
return value.decode("utf-8")
|
|
112
134
|
except Exception:
|
|
113
135
|
import base64
|
|
136
|
+
|
|
114
137
|
return base64.b64encode(value).decode("ascii")
|
|
115
138
|
|
|
116
139
|
|
|
@@ -238,35 +261,56 @@ class _ActivityInterceptor(ActivityInboundInterceptor):
|
|
|
238
261
|
)
|
|
239
262
|
except Exception:
|
|
240
263
|
# The activity (or a hook) raised, so _handle_completion is skipped. A
|
|
241
|
-
# completed-hook
|
|
242
|
-
#
|
|
243
|
-
#
|
|
244
|
-
#
|
|
264
|
+
# completed-hook stop recorded during user code must still be enforced:
|
|
265
|
+
# HALT reaches the terminate path and raises GovernanceHalt, and a
|
|
266
|
+
# retryable request raises GovernanceRetryableBlock — either REPLACES
|
|
267
|
+
# the original exception (completed-hook priority HALT > retryable >
|
|
268
|
+
# original exception). A plain completed BLOCK is a no-op, so the
|
|
269
|
+
# `raise` below re-propagates the original exception unchanged. Consume
|
|
270
|
+
# the completed-stop either way (also clears it, so it can never
|
|
271
|
+
# strand/leak on the exception path).
|
|
245
272
|
await self._consume_completed_halt(info)
|
|
246
273
|
raise
|
|
247
274
|
|
|
248
275
|
result = await self._handle_completion(
|
|
249
|
-
info,
|
|
250
|
-
|
|
276
|
+
info,
|
|
277
|
+
status,
|
|
278
|
+
error,
|
|
279
|
+
start_time,
|
|
280
|
+
end_time,
|
|
281
|
+
activity_input,
|
|
282
|
+
activity_output,
|
|
283
|
+
result,
|
|
251
284
|
session_id,
|
|
252
285
|
)
|
|
253
286
|
|
|
254
287
|
return result
|
|
255
288
|
|
|
256
289
|
async def _consume_completed_halt(self, info) -> None:
|
|
257
|
-
"""Exception-path safety net: a completed-hook
|
|
258
|
-
code must
|
|
259
|
-
|
|
290
|
+
"""Exception-path safety net: a completed-hook stop recorded during user
|
|
291
|
+
code must still be enforced even though the activity itself raised, so
|
|
292
|
+
_handle_completion (the success path) never runs. Priority: HALT raises
|
|
293
|
+
GovernanceHalt (terminating the workflow and REPLACING the original
|
|
294
|
+
exception); a retryable request raises GovernanceRetryableBlock (also
|
|
295
|
+
replacing the original exception with the restart request); a plain
|
|
296
|
+
completed BLOCK is a no-op here, so the caller's re-raise propagates the
|
|
297
|
+
original exception unchanged. Clears the completed-stop and the base abort
|
|
298
|
+
flag so nothing strands."""
|
|
260
299
|
stop = self._state.take_completed_stop(
|
|
261
300
|
info.workflow_id, info.workflow_run_id, info.activity_id
|
|
262
301
|
)
|
|
263
302
|
get_core_context_store().clear_activity_aborted(
|
|
264
303
|
info.workflow_id, info.activity_id
|
|
265
304
|
)
|
|
266
|
-
if stop is
|
|
305
|
+
if stop is None:
|
|
306
|
+
return
|
|
307
|
+
if stop.verdict is Verdict.HALT:
|
|
267
308
|
await _terminate_workflow_for_halt(
|
|
268
|
-
info.workflow_id, stop
|
|
309
|
+
info.workflow_id, stop.reason or "Governance halt"
|
|
269
310
|
)
|
|
311
|
+
return
|
|
312
|
+
if stop.request is not None:
|
|
313
|
+
_raise_retryable_block(stop.request)
|
|
270
314
|
|
|
271
315
|
async def _check_pending_verdicts(self, info) -> None:
|
|
272
316
|
"""Enforce a SignalReceived BLOCK/HALT recorded for this run by the
|
|
@@ -292,6 +336,7 @@ class _ActivityInterceptor(ActivityInboundInterceptor):
|
|
|
292
336
|
await _terminate_workflow_for_halt(workflow_id, reason)
|
|
293
337
|
else:
|
|
294
338
|
from temporalio.exceptions import ApplicationError
|
|
339
|
+
|
|
295
340
|
raise ApplicationError(
|
|
296
341
|
f"Governance blocked: {reason}",
|
|
297
342
|
type="GovernanceBlock",
|
|
@@ -329,9 +374,7 @@ class _ActivityInterceptor(ActivityInboundInterceptor):
|
|
|
329
374
|
info.activity_id,
|
|
330
375
|
)
|
|
331
376
|
if approved:
|
|
332
|
-
activity.logger.info(
|
|
333
|
-
f"Approval granted for workflow_id={info.workflow_id}"
|
|
334
|
-
)
|
|
377
|
+
activity.logger.info(f"Approval granted for workflow_id={info.workflow_id}")
|
|
335
378
|
self._state.clear_pending_approval(
|
|
336
379
|
info.workflow_id, info.workflow_run_id, info.activity_id
|
|
337
380
|
)
|
|
@@ -362,7 +405,21 @@ class _ActivityInterceptor(ActivityInboundInterceptor):
|
|
|
362
405
|
self, verdict_response: GovernanceVerdictResponse, info, context: str
|
|
363
406
|
) -> None:
|
|
364
407
|
"""Enforce a governance verdict (HITL, BLOCK, HALT, guardrails)."""
|
|
365
|
-
from .hitl import
|
|
408
|
+
from .hitl import raise_approval_pending, should_skip_hitl
|
|
409
|
+
|
|
410
|
+
# An exact BLOCK with a valid retry plan requests a workflow restart and
|
|
411
|
+
# takes priority over the generic BLOCK/HALT/guardrails mapping below —
|
|
412
|
+
# checked before enforce_verdict() so it is never first converted to a
|
|
413
|
+
# plain GovernanceBlockedError. These are REAL activity-lifecycle events
|
|
414
|
+
# (not hooks), so hook_trigger stays the default False.
|
|
415
|
+
event_type = (
|
|
416
|
+
WorkflowEventType.ACTIVITY_STARTED.value
|
|
417
|
+
if context == "activity_start"
|
|
418
|
+
else WorkflowEventType.ACTIVITY_COMPLETED.value
|
|
419
|
+
)
|
|
420
|
+
req = retryable_block_request(verdict_response, event_type=event_type)
|
|
421
|
+
if req is not None:
|
|
422
|
+
_raise_retryable_block(req)
|
|
366
423
|
|
|
367
424
|
try:
|
|
368
425
|
verdict_result = enforce_verdict(verdict_response, context)
|
|
@@ -384,6 +441,7 @@ class _ActivityInterceptor(ActivityInboundInterceptor):
|
|
|
384
441
|
await _terminate_workflow_for_halt(info.workflow_id, str(e))
|
|
385
442
|
except GovernanceBlockedError as e:
|
|
386
443
|
from temporalio.exceptions import ApplicationError
|
|
444
|
+
|
|
387
445
|
raise ApplicationError(
|
|
388
446
|
f"Governance blocked: {e.reason}",
|
|
389
447
|
type="GovernanceBlock",
|
|
@@ -391,6 +449,7 @@ class _ActivityInterceptor(ActivityInboundInterceptor):
|
|
|
391
449
|
)
|
|
392
450
|
except GuardrailsValidationError as e:
|
|
393
451
|
from temporalio.exceptions import ApplicationError
|
|
452
|
+
|
|
394
453
|
activity.logger.info(f"Guardrails validation failed: {e}")
|
|
395
454
|
raise ApplicationError(
|
|
396
455
|
f"Guardrails validation failed: {e}",
|
|
@@ -510,8 +569,16 @@ class _ActivityInterceptor(ActivityInboundInterceptor):
|
|
|
510
569
|
return result, status, error, activity_output, end_time
|
|
511
570
|
|
|
512
571
|
async def _handle_completion(
|
|
513
|
-
self,
|
|
514
|
-
|
|
572
|
+
self,
|
|
573
|
+
info,
|
|
574
|
+
status,
|
|
575
|
+
error,
|
|
576
|
+
start_time,
|
|
577
|
+
end_time,
|
|
578
|
+
activity_input,
|
|
579
|
+
activity_output,
|
|
580
|
+
result,
|
|
581
|
+
multi_agent_session_id=None,
|
|
515
582
|
) -> Any:
|
|
516
583
|
"""Send ActivityCompleted, enforce verdict, apply output redaction."""
|
|
517
584
|
store = get_core_context_store()
|
|
@@ -525,10 +592,16 @@ class _ActivityInterceptor(ActivityInboundInterceptor):
|
|
|
525
592
|
base_aborted = store.is_activity_aborted(info.workflow_id, info.activity_id)
|
|
526
593
|
store.clear_activity_aborted(info.workflow_id, info.activity_id)
|
|
527
594
|
|
|
528
|
-
if completed_stop is not None
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
595
|
+
if completed_stop is not None:
|
|
596
|
+
if completed_stop.verdict is Verdict.HALT:
|
|
597
|
+
await _terminate_workflow_for_halt(
|
|
598
|
+
info.workflow_id, completed_stop.reason or "Governance halt"
|
|
599
|
+
)
|
|
600
|
+
elif completed_stop.request is not None:
|
|
601
|
+
# A retryable request outranks the plain skip-completed-event
|
|
602
|
+
# behavior below — raise the restart request now that user code
|
|
603
|
+
# has already returned.
|
|
604
|
+
_raise_retryable_block(completed_stop.request)
|
|
532
605
|
|
|
533
606
|
was_aborted = base_aborted or completed_stop is not None
|
|
534
607
|
|