Repository navigation
Conversation
moetemp
force-pushed
the
moe/streams-workflow-read
branch
2 times, most recently
from
November 14, 2026 09:06
fe5c2c6 to
d33cbd8
Compare
One Local Activity reads pages until the Workflow's predicate is true, so History gets one marker for the whole read and no Record. The predicate and the Records pass through a registry in the Worker process, since a closure can't be serialized and Records must stay out of History.
History keeps only the marker of a read, so a replay reads its range from the Store again. The Worker checks the count, the last offset and a digest before the Workflow sees the Records.
The tests cover a first run, replays on a Worker and with the Replayer, and the limits of a read. The measurements show the History events and the Workflow Tasks that one read costs.
The API review asked for an idle stop, a byte cap and one reason for the end of each read. A fold keeps its value in the marker, so its replay needs no Store and calls no function of the Workflow.
Workflow code now appends and closes, so a replay skips the Store for those calls too, and a close's Store error comes from a regular Activity. Also sorted an import of the merge and added the changelog entry.
The refusal told Workflow code to read in an Activity. This branch has a read for Workflow code, so the message names it.
A read or a fold on a Worker with no Stream config failed the Workflow Task, which retried for good. They raise the same non-retryable `ApplicationError` as the other Workflow Stream calls.
The three calls share one scan, which Workflow code runs in a Local Activity and other code runs against the Store. The result goes into History, so a replay calls no Store and needs no config. A counter numbers the calls, and a result over 1 MiB fails at the await.
A fold can append each transformed Record to another Stream. The appender writes through the Activity's client there, and the call writes its Records before it returns, so History gets the result only after they are in the Store.
The tests cover each stop of each call with its last offset, replays with no Stream config, a Query after the Stream is gone, and a fold that appends to another Stream. The measurement tests print the History events of each case.
The type checkers and pydocstyle flagged an unused import, private helpers that other modules use, and module docstrings. The tests that misuse a call on purpose now say so to the checker.
The client must not import a contrib package. So the scan keeps the list of writers to flush, and the appender adds itself to it.
The WIT now declares the three calls as methods of the Stream resource. Only that file differs from the old pin, so the bridge needs no rebuild.
The Stream WIT marks take, filter and reduce with `@nexus.helper`. Only a fork of nexgen knows that tag until its `main` has it.
The client Stream and the Workflow Stream get the three calls as methods. Each body is one call of a support helper, and the result types are generated models.
The methods of the Stream are the public API, so the module functions and the hand-written result types go. A client reads its pages with `Stream.read`, so its interceptors see each one, and only Workflow code limits the size of a result, since only there it goes into History.
The take, filter and reduce calls don't chain. The pipeline compiles a chain into one of them, so in Workflow code it stays one Local Activity and one marker.
moetemp
force-pushed
the
moe/streams-workflow-read
branch
from
November 14, 2026 10:23
d33cbd8 to
116f765
Compare
take, filter and reduce to a Stream, and a pipeline on top
A large take put every body into History. With `StreamReplaySource.STORE` the marker holds only offsets and a digest, and the Worker reads the Records from the Store before a replay. A replay follows the marker, and a failed read fails the Workflow Task with `StreamReplayError`.
The tests cover History without bodies, the reads of a replay, its failures, and a change of the setting while a Workflow runs.
The docs tool read the bare enum default of the field as an alias, and failed on every link to the field. A field default is not an alias.
This branch has not been deployed
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.
What changed?
This PR adds three methods that scan a Stream,
take,filterandreduce, to the clientStreamand the WorkflowStream. It also addstemporalio.contrib.streams.pipeline, which chains steps over a Stream and runs the chain as one of these calls.It's a prototype, created for the purpose of getting your feedback on the API. Its base is #2129.
takegives every Record up to a stop.filtergives the Records that its predicate keeps.reducefolds the Records into one value. Each result haslast_offsetfor the next call, and astopthat says why the call ended.workflow_read_replay_from=StreamReplaySource.STOREon theStreamConfig, the marker of atakeor afilterhas only offsets, a count and a digest. A replay then reads the Records from the Store again, and it fails the Workflow Task withStreamReplayErrorif it can't. So the Records must stay in the Store for as long as the Workflow can replay.filterormap, runs nothing. An end, such asrecordsorcount, runs the whole chain as one call. So in Workflow code a chain is one Local Activity and one marker.It depends on two things that have no release yet.
@nexus.helper, which only the fork moetemp/nexgen has yet. Sopoe gen-protosand thecheck-protosjob build the Stream package with a commit of that fork.Why?
The Workflow
Streamof #2129 has noread. Workflow code usually doesn't need each Record. It needs a decision from the Stream, such as the next tool call or the full text of an answer. These calls give that decision with one Local Activity and one marker, and Workflow code doesn't run for each Record.It has two costs.
idle_timeoutof 1 second bounds that on a quiet Stream, and a shorttimeoutbounds it while Records flow.takeof 40 small Records is one marker of about 5.5 kB, or 383 bytes withSTORE.These points are open.
temporalio.contrib.streamsclear enough next totemporalio.contrib.workflow_streams?timeoutthe right word, or should it bemax_duration?How did you test it?
The tests are in
tests/nexus/test_stream_read_*.pyandtests/contrib/streams/test_pipeline.py. They run on the local dev server with the memory Store. The Redis tests don't call the three methods yet.Link to a test plan if any -