Repository navigation
Conversation
|
Pushed The test raced the fix: the fake model streamed five deltas 300 ms apart, so the cancel had about 0.5 s to arrive with the next heartbeat, and on a slow runner the model sometimes finished first. Now the fake model sends its first delta and then waits up to 10 s, and the workflow stays open until the test has read the topic's last event instead of sleeping. The replay test's History is recorded again on Locally the test now passes five runs of five on Python 3.13 and on 3.14, and still fails on |
…t guardrail trips
On the streaming route, the model activity publishes each event to the
stream topic as the model sends it, but the framework checks parallel
input guardrails only against the events the workflow yields after the
activity returns. So when such a guardrail tripped mid-stream, topic
subscribers still got the rest of the answer and response.completed.
Now the workflow-side model cancels the streaming activity when the
guardrail trips, and does not start it when the guardrail has already
tripped. The run raises InputGuardrailTripwireTriggered as before. On
cancel, the activity publishes an `error` event with the code
`cancelled` to the topic and stops. Both are behind
workflow.patched("openai_agents.stream-tripwire-cancel"), because runs
recorded before this change have no cancel command at that point.
…timing The test relied on the cancel reaching the streaming activity before the fake model finished its 1.5 s stream. The cancel arrives with the next heartbeat, so on a slow runner the model sometimes finished first and the topic got response.completed. The fake model now sends its first delta and then waits up to 10 s, so the guardrail always trips mid-stream and the cancel has time to arrive. The workflow stays open until the test has read the topic's last event, instead of sleeping. The replay test's History is recorded again on main with the new workflow.
…sion gate Fixes add no version gates for now, so remove the `openai_agents.stream-tripwire-cancel` patch and the replay test and History that only checked it. When a parallel input guardrail trips, the Workflow now cancels the streaming model Activity, or does not start it, in every run. Breaking change: a streamed run in which a parallel input guardrail tripped before this version recorded no cancel. On replay it now cancels the model Activity, or does not schedule it, so the replay fails, with a non-determinism error or with a `Fatal error in workflow machines` error from the SDK core. Let those runs finish before you upgrade, or use Worker Versioning.
f400ce4 to
b8a943e
Compare
What was changed
On the streaming route, a parallel input guardrail that trips now stops the streaming model activity, so stream topic subscribers stop getting the answer.
_TemporalModelStub.stream_responsecancels the activity. The run raisesInputGuardrailTripwireTriggered, as before.invoke_model_activity_streamingpublishes anerrorevent (ResponseErrorEvent, codecancelled) to the topic and stops. This also happens when the cancel arrives after the model finished, while the activity is still publishing its events, stopping the stream's background flusher, or stopping its heartbeats. Then theerrorevent comes after the buffered events. Other kinds of cancel (Worker shutdown, heartbeat timeout) publish noerrorevent, because the activity's retry can still answer.auto_heartbeater. It usesheartbeating(), a context manager taken out of the decorator, inside its cancellation handling.auto_heartbeateruses the same context manager, so other activities are unchanged.RunResultStreaming._input_guardrails_taskand_triggered_input_guardrail_result.temporalio>=1.35.0,<2, and the lock moves to 1.35.0. With 1.34.0, a workflow that cancels an activity inWAIT_CANCELLATION_COMPLETEDmode and is then evicted never finishes the eviction (Eviction processed, but 2 tasks remain), and Worker shutdown hangs. 1.35.0 fixes this SDK bug, and this change makes that cancel automatic.heartbeat_timeout. It says theerrorevent comes afterresponse.completedwhen the model had already finished, and that there is noerrorevent when the activity had already returned. It also says that with the defaultTRY_CANCEL, a workflow that completes right after the trip can close before theerrorevent is published, and thatWAIT_CANCELLATION_COMPLETEDmakes the run raise only after it is published. The README's version line now says 1.35.No public name or option changes.
Breaking change: the cancel request is a new command, and not starting the activity drops one, with no version gate. We are not adding version gates to these fixes: breaking old histories is accepted with the plugins' 1.0 releases. A streamed run in which a parallel input guardrail tripped before this version recorded no cancel, so its replay now fails. When the guardrail tripped before the model call, it fails with
[TMPRL1100]. When it tripped while the model streamed, it fails with[TMPRL1100]or with the SDK core'sFatal error in workflow machines: ActivityMachine in state ScheduledActivityCancelCommandCreated says the transition is invalid during event ... ActivityTaskStarted, depending on the History. Streamed runs whose guardrails did not trip, and runs that don't stream, are not affected.Upgrade: let open streamed runs in which a parallel input guardrail tripped finish on the earlier version before you upgrade, or keep them there with Worker Versioning. Upgrade
temporalioto 1.35.0 or later.Review the combined diff against
main.Why?
See #227. The streaming activity publishes each event to the topic as the model sends it, but the workflow only yields the events after the activity returns, so the framework's per-event guardrail check never gated the topic. A subscriber got the whole answer to a rejected request, and
response.completed.Checklist
Closes [Bug] openai_agents: stream topic subscribers get the rest of the answer after a parallel input guardrail trips #227
How was this tested:
tests/test_openai_streaming.py:test_streaming_input_guardrail_trip_stops_the_topic[alone|sibling-cleaning-up]: a parallel guardrail trips after one second while the fake model holds after its first delta. In the second case, another guardrail takes 12 s to clean up. The subscriber's last event is thecancellederror, with noresponse.completed, and the model's stream was cancelled.test_streaming_input_guardrail_known_trip_skips_the_model[alone|sibling-cleaning-up]: a guardrail trips while an agent hook delays the model call. No streaming activity is scheduled.test_streaming_input_guardrail_trip_waits_for_the_error_event[streaming|publishing|stopping-the-flusher|stopping-the-heartbeats]-[cached|uncached]: withWAIT_CANCELLATION_COMPLETED, a workflow returns right after the trip, and the Worker shuts down, with or without a Workflow cache. The stream's last event is theerrorevent. Instreaming, the cancel arrives while the model holds after its first delta, so the stream has that delta and theerrorevent. Inpublishing, the model finishes at once but the stream's publish signals are held for 8 s, so the cancel arrives during the activity's final flush. Instopping-the-flusher, the background flusher's first publish hangs until it is cancelled and then takes 8 s to clean up, so the cancel arrives while the stream's context manager stops the flusher. Instopping-the-heartbeats, the heartbeat task takes 8 s to stop and keeps heartbeating meanwhile, so the cancel arrives while the activity stops its heartbeats. In those three, the stream then has two deltas,response.completedand theerrorevent.test_streaming_worker_shutdown_publishes_no_error_event: the Worker shuts down while the model streams, and the retry on a new Worker answers. The topic gets noerrorevent.68c494b(on temporalio 1.34.0), these fail: thesibling-cleaning-upcases, the Worker shutdown test, and the uncachedwaits_for_the_error_eventcase (Eviction processed, but 2 tasks remain, pytest timeout). The other new cases pass.waits_for_the_error_eventcase fails, with the same eviction hang. On 1.35.0 it passes.ecff357, bothpublishingcases fail: the stream has noerrorevent. Thestreamingcases pass.b8a943e, bothstopping-the-flushercases fail: the stream's last event isresponse.completed. Thestreamingandpublishingcases pass.fb5db8c, bothstopping-the-heartbeatscases fail: the stream's last event isresponse.completed.TRY_CANCELand a workflow that returns right after the trip had only the delta in its stream, with and without a Workflow cache. That is the caveat the README describes.make lintandmake testpass (458 tests). These ran in Docker with Python 3.13 and uv 0.12.24.mainatc664a34, the trip test failed:assert 'response.completed' not in [...]. This was run on an earlier version of the test.main(the guardrail tripped mid-stream, the activity ran to completion, no cancel) fails replay on68c494b:[TMPRL1100] Nondeterminism error: Activity machine does not handle this event: HistoryEvent(id: 28, WorkflowExecutionUpdateCompleted). That is the breaking change above, so it is not in the tests.mainatc664a34. At68c494bit exits 0:subscriber got: ['part 0 ', 'part 1 ', 'part 2 ', 'part 3 ', 'error'], the same four deltas a native consumer got.Any docs updates needed?
The README is updated here. Release notes should mention the cancel, the
errorevent on the topic, the newtemporalio>=1.35.0floor, and which runs stop replaying (see "Breaking change").Not covered:
heartbeat_timeout, the activity is not told about the cancel and keeps publishing until the model finishes.response.completed. This race is inherent; the README states it.errorevent; pause and reset were not.main.temporalio1.34.x still bundles the old code astemporalio.contrib.openai_agents. No backport here.Open questions:
RunResultStreamingfields fine withopenai-agentspinned to<0.23, or should the plugin wrap each parallel input guardrail instead to learn about the trip?temporalio1.34.x get a backport?Draft while the breaking change and these questions are open.