Repository navigation
Add opt-in typed activity values (#184 slice 1) - #247
Merged
Merged
Conversation
Parked by General's priority call (#245 first). Not yet qualified: full suite, corpus validator, CHANGELOG and docs are still to do. - activity.defn(typed=True) decodes positional arguments to their annotations; a mismatch raises TypedValueError (non-retryable). - schedule_activity(result_type=) decodes the result for single calls, parallel groups and selections; a mismatch is raised at the yield. - Dataclass and Enum arguments and activity results are sent as their neutral values (the bytes of asdict() and .value). - Interop codec fixture and an opt-in replay corpus fixture. Refs #184. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs #184. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolve two additive conflicts: keep both new workflow.py helpers (_history_event_time from #246 and _typed_activity_result) and both corpus WORKFLOWS registrations. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keep the #184 entry under Unreleased above the released 2.13.2 section. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Member
Author
|
General interim review (Security unavailable), exact head Encoding is byte-identical
Decoding
Failure delivery
Tests
Follow-ups to do before the next sdk-python release that includes this
sdk-python may merge exactly this SHA. |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Member
Author
|
Merged at General's clearance and verification of refreshed head
|
This was referenced Nov 4, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Slice 1 of #184, under the contract General confirmed in #184 (comment) and the clarifications that followed. Slice 2 is not included:
workflow.defn(typed=True), a child'sresult_type, and the client'sresult(result_type=). #184 stays open for it.What users get
@activity.defn(name=..., typed=True): positional arguments are decoded to the function's parameter annotations. An argument that doesn't match raisesTypedValueError, which subclassesNonRetryableErrorandTypeError. The activity fails without retries, and every worker path already classifies non-retryable failures withisinstance(NonRetryableError). The wrapper keeps the function's module and qualified name, so supervised callbacks still pickle it by reference.ctx.schedule_activity(..., result_type=T): the completed result is decoded before workflow code sees it.yield.yield [...]): each member is decoded to its ownresult_type, and the first mismatch is thrown at the group'syield.ctx.select(...): the winner's mismatch becomesSelectionResult.failure, so.result()raises it, and awaiting a losing handle throws it.result_typeis a non-compared field, never encoded and never written to history.Enumactivity arguments (ScheduleActivity.__post_init__) and activity results (remote and local) are sent as their neutral values. Clarification 2 required these to be the same bytes asdataclasses.asdict()with each enum's.value. This is safe because the codec refuses those values today. Other rich values (datetime, UUID, Decimal, attrs, pydantic) keep their current behavior.Enum(includingIntEnumandStrEnum),Optional[T]/T | None,list[T],dict[str, T],str,int,float(an int is accepted),bool,bytesandAny. Anything else, including ambiguous unions, non-str map keys and unresolvable forward references, raisesTypeErrorwhen the activity is declared or the call is made.TypedValueError(path, expected, detail).detailnames the JSON kind received and never the value (clarification 1). Enum errors say "the string is not a member" without echoing it.Where the code is
src/durable_workflow/_typed_values.py(new): validation, decoding,to_payload_valueand the typed wrapper.errors.py:TypedValueError. It is exported from the package and pickles with its fields.activity.py: thetyped=option.workflow.py: theresult_typefield and parameter, argument conversion, and the three delivery points (single, parallel, the two selection sites).worker.py: remote and local activity results go throughto_payload_value. For a local activity, the workflow receives the same neutral value on the first run as on replay._avro.pyandserializer.pyare not changed, so the codec guard needs no fixture that fails on the old code. The conversion happens at the SDK boundary.Evidence
codec_regressions/typed-dataclass-enum-neutral-map.json(neutral map,round_trip). It passes the official runner.test_interop_fixture_pins_the_bytes_of_asdict_and_valueasserts that encoding the typed value and theasdict()/.valuevalue both give exactly these bytes, and that they decode back to the dataclass. The binding is listed aspythononly; PHP and Rust can adopt the fixture.replay_regressions/typed-activity-results-opt-in.json(clarification 3). The history was written by an untyped workflow: neutral maps, including an extra key from a newer version and a missing field from an older one, across a single call and a parallel group.TypedActivityResultsWorkflow(withresult_type) andUntypedActivityResultsWorkflowreplay it to the identicalCompleteWorkflow. The typed workflow is registered in the corpusWORKFLOWS.tests/test_typed_values.py(38 tests):TypedValueErrorpickling;failed_non_retryablewithfailure_type=TypedValueError, without completing;result_typestaying local;Local runs at d4a7829 (lab2, Python 3.13, this worktree's source)
ruff check src tests: clean.mypy src/durable_workflow: no issues.tests, excludingtests/integration): 4836 passed, 4 skipped ate6d245d. After merging main, the typed, replay-corpus and codec-corpus tests give 134 passed.validate-regression-corpus.py --base-ref origin/main:status: pass.Follow-ups
docs/sdk-reference.mdsection is included here.The first commit is the parked WIP from the #245 priority switch. The commits after it qualify it.
Not merged or released.
🤖 Generated with Claude Code