From 9d85d805e310e0ac0b1de8fcf4a94e3d7fa6d4ae Mon Sep 17 00:00:00 2001 From: Francesc Campoy Date: Mon, 26 Oct 2026 19:09:53 -0700 Subject: [PATCH 1/7] openai_agents: stop the streaming model activity when a parallel input 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. --- python/openai_agents/README.md | 10 + .../openai_agents/_invoke_model_activity.py | 15 +- .../openai_agents/_openai_runner.py | 28 +- .../openai_agents/_temporal_model_stub.py | 76 +- ...ming-input-guardrail-workflow-history.json | 771 ++++++++++++++++++ .../openai_agents/tests/test_openai_replay.py | 21 +- .../tests/test_openai_streaming.py | 147 ++++ 7 files changed, 1058 insertions(+), 10 deletions(-) create mode 100644 python/openai_agents/tests/histories/streaming-input-guardrail-workflow-history.json diff --git a/python/openai_agents/README.md b/python/openai_agents/README.md index 5fbb5bf5..336ac696 100644 --- a/python/openai_agents/README.md +++ b/python/openai_agents/README.md @@ -818,6 +818,16 @@ retries the same way as any other workflow_streams publisher — see the conventional `RETRY` event pattern for surfacing the transition to consumers. +Input guardrails run in parallel with the model call by default. When one +trips while the model is streaming, the plugin cancels the streaming +activity, and the run raises `InputGuardrailTripwireTriggered` as +`stream_events()` does. The cancel reaches the activity through its +heartbeat, so set `heartbeat_timeout`: the activity then stops and +publishes an `error` event with the code `cancelled` to the topic. +Subscribers can still get the events the model sent before that, as a +native `stream_events()` consumer can. When the guardrail has already +tripped, the plugin does not start the model call. + ## Feature Support This integration is presently subject to certain limitations. diff --git a/python/openai_agents/src/temporalio/openai_agents/_invoke_model_activity.py b/python/openai_agents/src/temporalio/openai_agents/_invoke_model_activity.py index 727d14e0..27c558c4 100644 --- a/python/openai_agents/src/temporalio/openai_agents/_invoke_model_activity.py +++ b/python/openai_agents/src/temporalio/openai_agents/_invoke_model_activity.py @@ -3,6 +3,7 @@ Implements mapping of OpenAI datastructures to Pydantic friendly types. """ +import asyncio import enum from collections.abc import Collection from dataclasses import dataclass @@ -44,7 +45,7 @@ APIStatusError, AsyncOpenAI, ) -from openai.types.responses import CustomToolParam +from openai.types.responses import CustomToolParam, ResponseErrorEvent from openai.types.responses.tool_param import Mcp from typing_extensions import Required, TypedDict @@ -517,5 +518,17 @@ async def invoke_model_activity_streaming( events_topic.publish(event) except APIStatusError as e: _raise_for_openai_status(e) + except asyncio.CancelledError: + # Tell subscribers the response ends here, for example because a parallel + # input guardrail tripped. Leaving the context manager flushes it. + events_topic.publish( + ResponseErrorEvent( + type="error", + code="cancelled", + message="The model call was cancelled.", + sequence_number=len(events), + ) + ) + raise return events diff --git a/python/openai_agents/src/temporalio/openai_agents/_openai_runner.py b/python/openai_agents/src/temporalio/openai_agents/_openai_runner.py index 9b034e81..4876a11f 100644 --- a/python/openai_agents/src/temporalio/openai_agents/_openai_runner.py +++ b/python/openai_agents/src/temporalio/openai_agents/_openai_runner.py @@ -40,7 +40,11 @@ capture_scope, install_model_provider, ) -from temporalio.openai_agents._temporal_model_stub import _TemporalModelStub +from temporalio.openai_agents._temporal_model_stub import ( + _current_streamed_run, + _StreamedRun, + _TemporalModelStub, +) from temporalio.openai_agents._trace_interceptor import _refuse_tracing_key from temporalio.openai_agents.sandbox._temporal_sandbox_client import ( TemporalSandboxClient, @@ -474,12 +478,22 @@ def run_streamed( # The streaming loop runs in a task created inside run_streamed, which copies # this context, so the scope only needs to cover the call itself. - with capture_scope(run_config.model_settings), _inline_sync_function_tools(): - streamed_result = self._runner.run_streamed( - starting_agent=converted_agent, - input=input, - **kwargs, - ) + streamed_run = _StreamedRun() + token = _current_streamed_run.set(streamed_run) + try: + with ( + capture_scope(run_config.model_settings), + _inline_sync_function_tools(), + ): + streamed_result = self._runner.run_streamed( + starting_agent=converted_agent, + input=input, + **kwargs, + ) + finally: + _current_streamed_run.reset(token) + # The model stub reads it to watch the run's parallel input guardrails. + streamed_run.result = streamed_result # Mirror the AgentsException -> AgentsWorkflowError rewrap done # in run() above. The streaming runner attaches the actual run diff --git a/python/openai_agents/src/temporalio/openai_agents/_temporal_model_stub.py b/python/openai_agents/src/temporalio/openai_agents/_temporal_model_stub.py index 56a36614..1057260e 100644 --- a/python/openai_agents/src/temporalio/openai_agents/_temporal_model_stub.py +++ b/python/openai_agents/src/temporalio/openai_agents/_temporal_model_stub.py @@ -1,6 +1,8 @@ from __future__ import annotations +import asyncio from collections.abc import AsyncIterator, Callable +from contextvars import ContextVar from typing import Any from agents import ( @@ -13,10 +15,13 @@ Handoff, HostedMCPTool, ImageGenerationTool, + InputGuardrailResult, + InputGuardrailTripwireTriggered, Model, ModelResponse, ModelSettings, ModelTracing, + RunResultStreaming, Tool, TResponseInputItem, WebSearchTool, @@ -32,6 +37,7 @@ from openai.types.responses.response_prompt_param import ResponsePromptParam from temporalio import workflow +from temporalio.exceptions import ActivityError from temporalio.openai_agents._errors import AgentsWorkflowError from temporalio.openai_agents._invoke_model_activity import ( ActivityModelInput, @@ -49,6 +55,32 @@ ) from temporalio.openai_agents._model_parameters import ModelActivityParameters +# Runs recorded before the streaming model activity was cancelled on an input guardrail trip +# have no cancel command at that point, so the cancel is behind this patch. +_STREAM_TRIPWIRE_CANCEL_PATCH = "openai_agents.stream-tripwire-cancel" + + +class _StreamedRun: + """Holds the ``RunResultStreaming`` of the streamed run in progress.""" + + result: RunResultStreaming | None = None + + +# Set by the runner around ``Runner.run_streamed``; the run loop task copies it. +_current_streamed_run: ContextVar[_StreamedRun | None] = ContextVar( + "temporal_openai_agents_streamed_run", default=None +) + + +def _parallel_input_guardrails() -> tuple[RunResultStreaming, asyncio.Task[Any]] | None: + """Return the streamed run in progress and its parallel input guardrails task, if any.""" + streamed_run = _current_streamed_run.get() + result = streamed_run.result if streamed_run is not None else None + task = result._input_guardrails_task if result is not None else None + if result is None or task is None: + return None + return result, task + class _TemporalModelStub(Model): # type:ignore[reportUnusedClass] """A stub that allows invoking models as Temporal activities.""" @@ -318,7 +350,22 @@ async def stream_response( "streaming_batch_interval": self.model_params.streaming_batch_interval, } - events = await workflow.execute_activity_method( + # The activity publishes each event to the topic as the model sends it, but the + # framework only checks the parallel input guardrails against the events it yields + # after the activity returns. So once one of them trips, don't start the activity, + # or cancel it: it then publishes an error event and stops. + guardrails = _parallel_input_guardrails() + if guardrails is not None: + result, task = guardrails + triggered = result._triggered_input_guardrail_result + if ( + task.done() + and triggered is not None + and workflow.patched(_STREAM_TRIPWIRE_CANCEL_PATCH) + ): + raise InputGuardrailTripwireTriggered(triggered) + + handle = workflow.start_activity_method( ModelActivity.invoke_model_activity_streaming, streaming_input, summary=summary, @@ -332,5 +379,32 @@ async def stream_response( versioning_intent=self.model_params.versioning_intent, priority=self.model_params.priority, ) + + cancelled_for: list[InputGuardrailResult] = [] + watcher: asyncio.Task[None] | None = None + if guardrails is not None and not guardrails[1].done(): + result, task = guardrails + + async def cancel_on_tripwire() -> None: + await workflow.wait([task]) + triggered = result._triggered_input_guardrail_result + if ( + triggered is not None + and not handle.done() + and workflow.patched(_STREAM_TRIPWIRE_CANCEL_PATCH) + ): + cancelled_for.append(triggered) + handle.cancel() + + watcher = asyncio.create_task(cancel_on_tripwire()) + try: + events = await handle + except ActivityError: + if cancelled_for: + raise InputGuardrailTripwireTriggered(cancelled_for[0]) from None + raise + finally: + if watcher is not None: + watcher.cancel() for event in events: yield event diff --git a/python/openai_agents/tests/histories/streaming-input-guardrail-workflow-history.json b/python/openai_agents/tests/histories/streaming-input-guardrail-workflow-history.json new file mode 100644 index 00000000..64bc1c6d --- /dev/null +++ b/python/openai_agents/tests/histories/streaming-input-guardrail-workflow-history.json @@ -0,0 +1,771 @@ +{ + "events": [ + { + "eventId": "1", + "eventTime": "2026-10-27T01:58:35.883570127Z", + "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_STARTED", + "taskId": "1048587", + "workflowExecutionStartedEventAttributes": { + "workflowType": { + "name": "StreamingInputGuardrailWorkflow" + }, + "taskQueue": { + "name": "tq", + "kind": "TASK_QUEUE_KIND_NORMAL" + }, + "input": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "IkhlbGxvIg==" + } + ] + }, + "workflowExecutionTimeout": "30s", + "workflowRunTimeout": "30s", + "workflowTaskTimeout": "10s", + "originalExecutionRunId": "01a17b14-b06b-7887-981b-e2815c6f7459", + "identity": "77@7eac8aae2e9c", + "firstExecutionRunId": "01a17b14-b06b-7887-981b-e2815c6f7459", + "attempt": 1, + "workflowExecutionExpirationTime": "2026-10-27T01:59:05.883Z", + "firstWorkflowTaskBackoff": "0s", + "header": { + "fields": { + "__openai_span": { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "eyJzcGFuSWQiOm51bGwsInRyYWNlSWQiOm51bGwsInRyYWNlTmFtZSI6IlVua25vd24gV29ya2Zsb3cifQ==" + } + } + }, + "workflowId": "streaming-input-guardrail-d26ffab7-f275-42f0-9789-24b6a3944369", + "priority": {} + } + }, + { + "eventId": "2", + "eventTime": "2026-10-27T01:58:35.883855544Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "1048588", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "tq", + "kind": "TASK_QUEUE_KIND_NORMAL" + }, + "startToCloseTimeout": "10s", + "attempt": 1 + } + }, + { + "eventId": "3", + "eventTime": "2026-10-27T01:58:35.888059836Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "1048594", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "2", + "identity": "77@7eac8aae2e9c", + "requestId": "55c1966f-10f2-4efd-b29a-724571a292fc", + "historySizeBytes": "452", + "workerVersion": { + "buildId": "671e8fc1ab3a8d78fa6965dec647cd9b" + } + } + }, + { + "eventId": "4", + "eventTime": "2026-10-27T01:58:36.022427336Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "1048599", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "2", + "startedEventId": "3", + "identity": "77@7eac8aae2e9c", + "workerVersion": { + "buildId": "671e8fc1ab3a8d78fa6965dec647cd9b" + }, + "sdkMetadata": { + "coreUsedFlags": [ + 1, + 3, + 2 + ], + "sdkName": "temporal-python", + "sdkVersion": "1.34.0" + }, + "meteringMetadata": {} + } + }, + { + "eventId": "5", + "eventTime": "2026-10-27T01:58:36.022596544Z", + "eventType": "EVENT_TYPE_TIMER_STARTED", + "taskId": "1048600", + "timerStartedEventAttributes": { + "timerId": "1", + "startToFireTimeout": "1s", + "workflowTaskCompletedEventId": "4" + } + }, + { + "eventId": "6", + "eventTime": "2026-10-27T01:58:36.022667002Z", + "eventType": "EVENT_TYPE_ACTIVITY_TASK_SCHEDULED", + "taskId": "1048601", + "activityTaskScheduledEventAttributes": { + "activityId": "1", + "activityType": { + "name": "invoke_model_activity_streaming" + }, + "taskQueue": { + "name": "tq", + "kind": "TASK_QUEUE_KIND_NORMAL" + }, + "header": { + "fields": { + "__openai_span": { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "eyJzcGFuSWQiOiJzcGFuXzZmYWQ3NDI0NTdiMDRmNjFiZmQyZmJjNCIsInRyYWNlSWQiOiJ0cmFjZV9jY2YzYjE4ZTAzNTc0M2VhYTYyN2FjMjMiLCJ0cmFjZU5hbWUiOiJBZ2VudCB3b3JrZmxvdyJ9" + } + } + }, + "input": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "eyJtb2RlbF9uYW1lIjpudWxsLCJzeXN0ZW1faW5zdHJ1Y3Rpb25zIjoiWW91IGFyZSBhIHRlc3QgYWdlbnQuIiwiaW5wdXQiOlt7ImNvbnRlbnQiOiJIZWxsbyIsInJvbGUiOiJ1c2VyIn1dLCJtb2RlbF9zZXR0aW5ncyI6eyJ0ZW1wZXJhdHVyZSI6bnVsbCwidG9wX3AiOm51bGwsImZyZXF1ZW5jeV9wZW5hbHR5IjpudWxsLCJwcmVzZW5jZV9wZW5hbHR5IjpudWxsLCJ0b29sX2Nob2ljZSI6bnVsbCwicGFyYWxsZWxfdG9vbF9jYWxscyI6bnVsbCwidHJ1bmNhdGlvbiI6bnVsbCwibWF4X3Rva2VucyI6bnVsbCwicmVhc29uaW5nIjp7ImVmZm9ydCI6Im5vbmUifSwidmVyYm9zaXR5IjoibG93IiwibWV0YWRhdGEiOm51bGwsInN0b3JlIjpudWxsLCJwcm9tcHRfY2FjaGVfcmV0ZW50aW9uIjpudWxsLCJpbmNsdWRlX3VzYWdlIjpudWxsLCJyZXNwb25zZV9pbmNsdWRlIjpudWxsLCJ0b3BfbG9ncHJvYnMiOm51bGwsImV4dHJhX3F1ZXJ5IjpudWxsLCJleHRyYV9ib2R5IjpudWxsLCJleHRyYV9oZWFkZXJzIjpudWxsLCJleHRyYV9hcmdzIjpudWxsLCJyZXRyeSI6bnVsbCwiY29udGV4dF9tYW5hZ2VtZW50IjpudWxsLCJwcm9tcHRfY2FjaGVfb3B0aW9ucyI6bnVsbCwicHJlc2VydmVfcmF3X3VzYWdlIjpudWxsLCJ0aW1lb3V0IjpudWxsfSwidG9vbHMiOltdLCJvdXRwdXRfc2NoZW1hIjpudWxsLCJoYW5kb2ZmcyI6W10sInRyYWNpbmciOjEsInByZXZpb3VzX3Jlc3BvbnNlX2lkIjpudWxsLCJjb252ZXJzYXRpb25faWQiOm51bGwsInByb21wdCI6bnVsbCwiaW1wbGljaXRfbW9kZWxfc2V0dGluZ3MiOnRydWUsInJ1bl9tb2RlbF9zZXR0aW5ncyI6bnVsbCwic3RyZWFtaW5nX3RvcGljIjoiZXZlbnRzIiwic3RyZWFtaW5nX2JhdGNoX2ludGVydmFsIjoiUFQwLjFTIn0=" + } + ] + }, + "scheduleToCloseTimeout": "30s", + "scheduleToStartTimeout": "30s", + "startToCloseTimeout": "30s", + "heartbeatTimeout": "1s", + "workflowTaskCompletedEventId": "4", + "retryPolicy": { + "initialInterval": "1s", + "backoffCoefficient": 2.0, + "maximumInterval": "100s" + }, + "useWorkflowBuildId": true, + "priority": {} + }, + "userMetadata": { + "summary": { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "IkFzc2lzdGFudCI=" + } + } + }, + { + "eventId": "7", + "eventTime": "2026-10-27T01:58:36.366446711Z", + "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_SIGNALED", + "taskId": "1048606", + "workflowExecutionSignaledEventAttributes": { + "signalName": "__temporal_workflow_stream_publish", + "input": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "eyJpdGVtcyI6W3sidG9waWMiOiJldmVudHMiLCJkYXRhIjoiQ2hZS0NHVnVZMjlrYVc1bkVncHFjMjl1TDNCc1lXbHVFbzRCZXlKamIyNTBaVzUwWDJsdVpHVjRJam93TENKa1pXeDBZU0k2SW5CaGNuUWdNQ0FpTENKcGRHVnRYMmxrSWpvaWFYUmxiVEVpTENKc2IyZHdjbTlpY3lJNlcxMHNJbTkxZEhCMWRGOXBibVJsZUNJNk1Dd2ljMlZ4ZFdWdVkyVmZiblZ0WW1WeUlqb3dMQ0owZVhCbElqb2ljbVZ6Y0c5dWMyVXViM1YwY0hWMFgzUmxlSFF1WkdWc2RHRWlmUT09In1dLCJwdWJsaXNoZXJfaWQiOiI5OGY2YjU1MmM0NjU0YzljIiwic2VxdWVuY2UiOjF9" + } + ] + }, + "identity": "77@7eac8aae2e9c", + "header": { + "fields": { + "__openai_span": { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "eyJzcGFuSWQiOiJzcGFuX2MxNzRkNzhjYzBmNjQyYzM4MDBiNjliOCIsInRyYWNlSWQiOiJ0cmFjZV9jY2YzYjE4ZTAzNTc0M2VhYTYyN2FjMjMiLCJ0cmFjZU5hbWUiOiJBZ2VudCB3b3JrZmxvdyJ9" + } + } + }, + "requestId": "64a4b259-4e18-45b6-888e-73c50dd293aa" + } + }, + { + "eventId": "8", + "eventTime": "2026-10-27T01:58:36.366773961Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "1048607", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "tq", + "kind": "TASK_QUEUE_KIND_NORMAL" + }, + "startToCloseTimeout": "10s", + "attempt": 1 + } + }, + { + "eventId": "9", + "eventTime": "2026-10-27T01:58:36.368957919Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "1048610", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "8", + "identity": "77@7eac8aae2e9c", + "requestId": "75fac5df-ffd8-4465-9d91-f6ca27fecd3a", + "historySizeBytes": "2640", + "workerVersion": { + "buildId": "671e8fc1ab3a8d78fa6965dec647cd9b" + } + } + }, + { + "eventId": "10", + "eventTime": "2026-10-27T01:58:36.477118919Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "1048614", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "8", + "startedEventId": "9", + "identity": "77@7eac8aae2e9c", + "workerVersion": { + "buildId": "671e8fc1ab3a8d78fa6965dec647cd9b" + }, + "sdkMetadata": {}, + "meteringMetadata": {} + } + }, + { + "eventId": "11", + "eventTime": "2026-10-27T01:58:36.674702044Z", + "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_SIGNALED", + "taskId": "1048617", + "workflowExecutionSignaledEventAttributes": { + "signalName": "__temporal_workflow_stream_publish", + "input": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "eyJpdGVtcyI6W3sidG9waWMiOiJldmVudHMiLCJkYXRhIjoiQ2hZS0NHVnVZMjlrYVc1bkVncHFjMjl1TDNCc1lXbHVFbzRCZXlKamIyNTBaVzUwWDJsdVpHVjRJam93TENKa1pXeDBZU0k2SW5CaGNuUWdNU0FpTENKcGRHVnRYMmxrSWpvaWFYUmxiVEVpTENKc2IyZHdjbTlpY3lJNlcxMHNJbTkxZEhCMWRGOXBibVJsZUNJNk1Dd2ljMlZ4ZFdWdVkyVmZiblZ0WW1WeUlqb3hMQ0owZVhCbElqb2ljbVZ6Y0c5dWMyVXViM1YwY0hWMFgzUmxlSFF1WkdWc2RHRWlmUT09In1dLCJwdWJsaXNoZXJfaWQiOiI5OGY2YjU1MmM0NjU0YzljIiwic2VxdWVuY2UiOjJ9" + } + ] + }, + "identity": "77@7eac8aae2e9c", + "header": { + "fields": { + "__openai_span": { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "eyJzcGFuSWQiOiJzcGFuXzk1MzdiMGVjNDIyZTRlNDBiODU1YzhlOSIsInRyYWNlSWQiOiJ0cmFjZV9jY2YzYjE4ZTAzNTc0M2VhYTYyN2FjMjMiLCJ0cmFjZU5hbWUiOiJBZ2VudCB3b3JrZmxvdyJ9" + } + } + }, + "requestId": "1028fa68-ce36-439a-9783-65cc6a9b672c" + } + }, + { + "eventId": "12", + "eventTime": "2026-10-27T01:58:36.674712753Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "1048618", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "tq", + "kind": "TASK_QUEUE_KIND_NORMAL" + }, + "startToCloseTimeout": "10s", + "attempt": 1 + } + }, + { + "eventId": "13", + "eventTime": "2026-10-27T01:58:36.676199128Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "1048621", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "12", + "identity": "77@7eac8aae2e9c", + "requestId": "b65afb18-61f3-4a55-b5ba-8b0cd551556a", + "historySizeBytes": "3521", + "workerVersion": { + "buildId": "671e8fc1ab3a8d78fa6965dec647cd9b" + } + } + }, + { + "eventId": "14", + "eventTime": "2026-10-27T01:58:36.771169919Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "1048625", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "12", + "startedEventId": "13", + "identity": "77@7eac8aae2e9c", + "workerVersion": { + "buildId": "671e8fc1ab3a8d78fa6965dec647cd9b" + }, + "sdkMetadata": {}, + "meteringMetadata": {} + } + }, + { + "eventId": "15", + "eventTime": "2026-10-27T01:58:36.980500295Z", + "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_SIGNALED", + "taskId": "1048627", + "workflowExecutionSignaledEventAttributes": { + "signalName": "__temporal_workflow_stream_publish", + "input": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "eyJpdGVtcyI6W3sidG9waWMiOiJldmVudHMiLCJkYXRhIjoiQ2hZS0NHVnVZMjlrYVc1bkVncHFjMjl1TDNCc1lXbHVFbzRCZXlKamIyNTBaVzUwWDJsdVpHVjRJam93TENKa1pXeDBZU0k2SW5CaGNuUWdNaUFpTENKcGRHVnRYMmxrSWpvaWFYUmxiVEVpTENKc2IyZHdjbTlpY3lJNlcxMHNJbTkxZEhCMWRGOXBibVJsZUNJNk1Dd2ljMlZ4ZFdWdVkyVmZiblZ0WW1WeUlqb3lMQ0owZVhCbElqb2ljbVZ6Y0c5dWMyVXViM1YwY0hWMFgzUmxlSFF1WkdWc2RHRWlmUT09In1dLCJwdWJsaXNoZXJfaWQiOiI5OGY2YjU1MmM0NjU0YzljIiwic2VxdWVuY2UiOjN9" + } + ] + }, + "identity": "77@7eac8aae2e9c", + "header": { + "fields": { + "__openai_span": { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "eyJzcGFuSWQiOiJzcGFuXzMzYjI4ODNlN2ViYTRlZTI4NTc5MDQ0NCIsInRyYWNlSWQiOiJ0cmFjZV9jY2YzYjE4ZTAzNTc0M2VhYTYyN2FjMjMiLCJ0cmFjZU5hbWUiOiJBZ2VudCB3b3JrZmxvdyJ9" + } + } + }, + "requestId": "1838277e-2de9-4154-8555-b0442ee6cbdd" + } + }, + { + "eventId": "16", + "eventTime": "2026-10-27T01:58:36.980508753Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "1048628", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "tq", + "kind": "TASK_QUEUE_KIND_NORMAL" + }, + "startToCloseTimeout": "10s", + "attempt": 1 + } + }, + { + "eventId": "17", + "eventTime": "2026-10-27T01:58:36.982470420Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "1048631", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "16", + "identity": "77@7eac8aae2e9c", + "requestId": "35712342-3e6e-42ec-abd2-0f4c53e18e8b", + "historySizeBytes": "4402", + "workerVersion": { + "buildId": "671e8fc1ab3a8d78fa6965dec647cd9b" + } + } + }, + { + "eventId": "18", + "eventTime": "2026-10-27T01:58:37.088481711Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "1048637", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "16", + "startedEventId": "17", + "identity": "77@7eac8aae2e9c", + "workerVersion": { + "buildId": "671e8fc1ab3a8d78fa6965dec647cd9b" + }, + "sdkMetadata": {}, + "meteringMetadata": {} + } + }, + { + "eventId": "19", + "eventTime": "2026-10-27T01:58:37.026199920Z", + "eventType": "EVENT_TYPE_TIMER_FIRED", + "taskId": "1048638", + "timerFiredEventAttributes": { + "timerId": "1", + "startedEventId": "5" + } + }, + { + "eventId": "20", + "eventTime": "2026-10-27T01:58:37.088528711Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "1048639", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "tq", + "kind": "TASK_QUEUE_KIND_NORMAL" + }, + "startToCloseTimeout": "10s", + "attempt": 1 + } + }, + { + "eventId": "21", + "eventTime": "2026-10-27T01:58:37.090284086Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "1048642", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "20", + "identity": "77@7eac8aae2e9c", + "requestId": "97279b2e-80ab-4168-994c-9b89936785ce", + "historySizeBytes": "4680", + "workerVersion": { + "buildId": "671e8fc1ab3a8d78fa6965dec647cd9b" + } + } + }, + { + "eventId": "22", + "eventTime": "2026-10-27T01:58:37.183566420Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "1048646", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "20", + "startedEventId": "21", + "identity": "77@7eac8aae2e9c", + "workerVersion": { + "buildId": "671e8fc1ab3a8d78fa6965dec647cd9b" + }, + "sdkMetadata": {}, + "meteringMetadata": {} + } + }, + { + "eventId": "23", + "eventTime": "2026-10-27T01:58:37.286480795Z", + "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_SIGNALED", + "taskId": "1048648", + "workflowExecutionSignaledEventAttributes": { + "signalName": "__temporal_workflow_stream_publish", + "input": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "eyJpdGVtcyI6W3sidG9waWMiOiJldmVudHMiLCJkYXRhIjoiQ2hZS0NHVnVZMjlrYVc1bkVncHFjMjl1TDNCc1lXbHVFbzRCZXlKamIyNTBaVzUwWDJsdVpHVjRJam93TENKa1pXeDBZU0k2SW5CaGNuUWdNeUFpTENKcGRHVnRYMmxrSWpvaWFYUmxiVEVpTENKc2IyZHdjbTlpY3lJNlcxMHNJbTkxZEhCMWRGOXBibVJsZUNJNk1Dd2ljMlZ4ZFdWdVkyVmZiblZ0WW1WeUlqb3pMQ0owZVhCbElqb2ljbVZ6Y0c5dWMyVXViM1YwY0hWMFgzUmxlSFF1WkdWc2RHRWlmUT09In1dLCJwdWJsaXNoZXJfaWQiOiI5OGY2YjU1MmM0NjU0YzljIiwic2VxdWVuY2UiOjR9" + } + ] + }, + "identity": "77@7eac8aae2e9c", + "header": { + "fields": { + "__openai_span": { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "eyJzcGFuSWQiOiJzcGFuXzY5ZDE4ZTA1NjFkMTQ4NGNhOWZmZjQyNyIsInRyYWNlSWQiOiJ0cmFjZV9jY2YzYjE4ZTAzNTc0M2VhYTYyN2FjMjMiLCJ0cmFjZU5hbWUiOiJBZ2VudCB3b3JrZmxvdyJ9" + } + } + }, + "requestId": "c2e27e1f-87ff-4b42-95d5-f1808960a037" + } + }, + { + "eventId": "24", + "eventTime": "2026-10-27T01:58:37.286488253Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "1048649", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "tq", + "kind": "TASK_QUEUE_KIND_NORMAL" + }, + "startToCloseTimeout": "10s", + "attempt": 1 + } + }, + { + "eventId": "25", + "eventTime": "2026-10-27T01:58:37.287534461Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "1048652", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "24", + "identity": "77@7eac8aae2e9c", + "requestId": "2daf6b09-f0d7-46df-84b2-95ada77c2f78", + "historySizeBytes": "5559", + "workerVersion": { + "buildId": "671e8fc1ab3a8d78fa6965dec647cd9b" + } + } + }, + { + "eventId": "26", + "eventTime": "2026-10-27T01:58:37.401103670Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "1048657", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "24", + "startedEventId": "25", + "identity": "77@7eac8aae2e9c", + "workerVersion": { + "buildId": "671e8fc1ab3a8d78fa6965dec647cd9b" + }, + "sdkMetadata": {}, + "meteringMetadata": {} + } + }, + { + "eventId": "27", + "eventTime": "2026-10-27T01:58:37.659355337Z", + "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_SIGNALED", + "taskId": "1048659", + "workflowExecutionSignaledEventAttributes": { + "signalName": "__temporal_workflow_stream_publish", + "input": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "eyJpdGVtcyI6W3sidG9waWMiOiJldmVudHMiLCJkYXRhIjoiQ2hZS0NHVnVZMjlrYVc1bkVncHFjMjl1TDNCc1lXbHVFbzRCZXlKamIyNTBaVzUwWDJsdVpHVjRJam93TENKa1pXeDBZU0k2SW5CaGNuUWdOQ0FpTENKcGRHVnRYMmxrSWpvaWFYUmxiVEVpTENKc2IyZHdjbTlpY3lJNlcxMHNJbTkxZEhCMWRGOXBibVJsZUNJNk1Dd2ljMlZ4ZFdWdVkyVmZiblZ0WW1WeUlqbzBMQ0owZVhCbElqb2ljbVZ6Y0c5dWMyVXViM1YwY0hWMFgzUmxlSFF1WkdWc2RHRWlmUT09In0seyJ0b3BpYyI6ImV2ZW50cyIsImRhdGEiOiJDaFlLQ0dWdVkyOWthVzVuRWdwcWMyOXVMM0JzWVdsdUV0TUZleUp5WlhOd2IyNXpaU0k2ZXlKcFpDSTZJbkpsYzNCZmRHVnpkQ0lzSW1OeVpXRjBaV1JmWVhRaU9qQXVNQ3dpWlhKeWIzSWlPbTUxYkd3c0ltbHVZMjl0Y0d4bGRHVmZaR1YwWVdsc2N5STZiblZzYkN3aWFXNXpkSEoxWTNScGIyNXpJanB1ZFd4c0xDSnRaWFJoWkdGMFlTSTZlMzBzSW0xdlpHVnNJam9pZEdWemRDSXNJbTlpYW1WamRDSTZJbkpsYzNCdmJuTmxJaXdpYjNWMGNIVjBJanBiZXlKcFpDSTZJbTF6WjE5MFpYTjBJaXdpWTI5dWRHVnVkQ0k2VzNzaVlXNXViM1JoZEdsdmJuTWlPbHRkTENKMFpYaDBJam9pU0dWc2JHOGdkMjl5YkdRaElpd2lkSGx3WlNJNkltOTFkSEIxZEY5MFpYaDBJaXdpYkc5bmNISnZZbk1pT2x0ZGZWMHNJbkp2YkdVaU9pSmhjM05wYzNSaGJuUWlMQ0p6ZEdGMGRYTWlPaUpqYjIxd2JHVjBaV1FpTENKMGVYQmxJam9pYldWemMyRm5aU0o5WFN3aWNHRnlZV3hzWld4ZmRHOXZiRjlqWVd4c2N5STZkSEoxWlN3aWRHVnRjR1Z5WVhSMWNtVWlPakV1TUN3aWRHOXZiRjlqYUc5cFkyVWlPaUpoZFhSdklpd2lkRzl2YkhNaU9sdGRMQ0owYjNCZmNDSTZNUzR3TENKemRHRjBkWE1pT2lKamIyMXdiR1YwWldRaUxDSjBaWGgwSWpwN0ltWnZjbTFoZENJNmV5SjBlWEJsSWpvaWRHVjRkQ0o5ZlN3aWRISjFibU5oZEdsdmJpSTZJbVJwYzJGaWJHVmtJaXdpZFhOaFoyVWlPbnNpYVc1d2RYUmZkRzlyWlc1eklqb3hNQ3dpYVc1d2RYUmZkRzlyWlc1elgyUmxkR0ZwYkhNaU9uc2lZMkZqYUdWZmQzSnBkR1ZmZEc5clpXNXpJam93TENKallXTm9aV1JmZEc5clpXNXpJam93ZlN3aWIzVjBjSFYwWDNSdmEyVnVjeUk2TlN3aWIzVjBjSFYwWDNSdmEyVnVjMTlrWlhSaGFXeHpJanA3SW5KbFlYTnZibWx1WjE5MGIydGxibk1pT2pCOUxDSjBiM1JoYkY5MGIydGxibk1pT2pFMWZYMHNJbk5sY1hWbGJtTmxYMjUxYldKbGNpSTZNaXdpZEhsd1pTSTZJbkpsYzNCdmJuTmxMbU52YlhCc1pYUmxaQ0o5In1dLCJwdWJsaXNoZXJfaWQiOiI5OGY2YjU1MmM0NjU0YzljIiwic2VxdWVuY2UiOjV9" + } + ] + }, + "identity": "77@7eac8aae2e9c", + "header": { + "fields": { + "__openai_span": { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "eyJzcGFuSWQiOiJzcGFuXzRjZjcxODgxMmMxODRlNjk4NzNkZjI4OCIsInRyYWNlSWQiOiJ0cmFjZV9jY2YzYjE4ZTAzNTc0M2VhYTYyN2FjMjMiLCJ0cmFjZU5hbWUiOiJBZ2VudCB3b3JrZmxvdyJ9" + } + } + }, + "requestId": "59c15d9a-9777-4ab2-b6ff-6d41147c9b30" + } + }, + { + "eventId": "28", + "eventTime": "2026-10-27T01:58:37.659397045Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "1048660", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "tq", + "kind": "TASK_QUEUE_KIND_NORMAL" + }, + "startToCloseTimeout": "10s", + "attempt": 1 + } + }, + { + "eventId": "29", + "eventTime": "2026-10-27T01:58:37.715473503Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "1048663", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "28", + "identity": "77@7eac8aae2e9c", + "requestId": "6dc5538a-219e-4857-a066-1e09af3a7514", + "historySizeBytes": "7469", + "workerVersion": { + "buildId": "671e8fc1ab3a8d78fa6965dec647cd9b" + } + } + }, + { + "eventId": "30", + "eventTime": "2026-10-27T01:58:38.474208129Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "1048668", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "28", + "startedEventId": "29", + "identity": "77@7eac8aae2e9c", + "workerVersion": { + "buildId": "671e8fc1ab3a8d78fa6965dec647cd9b" + }, + "sdkMetadata": {}, + "meteringMetadata": {} + } + }, + { + "eventId": "31", + "eventTime": "2026-10-27T01:58:36.022983294Z", + "eventType": "EVENT_TYPE_ACTIVITY_TASK_STARTED", + "taskId": "1048669", + "activityTaskStartedEventAttributes": { + "scheduledEventId": "6", + "identity": "77@7eac8aae2e9c", + "requestId": "e205acab-d828-4f30-bfab-4d403fa57eaa", + "attempt": 1, + "workerVersion": { + "buildId": "671e8fc1ab3a8d78fa6965dec647cd9b" + } + } + }, + { + "eventId": "32", + "eventTime": "2026-10-27T01:58:37.946990378Z", + "eventType": "EVENT_TYPE_ACTIVITY_TASK_COMPLETED", + "taskId": "1048670", + "activityTaskCompletedEventAttributes": { + "result": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "W3siY29udGVudF9pbmRleCI6MCwiZGVsdGEiOiJwYXJ0IDAgIiwiaXRlbV9pZCI6Iml0ZW0xIiwibG9ncHJvYnMiOltdLCJvdXRwdXRfaW5kZXgiOjAsInNlcXVlbmNlX251bWJlciI6MCwidHlwZSI6InJlc3BvbnNlLm91dHB1dF90ZXh0LmRlbHRhIn0seyJjb250ZW50X2luZGV4IjowLCJkZWx0YSI6InBhcnQgMSAiLCJpdGVtX2lkIjoiaXRlbTEiLCJsb2dwcm9icyI6W10sIm91dHB1dF9pbmRleCI6MCwic2VxdWVuY2VfbnVtYmVyIjoxLCJ0eXBlIjoicmVzcG9uc2Uub3V0cHV0X3RleHQuZGVsdGEifSx7ImNvbnRlbnRfaW5kZXgiOjAsImRlbHRhIjoicGFydCAyICIsIml0ZW1faWQiOiJpdGVtMSIsImxvZ3Byb2JzIjpbXSwib3V0cHV0X2luZGV4IjowLCJzZXF1ZW5jZV9udW1iZXIiOjIsInR5cGUiOiJyZXNwb25zZS5vdXRwdXRfdGV4dC5kZWx0YSJ9LHsiY29udGVudF9pbmRleCI6MCwiZGVsdGEiOiJwYXJ0IDMgIiwiaXRlbV9pZCI6Iml0ZW0xIiwibG9ncHJvYnMiOltdLCJvdXRwdXRfaW5kZXgiOjAsInNlcXVlbmNlX251bWJlciI6MywidHlwZSI6InJlc3BvbnNlLm91dHB1dF90ZXh0LmRlbHRhIn0seyJjb250ZW50X2luZGV4IjowLCJkZWx0YSI6InBhcnQgNCAiLCJpdGVtX2lkIjoiaXRlbTEiLCJsb2dwcm9icyI6W10sIm91dHB1dF9pbmRleCI6MCwic2VxdWVuY2VfbnVtYmVyIjo0LCJ0eXBlIjoicmVzcG9uc2Uub3V0cHV0X3RleHQuZGVsdGEifSx7InJlc3BvbnNlIjp7ImlkIjoicmVzcF90ZXN0IiwiY3JlYXRlZF9hdCI6MC4wLCJlcnJvciI6bnVsbCwiaW5jb21wbGV0ZV9kZXRhaWxzIjpudWxsLCJpbnN0cnVjdGlvbnMiOm51bGwsIm1ldGFkYXRhIjp7fSwibW9kZWwiOiJ0ZXN0Iiwib2JqZWN0IjoicmVzcG9uc2UiLCJvdXRwdXQiOlt7ImlkIjoibXNnX3Rlc3QiLCJjb250ZW50IjpbeyJhbm5vdGF0aW9ucyI6W10sInRleHQiOiJIZWxsbyB3b3JsZCEiLCJ0eXBlIjoib3V0cHV0X3RleHQiLCJsb2dwcm9icyI6W119XSwicm9sZSI6ImFzc2lzdGFudCIsInN0YXR1cyI6ImNvbXBsZXRlZCIsInR5cGUiOiJtZXNzYWdlIn1dLCJwYXJhbGxlbF90b29sX2NhbGxzIjp0cnVlLCJ0ZW1wZXJhdHVyZSI6MS4wLCJ0b29sX2Nob2ljZSI6ImF1dG8iLCJ0b29scyI6W10sInRvcF9wIjoxLjAsInN0YXR1cyI6ImNvbXBsZXRlZCIsInRleHQiOnsiZm9ybWF0Ijp7InR5cGUiOiJ0ZXh0In19LCJ0cnVuY2F0aW9uIjoiZGlzYWJsZWQiLCJ1c2FnZSI6eyJpbnB1dF90b2tlbnMiOjEwLCJpbnB1dF90b2tlbnNfZGV0YWlscyI6eyJjYWNoZV93cml0ZV90b2tlbnMiOjAsImNhY2hlZF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5zIjo1LCJvdXRwdXRfdG9rZW5zX2RldGFpbHMiOnsicmVhc29uaW5nX3Rva2VucyI6MH0sInRvdGFsX3Rva2VucyI6MTV9fSwic2VxdWVuY2VfbnVtYmVyIjoyLCJ0eXBlIjoicmVzcG9uc2UuY29tcGxldGVkIn1d" + } + ] + }, + "scheduledEventId": "6", + "startedEventId": "31", + "identity": "77@7eac8aae2e9c" + } + }, + { + "eventId": "33", + "eventTime": "2026-10-27T01:58:38.474383254Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "1048671", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "tq", + "kind": "TASK_QUEUE_KIND_NORMAL" + }, + "startToCloseTimeout": "10s", + "attempt": 1 + } + }, + { + "eventId": "34", + "eventTime": "2026-10-27T01:58:38.476993920Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "1048674", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "33", + "identity": "77@7eac8aae2e9c", + "requestId": "41a99537-e7c9-4b6b-b5e2-891f8afcde96", + "historySizeBytes": "9362", + "workerVersion": { + "buildId": "671e8fc1ab3a8d78fa6965dec647cd9b" + } + } + }, + { + "eventId": "35", + "eventTime": "2026-10-27T01:58:38.611814129Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "1048678", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "33", + "startedEventId": "34", + "identity": "77@7eac8aae2e9c", + "workerVersion": { + "buildId": "671e8fc1ab3a8d78fa6965dec647cd9b" + }, + "sdkMetadata": {}, + "meteringMetadata": {} + } + }, + { + "eventId": "36", + "eventTime": "2026-10-27T01:58:38.611845795Z", + "eventType": "EVENT_TYPE_TIMER_STARTED", + "taskId": "1048679", + "timerStartedEventAttributes": { + "timerId": "2", + "startToFireTimeout": "3s", + "workflowTaskCompletedEventId": "35" + } + }, + { + "eventId": "37", + "eventTime": "2026-10-27T01:58:55.857864345Z", + "eventType": "EVENT_TYPE_TIMER_FIRED", + "taskId": "1048685", + "timerFiredEventAttributes": { + "timerId": "2", + "startedEventId": "36" + } + }, + { + "eventId": "38", + "eventTime": "2026-10-27T01:58:55.858050762Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", + "taskId": "1048686", + "workflowTaskScheduledEventAttributes": { + "taskQueue": { + "name": "tq", + "kind": "TASK_QUEUE_KIND_NORMAL" + }, + "startToCloseTimeout": "10s", + "attempt": 1 + } + }, + { + "eventId": "39", + "eventTime": "2026-10-27T01:58:55.920337137Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", + "taskId": "1048689", + "workflowTaskStartedEventAttributes": { + "scheduledEventId": "38", + "identity": "77@7eac8aae2e9c", + "requestId": "07a05d00-de7d-4ce8-8296-74276b852011", + "historySizeBytes": "9679", + "workerVersion": { + "buildId": "671e8fc1ab3a8d78fa6965dec647cd9b" + } + } + }, + { + "eventId": "40", + "eventTime": "2026-10-27T01:58:56.450144387Z", + "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", + "taskId": "1048693", + "workflowTaskCompletedEventAttributes": { + "scheduledEventId": "38", + "startedEventId": "39", + "identity": "77@7eac8aae2e9c", + "workerVersion": { + "buildId": "671e8fc1ab3a8d78fa6965dec647cd9b" + }, + "sdkMetadata": {}, + "meteringMetadata": {} + } + }, + { + "eventId": "41", + "eventTime": "2026-10-27T01:58:56.451185762Z", + "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_COMPLETED", + "taskId": "1048694", + "workflowExecutionCompletedEventAttributes": { + "result": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "InJlZnVzZWQi" + } + ] + }, + "workflowTaskCompletedEventId": "40" + } + } + ] +} diff --git a/python/openai_agents/tests/test_openai_replay.py b/python/openai_agents/tests/test_openai_replay.py index 7ebcb6c3..7e9be419 100644 --- a/python/openai_agents/tests/test_openai_replay.py +++ b/python/openai_agents/tests/test_openai_replay.py @@ -4,7 +4,7 @@ import pytest from temporalio.client import WorkflowHistory -from temporalio.openai_agents import OpenAIAgentsPlugin +from temporalio.openai_agents import ModelActivityParameters, OpenAIAgentsPlugin from temporalio.worker import Replayer from tests.test_openai import ( AgentsAsToolsWorkflow, @@ -16,6 +16,7 @@ RunModelProviderWorkflow, ToolsWorkflow, ) +from tests.test_openai_streaming import StreamingInputGuardrailWorkflow @pytest.mark.parametrize( @@ -59,3 +60,21 @@ async def test_replay(file_name: str) -> None: for w in recorder if "was imported after initial workflow load" in str(w.message) ] + + +async def test_replay_streaming_input_guardrail_trip() -> None: + # Recorded before the streaming model activity was cancelled on a tripwire: the parallel + # input guardrail tripped mid-stream and the activity ran to completion. Streamed runs + # import modules during activations, so this case does not check sandbox imports. + history_json = ( + Path(__file__).with_name("histories") + / "streaming-input-guardrail-workflow-history.json" + ).read_text() + await Replayer( + workflows=[StreamingInputGuardrailWorkflow], + plugins=[ + OpenAIAgentsPlugin( + model_params=ModelActivityParameters(streaming_topic="events") + ) + ], + ).replay_workflow(WorkflowHistory.from_json("fake", history_json)) diff --git a/python/openai_agents/tests/test_openai_streaming.py b/python/openai_agents/tests/test_openai_streaming.py index 8996a338..16bdd37a 100644 --- a/python/openai_agents/tests/test_openai_streaming.py +++ b/python/openai_agents/tests/test_openai_streaming.py @@ -17,24 +17,29 @@ from agents import ( Agent, AgentOutputSchemaBase, + GuardrailFunctionOutput, Handoff, + InputGuardrailTripwireTriggered, Model, ModelResponse, ModelSettings, ModelTracing, RunConfig, + RunContextWrapper, Runner, Tool, ToolCallOutputItem, TResponseInputItem, Usage, function_tool, + input_guardrail, ) from agents.items import TResponseStreamEvent from agents.models import get_default_model_settings from openai.types.responses import ( Response, ResponseCompletedEvent, + ResponseErrorEvent, ResponseFunctionToolCall, ResponseOutputMessage, ResponseOutputText, @@ -528,3 +533,145 @@ async def test_streamed_run_runs_sync_function_tools(client: Client): ) # The sync tool ran in the streamed run, so the model got its result. assert outputs == [5] + + +class SlowStreamingModel(StreamingTestModel): + """Streaming test model that sends five deltas 300 ms apart, then completes. + + Records the deltas it sent and whether its stream was cancelled. + """ + + def __init__(self) -> None: + self.sent: list[str] = [] + self.cancelled = False + + async def stream_response( + self, + system_instructions: str | None, + input: str | list[TResponseInputItem], + model_settings: ModelSettings, + tools: list[Tool], + output_schema: AgentOutputSchemaBase | None, + handoffs: list[Handoff], + tracing: ModelTracing, + **kwargs: Any, + ) -> AsyncIterator[TResponseStreamEvent]: + try: + for i in range(5): + await asyncio.sleep(0.3) + self.sent.append(f"part {i} ") + yield ResponseTextDeltaEvent( + content_index=0, + delta=f"part {i} ", + item_id="item1", + output_index=0, + sequence_number=i, + type="response.output_text.delta", + logprobs=[], + ) + except asyncio.CancelledError: + self.cancelled = True + raise + async for event in super().stream_response( + system_instructions, + input, + model_settings, + tools, + output_schema, + handoffs, + tracing, + **kwargs, + ): + if event.type == "response.completed": + yield event + + +@input_guardrail +async def refuse_after_a_second( + _context: RunContextWrapper[None], + _agent: Agent, + _input: str | list[TResponseInputItem], +) -> GuardrailFunctionOutput: + """Runs in parallel with the model call (the default) and trips after one second.""" + await workflow.sleep(1) + return GuardrailFunctionOutput(output_info=None, tripwire_triggered=True) + + +@workflow.defn +class StreamingInputGuardrailWorkflow: + @workflow.init + def __init__(self, prompt: str) -> None: + self.stream = WorkflowStream() + + @workflow.run + async def run(self, prompt: str) -> str: + agent = Agent[None]( + name="Assistant", + instructions="You are a test agent.", + input_guardrails=[refuse_after_a_second], + ) + result = Runner.run_streamed(starting_agent=agent, input=prompt) + try: + async for _ in result.stream_events(): + pass + except InputGuardrailTripwireTriggered: + # Stay open so the stream topic can take the activity's last events. + await workflow.sleep(3) + return "refused" + return result.final_output + + +@pytest.mark.asyncio +async def test_streaming_input_guardrail_trip_stops_the_topic(client: Client): + """A parallel input guardrail that trips while the model is streaming cancels the + streaming activity: topic subscribers get an error event, not the rest of the answer + and ``response.completed``.""" + model = SlowStreamingModel() + async with AgentEnvironment( + model=model, + model_params=ModelActivityParameters( + start_to_close_timeout=timedelta(seconds=30), + heartbeat_timeout=timedelta(seconds=1), + streaming_topic="events", + ), + ) as env: + client = env.applied_on_client(client) + workflow_id = f"openai-streaming-guardrail-{uuid.uuid4()}" + + async with new_worker( + client, StreamingInputGuardrailWorkflow, max_cached_workflows=0 + ) as worker: + handle = await client.start_workflow( + StreamingInputGuardrailWorkflow.run, + "Hello", + id=workflow_id, + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=30), + ) + + stream = WorkflowStreamClient.create(client, workflow_id) + published: list[TResponseStreamEvent] = [] + + async def collect_events() -> None: + async for item in stream.subscribe( + ["events"], + from_offset=0, + result_type=TResponseStreamEvent, # type: ignore[arg-type,call-overload] + poll_cooldown=timedelta(milliseconds=50), + ): + published.append(item.data) + if item.data.type in ("error", "response.completed"): + break + + collect_task = asyncio.create_task(collect_events()) + result = await handle.result() + await asyncio.wait_for(collect_task, timeout=10.0) + + assert result == "refused" + published_types = [e.type for e in published] + assert "response.completed" not in published_types, published_types + last = published[-1] + assert isinstance(last, ResponseErrorEvent) + assert last.code == "cancelled" + assert model.cancelled + assert len(model.sent) < 5 From f62c838f97ae2386fabea6ca20d37e7a9645205c Mon Sep 17 00:00:00 2001 From: Francesc Campoy Date: Mon, 26 Oct 2026 20:15:08 -0700 Subject: [PATCH 2/7] openai_agents: make the streaming guardrail trip test independent of 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. --- ...ming-input-guardrail-workflow-history.json | 598 +++++++++--------- .../tests/test_openai_streaming.py | 53 +- 2 files changed, 318 insertions(+), 333 deletions(-) diff --git a/python/openai_agents/tests/histories/streaming-input-guardrail-workflow-history.json b/python/openai_agents/tests/histories/streaming-input-guardrail-workflow-history.json index 64bc1c6d..06554ca1 100644 --- a/python/openai_agents/tests/histories/streaming-input-guardrail-workflow-history.json +++ b/python/openai_agents/tests/histories/streaming-input-guardrail-workflow-history.json @@ -2,7 +2,7 @@ "events": [ { "eventId": "1", - "eventTime": "2026-10-27T01:58:35.883570127Z", + "eventTime": "2026-10-27T03:10:47.376720215Z", "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_STARTED", "taskId": "1048587", "workflowExecutionStartedEventAttributes": { @@ -26,11 +26,11 @@ "workflowExecutionTimeout": "30s", "workflowRunTimeout": "30s", "workflowTaskTimeout": "10s", - "originalExecutionRunId": "01a17b14-b06b-7887-981b-e2815c6f7459", - "identity": "77@7eac8aae2e9c", - "firstExecutionRunId": "01a17b14-b06b-7887-981b-e2815c6f7459", + "originalExecutionRunId": "01a17b56-c850-7afb-a53d-ed9305a21ffa", + "identity": "12@cb562f37fc4d", + "firstExecutionRunId": "01a17b56-c850-7afb-a53d-ed9305a21ffa", "attempt": 1, - "workflowExecutionExpirationTime": "2026-10-27T01:59:05.883Z", + "workflowExecutionExpirationTime": "2026-10-27T03:11:17.376Z", "firstWorkflowTaskBackoff": "0s", "header": { "fields": { @@ -42,13 +42,13 @@ } } }, - "workflowId": "streaming-input-guardrail-d26ffab7-f275-42f0-9789-24b6a3944369", + "workflowId": "streaming-input-guardrail-5c5ba570-d9dd-4a3d-9d12-455452a239c2", "priority": {} } }, { "eventId": "2", - "eventTime": "2026-10-27T01:58:35.883855544Z", + "eventTime": "2026-10-27T03:10:47.376779673Z", "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", "taskId": "1048588", "workflowTaskScheduledEventAttributes": { @@ -62,35 +62,35 @@ }, { "eventId": "3", - "eventTime": "2026-10-27T01:58:35.888059836Z", + "eventTime": "2026-10-27T03:10:47.379806048Z", "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", "taskId": "1048594", "workflowTaskStartedEventAttributes": { "scheduledEventId": "2", - "identity": "77@7eac8aae2e9c", - "requestId": "55c1966f-10f2-4efd-b29a-724571a292fc", + "identity": "12@cb562f37fc4d", + "requestId": "9cb0b83d-1bc2-4c07-9616-790f20db4852", "historySizeBytes": "452", "workerVersion": { - "buildId": "671e8fc1ab3a8d78fa6965dec647cd9b" + "buildId": "33c39efbd03dad24ce42918880b097da" } } }, { "eventId": "4", - "eventTime": "2026-10-27T01:58:36.022427336Z", + "eventTime": "2026-10-27T03:10:47.450209215Z", "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", "taskId": "1048599", "workflowTaskCompletedEventAttributes": { "scheduledEventId": "2", "startedEventId": "3", - "identity": "77@7eac8aae2e9c", + "identity": "12@cb562f37fc4d", "workerVersion": { - "buildId": "671e8fc1ab3a8d78fa6965dec647cd9b" + "buildId": "33c39efbd03dad24ce42918880b097da" }, "sdkMetadata": { "coreUsedFlags": [ - 1, 3, + 1, 2 ], "sdkName": "temporal-python", @@ -101,7 +101,7 @@ }, { "eventId": "5", - "eventTime": "2026-10-27T01:58:36.022596544Z", + "eventTime": "2026-10-27T03:10:47.450288465Z", "eventType": "EVENT_TYPE_TIMER_STARTED", "taskId": "1048600", "timerStartedEventAttributes": { @@ -112,7 +112,7 @@ }, { "eventId": "6", - "eventTime": "2026-10-27T01:58:36.022667002Z", + "eventTime": "2026-10-27T03:10:47.450329007Z", "eventType": "EVENT_TYPE_ACTIVITY_TASK_SCHEDULED", "taskId": "1048601", "activityTaskScheduledEventAttributes": { @@ -130,7 +130,7 @@ "metadata": { "encoding": "anNvbi9wbGFpbg==" }, - "data": "eyJzcGFuSWQiOiJzcGFuXzZmYWQ3NDI0NTdiMDRmNjFiZmQyZmJjNCIsInRyYWNlSWQiOiJ0cmFjZV9jY2YzYjE4ZTAzNTc0M2VhYTYyN2FjMjMiLCJ0cmFjZU5hbWUiOiJBZ2VudCB3b3JrZmxvdyJ9" + "data": "eyJzcGFuSWQiOiJzcGFuX2U0Mzk0ZTdlZTQwZTQ4YTY4ZmQ3Zjk5YiIsInRyYWNlSWQiOiJ0cmFjZV9hZjQyYzY1MDYwYzI0MDZlOGUyMWNhYTMiLCJ0cmFjZU5hbWUiOiJBZ2VudCB3b3JrZmxvdyJ9" } } }, @@ -168,40 +168,9 @@ }, { "eventId": "7", - "eventTime": "2026-10-27T01:58:36.366446711Z", - "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_SIGNALED", - "taskId": "1048606", - "workflowExecutionSignaledEventAttributes": { - "signalName": "__temporal_workflow_stream_publish", - "input": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJpdGVtcyI6W3sidG9waWMiOiJldmVudHMiLCJkYXRhIjoiQ2hZS0NHVnVZMjlrYVc1bkVncHFjMjl1TDNCc1lXbHVFbzRCZXlKamIyNTBaVzUwWDJsdVpHVjRJam93TENKa1pXeDBZU0k2SW5CaGNuUWdNQ0FpTENKcGRHVnRYMmxrSWpvaWFYUmxiVEVpTENKc2IyZHdjbTlpY3lJNlcxMHNJbTkxZEhCMWRGOXBibVJsZUNJNk1Dd2ljMlZ4ZFdWdVkyVmZiblZ0WW1WeUlqb3dMQ0owZVhCbElqb2ljbVZ6Y0c5dWMyVXViM1YwY0hWMFgzUmxlSFF1WkdWc2RHRWlmUT09In1dLCJwdWJsaXNoZXJfaWQiOiI5OGY2YjU1MmM0NjU0YzljIiwic2VxdWVuY2UiOjF9" - } - ] - }, - "identity": "77@7eac8aae2e9c", - "header": { - "fields": { - "__openai_span": { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJzcGFuSWQiOiJzcGFuX2MxNzRkNzhjYzBmNjQyYzM4MDBiNjliOCIsInRyYWNlSWQiOiJ0cmFjZV9jY2YzYjE4ZTAzNTc0M2VhYTYyN2FjMjMiLCJ0cmFjZU5hbWUiOiJBZ2VudCB3b3JrZmxvdyJ9" - } - } - }, - "requestId": "64a4b259-4e18-45b6-888e-73c50dd293aa" - } - }, - { - "eventId": "8", - "eventTime": "2026-10-27T01:58:36.366773961Z", + "eventTime": "2026-10-27T03:10:47.450519715Z", "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", - "taskId": "1048607", + "taskId": "1048602", "workflowTaskScheduledEventAttributes": { "taskQueue": { "name": "tq", @@ -212,41 +181,81 @@ } }, { - "eventId": "9", - "eventTime": "2026-10-27T01:58:36.368957919Z", + "eventId": "8", + "eventTime": "2026-10-27T03:10:47.451701465Z", "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", - "taskId": "1048610", + "taskId": "1048608", "workflowTaskStartedEventAttributes": { - "scheduledEventId": "8", - "identity": "77@7eac8aae2e9c", - "requestId": "75fac5df-ffd8-4465-9d91-f6ca27fecd3a", - "historySizeBytes": "2640", + "scheduledEventId": "7", + "identity": "12@cb562f37fc4d", + "requestId": "f6247432-c818-4778-b82f-80c6107e374a", + "historySizeBytes": "2011", "workerVersion": { - "buildId": "671e8fc1ab3a8d78fa6965dec647cd9b" + "buildId": "33c39efbd03dad24ce42918880b097da" } } }, { - "eventId": "10", - "eventTime": "2026-10-27T01:58:36.477118919Z", + "eventId": "9", + "eventTime": "2026-10-27T03:10:47.537492382Z", "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", - "taskId": "1048614", + "taskId": "1048612", "workflowTaskCompletedEventAttributes": { - "scheduledEventId": "8", - "startedEventId": "9", - "identity": "77@7eac8aae2e9c", + "scheduledEventId": "7", + "startedEventId": "8", + "identity": "12@cb562f37fc4d", "workerVersion": { - "buildId": "671e8fc1ab3a8d78fa6965dec647cd9b" + "buildId": "33c39efbd03dad24ce42918880b097da" }, "sdkMetadata": {}, "meteringMetadata": {} } }, + { + "eventId": "10", + "eventTime": "2026-10-27T03:10:47.537597632Z", + "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_UPDATE_ACCEPTED", + "taskId": "1048613", + "workflowExecutionUpdateAcceptedEventAttributes": { + "protocolInstanceId": "252388f4-e387-4452-9745-4bed9260fbd6", + "acceptedRequestMessageId": "252388f4-e387-4452-9745-4bed9260fbd6/request", + "acceptedRequestSequencingEventId": "7", + "acceptedRequest": { + "meta": { + "updateId": "252388f4-e387-4452-9745-4bed9260fbd6", + "identity": "12@cb562f37fc4d" + }, + "input": { + "header": { + "fields": { + "__openai_span": { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "eyJzcGFuSWQiOm51bGwsInRyYWNlSWQiOm51bGwsInRyYWNlTmFtZSI6IlVua25vd24gV29ya2Zsb3cifQ==" + } + } + }, + "name": "__temporal_workflow_stream_poll", + "args": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "eyJ0b3BpY3MiOlsiZXZlbnRzIl0sImZyb21fb2Zmc2V0IjowfQ==" + } + ] + } + } + } + } + }, { "eventId": "11", - "eventTime": "2026-10-27T01:58:36.674702044Z", + "eventTime": "2026-10-27T03:10:47.578171882Z", "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_SIGNALED", - "taskId": "1048617", + "taskId": "1048615", "workflowExecutionSignaledEventAttributes": { "signalName": "__temporal_workflow_stream_publish", "input": { @@ -255,29 +264,29 @@ "metadata": { "encoding": "anNvbi9wbGFpbg==" }, - "data": "eyJpdGVtcyI6W3sidG9waWMiOiJldmVudHMiLCJkYXRhIjoiQ2hZS0NHVnVZMjlrYVc1bkVncHFjMjl1TDNCc1lXbHVFbzRCZXlKamIyNTBaVzUwWDJsdVpHVjRJam93TENKa1pXeDBZU0k2SW5CaGNuUWdNU0FpTENKcGRHVnRYMmxrSWpvaWFYUmxiVEVpTENKc2IyZHdjbTlpY3lJNlcxMHNJbTkxZEhCMWRGOXBibVJsZUNJNk1Dd2ljMlZ4ZFdWdVkyVmZiblZ0WW1WeUlqb3hMQ0owZVhCbElqb2ljbVZ6Y0c5dWMyVXViM1YwY0hWMFgzUmxlSFF1WkdWc2RHRWlmUT09In1dLCJwdWJsaXNoZXJfaWQiOiI5OGY2YjU1MmM0NjU0YzljIiwic2VxdWVuY2UiOjJ9" + "data": "eyJpdGVtcyI6W3sidG9waWMiOiJldmVudHMiLCJkYXRhIjoiQ2hZS0NHVnVZMjlrYVc1bkVncHFjMjl1TDNCc1lXbHVFbzBCZXlKamIyNTBaVzUwWDJsdVpHVjRJam93TENKa1pXeDBZU0k2SWtobGJHeHZJQ0lzSW1sMFpXMWZhV1FpT2lKcGRHVnRNU0lzSW14dlozQnliMkp6SWpwYlhTd2liM1YwY0hWMFgybHVaR1Y0SWpvd0xDSnpaWEYxWlc1alpWOXVkVzFpWlhJaU9qQXNJblI1Y0dVaU9pSnlaWE53YjI1elpTNXZkWFJ3ZFhSZmRHVjRkQzVrWld4MFlTSjkifV0sInB1Ymxpc2hlcl9pZCI6IjJmNjRkNmMzNGQwMzRlMjUiLCJzZXF1ZW5jZSI6MX0=" } ] }, - "identity": "77@7eac8aae2e9c", + "identity": "12@cb562f37fc4d", "header": { "fields": { "__openai_span": { "metadata": { "encoding": "anNvbi9wbGFpbg==" }, - "data": "eyJzcGFuSWQiOiJzcGFuXzk1MzdiMGVjNDIyZTRlNDBiODU1YzhlOSIsInRyYWNlSWQiOiJ0cmFjZV9jY2YzYjE4ZTAzNTc0M2VhYTYyN2FjMjMiLCJ0cmFjZU5hbWUiOiJBZ2VudCB3b3JrZmxvdyJ9" + "data": "eyJzcGFuSWQiOiJzcGFuX2U1NTFiMzUyNzEyZjQ0ZDZiZjNmZGY5OSIsInRyYWNlSWQiOiJ0cmFjZV9hZjQyYzY1MDYwYzI0MDZlOGUyMWNhYTMiLCJ0cmFjZU5hbWUiOiJBZ2VudCB3b3JrZmxvdyJ9" } } }, - "requestId": "1028fa68-ce36-439a-9783-65cc6a9b672c" + "requestId": "52aa98e1-ff33-45bc-b637-c4865fe2ef24" } }, { "eventId": "12", - "eventTime": "2026-10-27T01:58:36.674712753Z", + "eventTime": "2026-10-27T03:10:47.578191132Z", "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", - "taskId": "1048618", + "taskId": "1048616", "workflowTaskScheduledEventAttributes": { "taskQueue": { "name": "tq", @@ -289,30 +298,30 @@ }, { "eventId": "13", - "eventTime": "2026-10-27T01:58:36.676199128Z", + "eventTime": "2026-10-27T03:10:47.579264590Z", "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", - "taskId": "1048621", + "taskId": "1048619", "workflowTaskStartedEventAttributes": { "scheduledEventId": "12", - "identity": "77@7eac8aae2e9c", - "requestId": "b65afb18-61f3-4a55-b5ba-8b0cd551556a", - "historySizeBytes": "3521", + "identity": "12@cb562f37fc4d", + "requestId": "bd6fe8fd-f851-4336-bcb8-2e0f78777bb7", + "historySizeBytes": "3274", "workerVersion": { - "buildId": "671e8fc1ab3a8d78fa6965dec647cd9b" + "buildId": "33c39efbd03dad24ce42918880b097da" } } }, { "eventId": "14", - "eventTime": "2026-10-27T01:58:36.771169919Z", + "eventTime": "2026-10-27T03:10:47.652576882Z", "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", - "taskId": "1048625", + "taskId": "1048623", "workflowTaskCompletedEventAttributes": { "scheduledEventId": "12", "startedEventId": "13", - "identity": "77@7eac8aae2e9c", + "identity": "12@cb562f37fc4d", "workerVersion": { - "buildId": "671e8fc1ab3a8d78fa6965dec647cd9b" + "buildId": "33c39efbd03dad24ce42918880b097da" }, "sdkMetadata": {}, "meteringMetadata": {} @@ -320,40 +329,34 @@ }, { "eventId": "15", - "eventTime": "2026-10-27T01:58:36.980500295Z", - "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_SIGNALED", - "taskId": "1048627", - "workflowExecutionSignaledEventAttributes": { - "signalName": "__temporal_workflow_stream_publish", - "input": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJpdGVtcyI6W3sidG9waWMiOiJldmVudHMiLCJkYXRhIjoiQ2hZS0NHVnVZMjlrYVc1bkVncHFjMjl1TDNCc1lXbHVFbzRCZXlKamIyNTBaVzUwWDJsdVpHVjRJam93TENKa1pXeDBZU0k2SW5CaGNuUWdNaUFpTENKcGRHVnRYMmxrSWpvaWFYUmxiVEVpTENKc2IyZHdjbTlpY3lJNlcxMHNJbTkxZEhCMWRGOXBibVJsZUNJNk1Dd2ljMlZ4ZFdWdVkyVmZiblZ0WW1WeUlqb3lMQ0owZVhCbElqb2ljbVZ6Y0c5dWMyVXViM1YwY0hWMFgzUmxlSFF1WkdWc2RHRWlmUT09In1dLCJwdWJsaXNoZXJfaWQiOiI5OGY2YjU1MmM0NjU0YzljIiwic2VxdWVuY2UiOjN9" - } - ] - }, - "identity": "77@7eac8aae2e9c", - "header": { - "fields": { - "__openai_span": { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJzcGFuSWQiOiJzcGFuXzMzYjI4ODNlN2ViYTRlZTI4NTc5MDQ0NCIsInRyYWNlSWQiOiJ0cmFjZV9jY2YzYjE4ZTAzNTc0M2VhYTYyN2FjMjMiLCJ0cmFjZU5hbWUiOiJBZ2VudCB3b3JrZmxvdyJ9" - } + "eventTime": "2026-10-27T03:10:47.652678257Z", + "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_UPDATE_COMPLETED", + "taskId": "1048624", + "workflowExecutionUpdateCompletedEventAttributes": { + "meta": { + "updateId": "252388f4-e387-4452-9745-4bed9260fbd6", + "identity": "12@cb562f37fc4d" + }, + "outcome": { + "success": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "eyJpdGVtcyI6W3sidG9waWMiOiJldmVudHMiLCJkYXRhIjoiQ2hZS0NHVnVZMjlrYVc1bkVncHFjMjl1TDNCc1lXbHVFbzBCZXlKamIyNTBaVzUwWDJsdVpHVjRJam93TENKa1pXeDBZU0k2SWtobGJHeHZJQ0lzSW1sMFpXMWZhV1FpT2lKcGRHVnRNU0lzSW14dlozQnliMkp6SWpwYlhTd2liM1YwY0hWMFgybHVaR1Y0SWpvd0xDSnpaWEYxWlc1alpWOXVkVzFpWlhJaU9qQXNJblI1Y0dVaU9pSnlaWE53YjI1elpTNXZkWFJ3ZFhSZmRHVjRkQzVrWld4MFlTSjkiLCJvZmZzZXQiOjB9XSwibmV4dF9vZmZzZXQiOjEsIm1vcmVfcmVhZHkiOmZhbHNlfQ==" + } + ] } }, - "requestId": "1838277e-2de9-4154-8555-b0442ee6cbdd" + "acceptedEventId": "10" } }, { "eventId": "16", - "eventTime": "2026-10-27T01:58:36.980508753Z", + "eventTime": "2026-10-27T03:10:47.727664257Z", "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", - "taskId": "1048628", + "taskId": "1048630", "workflowTaskScheduledEventAttributes": { "taskQueue": { "name": "tq", @@ -365,30 +368,30 @@ }, { "eventId": "17", - "eventTime": "2026-10-27T01:58:36.982470420Z", + "eventTime": "2026-10-27T03:10:47.728270882Z", "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", "taskId": "1048631", "workflowTaskStartedEventAttributes": { "scheduledEventId": "16", - "identity": "77@7eac8aae2e9c", - "requestId": "35712342-3e6e-42ec-abd2-0f4c53e18e8b", - "historySizeBytes": "4402", + "identity": "12@cb562f37fc4d", + "requestId": "de2a93e2-69da-4dda-9c89-5de696cd73ca", + "historySizeBytes": "3917", "workerVersion": { - "buildId": "671e8fc1ab3a8d78fa6965dec647cd9b" + "buildId": "33c39efbd03dad24ce42918880b097da" } } }, { "eventId": "18", - "eventTime": "2026-10-27T01:58:37.088481711Z", + "eventTime": "2026-10-27T03:10:47.794331298Z", "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", - "taskId": "1048637", + "taskId": "1048632", "workflowTaskCompletedEventAttributes": { "scheduledEventId": "16", "startedEventId": "17", - "identity": "77@7eac8aae2e9c", + "identity": "12@cb562f37fc4d", "workerVersion": { - "buildId": "671e8fc1ab3a8d78fa6965dec647cd9b" + "buildId": "33c39efbd03dad24ce42918880b097da" }, "sdkMetadata": {}, "meteringMetadata": {} @@ -396,19 +399,59 @@ }, { "eventId": "19", - "eventTime": "2026-10-27T01:58:37.026199920Z", + "eventTime": "2026-10-27T03:10:47.794387840Z", + "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_UPDATE_ACCEPTED", + "taskId": "1048633", + "workflowExecutionUpdateAcceptedEventAttributes": { + "protocolInstanceId": "c1370f64-c9b6-4022-97e8-8d95040b5925", + "acceptedRequestMessageId": "c1370f64-c9b6-4022-97e8-8d95040b5925/request", + "acceptedRequestSequencingEventId": "16", + "acceptedRequest": { + "meta": { + "updateId": "c1370f64-c9b6-4022-97e8-8d95040b5925", + "identity": "12@cb562f37fc4d" + }, + "input": { + "header": { + "fields": { + "__openai_span": { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "eyJzcGFuSWQiOm51bGwsInRyYWNlSWQiOm51bGwsInRyYWNlTmFtZSI6IlVua25vd24gV29ya2Zsb3cifQ==" + } + } + }, + "name": "__temporal_workflow_stream_poll", + "args": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "eyJ0b3BpY3MiOlsiZXZlbnRzIl0sImZyb21fb2Zmc2V0IjoxfQ==" + } + ] + } + } + } + } + }, + { + "eventId": "20", + "eventTime": "2026-10-27T03:10:48.453296215Z", "eventType": "EVENT_TYPE_TIMER_FIRED", - "taskId": "1048638", + "taskId": "1048637", "timerFiredEventAttributes": { "timerId": "1", "startedEventId": "5" } }, { - "eventId": "20", - "eventTime": "2026-10-27T01:58:37.088528711Z", + "eventId": "21", + "eventTime": "2026-10-27T03:10:48.453340590Z", "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", - "taskId": "1048639", + "taskId": "1048638", "workflowTaskScheduledEventAttributes": { "taskQueue": { "name": "tq", @@ -419,41 +462,41 @@ } }, { - "eventId": "21", - "eventTime": "2026-10-27T01:58:37.090284086Z", + "eventId": "22", + "eventTime": "2026-10-27T03:10:48.454838007Z", "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", "taskId": "1048642", "workflowTaskStartedEventAttributes": { - "scheduledEventId": "20", - "identity": "77@7eac8aae2e9c", - "requestId": "97279b2e-80ab-4168-994c-9b89936785ce", - "historySizeBytes": "4680", + "scheduledEventId": "21", + "identity": "12@cb562f37fc4d", + "requestId": "c503d485-db50-44a0-b7d5-bc4fc57f756f", + "historySizeBytes": "4624", "workerVersion": { - "buildId": "671e8fc1ab3a8d78fa6965dec647cd9b" + "buildId": "33c39efbd03dad24ce42918880b097da" } } }, { - "eventId": "22", - "eventTime": "2026-10-27T01:58:37.183566420Z", + "eventId": "23", + "eventTime": "2026-10-27T03:10:48.592583590Z", "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", "taskId": "1048646", "workflowTaskCompletedEventAttributes": { - "scheduledEventId": "20", - "startedEventId": "21", - "identity": "77@7eac8aae2e9c", + "scheduledEventId": "21", + "startedEventId": "22", + "identity": "12@cb562f37fc4d", "workerVersion": { - "buildId": "671e8fc1ab3a8d78fa6965dec647cd9b" + "buildId": "33c39efbd03dad24ce42918880b097da" }, "sdkMetadata": {}, "meteringMetadata": {} } }, { - "eventId": "23", - "eventTime": "2026-10-27T01:58:37.286480795Z", + "eventId": "24", + "eventTime": "2026-10-27T03:10:57.518838428Z", "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_SIGNALED", - "taskId": "1048648", + "taskId": "1048668", "workflowExecutionSignaledEventAttributes": { "signalName": "__temporal_workflow_stream_publish", "input": { @@ -462,29 +505,29 @@ "metadata": { "encoding": "anNvbi9wbGFpbg==" }, - "data": "eyJpdGVtcyI6W3sidG9waWMiOiJldmVudHMiLCJkYXRhIjoiQ2hZS0NHVnVZMjlrYVc1bkVncHFjMjl1TDNCc1lXbHVFbzRCZXlKamIyNTBaVzUwWDJsdVpHVjRJam93TENKa1pXeDBZU0k2SW5CaGNuUWdNeUFpTENKcGRHVnRYMmxrSWpvaWFYUmxiVEVpTENKc2IyZHdjbTlpY3lJNlcxMHNJbTkxZEhCMWRGOXBibVJsZUNJNk1Dd2ljMlZ4ZFdWdVkyVmZiblZ0WW1WeUlqb3pMQ0owZVhCbElqb2ljbVZ6Y0c5dWMyVXViM1YwY0hWMFgzUmxlSFF1WkdWc2RHRWlmUT09In1dLCJwdWJsaXNoZXJfaWQiOiI5OGY2YjU1MmM0NjU0YzljIiwic2VxdWVuY2UiOjR9" + "data": "eyJpdGVtcyI6W3sidG9waWMiOiJldmVudHMiLCJkYXRhIjoiQ2hZS0NHVnVZMjlrYVc1bkVncHFjMjl1TDNCc1lXbHVFbzBCZXlKamIyNTBaVzUwWDJsdVpHVjRJam93TENKa1pXeDBZU0k2SW5kdmNteGtJU0lzSW1sMFpXMWZhV1FpT2lKcGRHVnRNU0lzSW14dlozQnliMkp6SWpwYlhTd2liM1YwY0hWMFgybHVaR1Y0SWpvd0xDSnpaWEYxWlc1alpWOXVkVzFpWlhJaU9qRXNJblI1Y0dVaU9pSnlaWE53YjI1elpTNXZkWFJ3ZFhSZmRHVjRkQzVrWld4MFlTSjkifSx7InRvcGljIjoiZXZlbnRzIiwiZGF0YSI6IkNoWUtDR1Z1WTI5a2FXNW5FZ3BxYzI5dUwzQnNZV2x1RXRNRmV5SnlaWE53YjI1elpTSTZleUpwWkNJNkluSmxjM0JmZEdWemRDSXNJbU55WldGMFpXUmZZWFFpT2pBdU1Dd2laWEp5YjNJaU9tNTFiR3dzSW1sdVkyOXRjR3hsZEdWZlpHVjBZV2xzY3lJNmJuVnNiQ3dpYVc1emRISjFZM1JwYjI1eklqcHVkV3hzTENKdFpYUmhaR0YwWVNJNmUzMHNJbTF2WkdWc0lqb2lkR1Z6ZENJc0ltOWlhbVZqZENJNkluSmxjM0J2Ym5ObElpd2liM1YwY0hWMElqcGJleUpwWkNJNkltMXpaMTkwWlhOMElpd2lZMjl1ZEdWdWRDSTZXM3NpWVc1dWIzUmhkR2x2Ym5NaU9sdGRMQ0owWlhoMElqb2lTR1ZzYkc4Z2QyOXliR1FoSWl3aWRIbHdaU0k2SW05MWRIQjFkRjkwWlhoMElpd2liRzluY0hKdlluTWlPbHRkZlYwc0luSnZiR1VpT2lKaGMzTnBjM1JoYm5RaUxDSnpkR0YwZFhNaU9pSmpiMjF3YkdWMFpXUWlMQ0owZVhCbElqb2liV1Z6YzJGblpTSjlYU3dpY0dGeVlXeHNaV3hmZEc5dmJGOWpZV3hzY3lJNmRISjFaU3dpZEdWdGNHVnlZWFIxY21VaU9qRXVNQ3dpZEc5dmJGOWphRzlwWTJVaU9pSmhkWFJ2SWl3aWRHOXZiSE1pT2x0ZExDSjBiM0JmY0NJNk1TNHdMQ0p6ZEdGMGRYTWlPaUpqYjIxd2JHVjBaV1FpTENKMFpYaDBJanA3SW1admNtMWhkQ0k2ZXlKMGVYQmxJam9pZEdWNGRDSjlmU3dpZEhKMWJtTmhkR2x2YmlJNkltUnBjMkZpYkdWa0lpd2lkWE5oWjJVaU9uc2lhVzV3ZFhSZmRHOXJaVzV6SWpveE1Dd2lhVzV3ZFhSZmRHOXJaVzV6WDJSbGRHRnBiSE1pT25zaVkyRmphR1ZmZDNKcGRHVmZkRzlyWlc1eklqb3dMQ0pqWVdOb1pXUmZkRzlyWlc1eklqb3dmU3dpYjNWMGNIVjBYM1J2YTJWdWN5STZOU3dpYjNWMGNIVjBYM1J2YTJWdWMxOWtaWFJoYVd4eklqcDdJbkpsWVhOdmJtbHVaMTkwYjJ0bGJuTWlPakI5TENKMGIzUmhiRjkwYjJ0bGJuTWlPakUxZlgwc0luTmxjWFZsYm1ObFgyNTFiV0psY2lJNk1pd2lkSGx3WlNJNkluSmxjM0J2Ym5ObExtTnZiWEJzWlhSbFpDSjkifV0sInB1Ymxpc2hlcl9pZCI6IjJmNjRkNmMzNGQwMzRlMjUiLCJzZXF1ZW5jZSI6Mn0=" } ] }, - "identity": "77@7eac8aae2e9c", + "identity": "12@cb562f37fc4d", "header": { "fields": { "__openai_span": { "metadata": { "encoding": "anNvbi9wbGFpbg==" }, - "data": "eyJzcGFuSWQiOiJzcGFuXzY5ZDE4ZTA1NjFkMTQ4NGNhOWZmZjQyNyIsInRyYWNlSWQiOiJ0cmFjZV9jY2YzYjE4ZTAzNTc0M2VhYTYyN2FjMjMiLCJ0cmFjZU5hbWUiOiJBZ2VudCB3b3JrZmxvdyJ9" + "data": "eyJzcGFuSWQiOiJzcGFuXzBmYzEzNzczMWI0NTRjMGQ5ZDBiZWFmMyIsInRyYWNlSWQiOiJ0cmFjZV9hZjQyYzY1MDYwYzI0MDZlOGUyMWNhYTMiLCJ0cmFjZU5hbWUiOiJBZ2VudCB3b3JrZmxvdyJ9" } } }, - "requestId": "c2e27e1f-87ff-4b42-95d5-f1808960a037" + "requestId": "e815873e-425e-4382-80c3-0b8ff88e123f" } }, { - "eventId": "24", - "eventTime": "2026-10-27T01:58:37.286488253Z", + "eventId": "25", + "eventTime": "2026-10-27T03:10:57.518863803Z", "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", - "taskId": "1048649", + "taskId": "1048669", "workflowTaskScheduledEventAttributes": { "taskQueue": { "name": "tq", @@ -495,132 +538,81 @@ } }, { - "eventId": "25", - "eventTime": "2026-10-27T01:58:37.287534461Z", + "eventId": "26", + "eventTime": "2026-10-27T03:10:57.520626761Z", "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", - "taskId": "1048652", + "taskId": "1048672", "workflowTaskStartedEventAttributes": { - "scheduledEventId": "24", - "identity": "77@7eac8aae2e9c", - "requestId": "2daf6b09-f0d7-46df-84b2-95ada77c2f78", - "historySizeBytes": "5559", + "scheduledEventId": "25", + "identity": "12@cb562f37fc4d", + "requestId": "7d34a8ea-65e5-4829-8be3-ba9ea7621e6b", + "historySizeBytes": "6530", "workerVersion": { - "buildId": "671e8fc1ab3a8d78fa6965dec647cd9b" + "buildId": "33c39efbd03dad24ce42918880b097da" } } }, { - "eventId": "26", - "eventTime": "2026-10-27T01:58:37.401103670Z", + "eventId": "27", + "eventTime": "2026-10-27T03:10:57.929872220Z", "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", - "taskId": "1048657", + "taskId": "1048677", "workflowTaskCompletedEventAttributes": { - "scheduledEventId": "24", - "startedEventId": "25", - "identity": "77@7eac8aae2e9c", + "scheduledEventId": "25", + "startedEventId": "26", + "identity": "12@cb562f37fc4d", "workerVersion": { - "buildId": "671e8fc1ab3a8d78fa6965dec647cd9b" + "buildId": "33c39efbd03dad24ce42918880b097da" }, "sdkMetadata": {}, "meteringMetadata": {} } }, - { - "eventId": "27", - "eventTime": "2026-10-27T01:58:37.659355337Z", - "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_SIGNALED", - "taskId": "1048659", - "workflowExecutionSignaledEventAttributes": { - "signalName": "__temporal_workflow_stream_publish", - "input": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJpdGVtcyI6W3sidG9waWMiOiJldmVudHMiLCJkYXRhIjoiQ2hZS0NHVnVZMjlrYVc1bkVncHFjMjl1TDNCc1lXbHVFbzRCZXlKamIyNTBaVzUwWDJsdVpHVjRJam93TENKa1pXeDBZU0k2SW5CaGNuUWdOQ0FpTENKcGRHVnRYMmxrSWpvaWFYUmxiVEVpTENKc2IyZHdjbTlpY3lJNlcxMHNJbTkxZEhCMWRGOXBibVJsZUNJNk1Dd2ljMlZ4ZFdWdVkyVmZiblZ0WW1WeUlqbzBMQ0owZVhCbElqb2ljbVZ6Y0c5dWMyVXViM1YwY0hWMFgzUmxlSFF1WkdWc2RHRWlmUT09In0seyJ0b3BpYyI6ImV2ZW50cyIsImRhdGEiOiJDaFlLQ0dWdVkyOWthVzVuRWdwcWMyOXVMM0JzWVdsdUV0TUZleUp5WlhOd2IyNXpaU0k2ZXlKcFpDSTZJbkpsYzNCZmRHVnpkQ0lzSW1OeVpXRjBaV1JmWVhRaU9qQXVNQ3dpWlhKeWIzSWlPbTUxYkd3c0ltbHVZMjl0Y0d4bGRHVmZaR1YwWVdsc2N5STZiblZzYkN3aWFXNXpkSEoxWTNScGIyNXpJanB1ZFd4c0xDSnRaWFJoWkdGMFlTSTZlMzBzSW0xdlpHVnNJam9pZEdWemRDSXNJbTlpYW1WamRDSTZJbkpsYzNCdmJuTmxJaXdpYjNWMGNIVjBJanBiZXlKcFpDSTZJbTF6WjE5MFpYTjBJaXdpWTI5dWRHVnVkQ0k2VzNzaVlXNXViM1JoZEdsdmJuTWlPbHRkTENKMFpYaDBJam9pU0dWc2JHOGdkMjl5YkdRaElpd2lkSGx3WlNJNkltOTFkSEIxZEY5MFpYaDBJaXdpYkc5bmNISnZZbk1pT2x0ZGZWMHNJbkp2YkdVaU9pSmhjM05wYzNSaGJuUWlMQ0p6ZEdGMGRYTWlPaUpqYjIxd2JHVjBaV1FpTENKMGVYQmxJam9pYldWemMyRm5aU0o5WFN3aWNHRnlZV3hzWld4ZmRHOXZiRjlqWVd4c2N5STZkSEoxWlN3aWRHVnRjR1Z5WVhSMWNtVWlPakV1TUN3aWRHOXZiRjlqYUc5cFkyVWlPaUpoZFhSdklpd2lkRzl2YkhNaU9sdGRMQ0owYjNCZmNDSTZNUzR3TENKemRHRjBkWE1pT2lKamIyMXdiR1YwWldRaUxDSjBaWGgwSWpwN0ltWnZjbTFoZENJNmV5SjBlWEJsSWpvaWRHVjRkQ0o5ZlN3aWRISjFibU5oZEdsdmJpSTZJbVJwYzJGaWJHVmtJaXdpZFhOaFoyVWlPbnNpYVc1d2RYUmZkRzlyWlc1eklqb3hNQ3dpYVc1d2RYUmZkRzlyWlc1elgyUmxkR0ZwYkhNaU9uc2lZMkZqYUdWZmQzSnBkR1ZmZEc5clpXNXpJam93TENKallXTm9aV1JmZEc5clpXNXpJam93ZlN3aWIzVjBjSFYwWDNSdmEyVnVjeUk2TlN3aWIzVjBjSFYwWDNSdmEyVnVjMTlrWlhSaGFXeHpJanA3SW5KbFlYTnZibWx1WjE5MGIydGxibk1pT2pCOUxDSjBiM1JoYkY5MGIydGxibk1pT2pFMWZYMHNJbk5sY1hWbGJtTmxYMjUxYldKbGNpSTZNaXdpZEhsd1pTSTZJbkpsYzNCdmJuTmxMbU52YlhCc1pYUmxaQ0o5In1dLCJwdWJsaXNoZXJfaWQiOiI5OGY2YjU1MmM0NjU0YzljIiwic2VxdWVuY2UiOjV9" - } - ] - }, - "identity": "77@7eac8aae2e9c", - "header": { - "fields": { - "__openai_span": { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJzcGFuSWQiOiJzcGFuXzRjZjcxODgxMmMxODRlNjk4NzNkZjI4OCIsInRyYWNlSWQiOiJ0cmFjZV9jY2YzYjE4ZTAzNTc0M2VhYTYyN2FjMjMiLCJ0cmFjZU5hbWUiOiJBZ2VudCB3b3JrZmxvdyJ9" - } - } - }, - "requestId": "59c15d9a-9777-4ab2-b6ff-6d41147c9b30" - } - }, { "eventId": "28", - "eventTime": "2026-10-27T01:58:37.659397045Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", - "taskId": "1048660", - "workflowTaskScheduledEventAttributes": { - "taskQueue": { - "name": "tq", - "kind": "TASK_QUEUE_KIND_NORMAL" + "eventTime": "2026-10-27T03:10:57.930031428Z", + "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_UPDATE_COMPLETED", + "taskId": "1048678", + "workflowExecutionUpdateCompletedEventAttributes": { + "meta": { + "updateId": "c1370f64-c9b6-4022-97e8-8d95040b5925", + "identity": "12@cb562f37fc4d" + }, + "outcome": { + "success": { + "payloads": [ + { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "eyJpdGVtcyI6W3sidG9waWMiOiJldmVudHMiLCJkYXRhIjoiQ2hZS0NHVnVZMjlrYVc1bkVncHFjMjl1TDNCc1lXbHVFbzBCZXlKamIyNTBaVzUwWDJsdVpHVjRJam93TENKa1pXeDBZU0k2SW5kdmNteGtJU0lzSW1sMFpXMWZhV1FpT2lKcGRHVnRNU0lzSW14dlozQnliMkp6SWpwYlhTd2liM1YwY0hWMFgybHVaR1Y0SWpvd0xDSnpaWEYxWlc1alpWOXVkVzFpWlhJaU9qRXNJblI1Y0dVaU9pSnlaWE53YjI1elpTNXZkWFJ3ZFhSZmRHVjRkQzVrWld4MFlTSjkiLCJvZmZzZXQiOjF9LHsidG9waWMiOiJldmVudHMiLCJkYXRhIjoiQ2hZS0NHVnVZMjlrYVc1bkVncHFjMjl1TDNCc1lXbHVFdE1GZXlKeVpYTndiMjV6WlNJNmV5SnBaQ0k2SW5KbGMzQmZkR1Z6ZENJc0ltTnlaV0YwWldSZllYUWlPakF1TUN3aVpYSnliM0lpT201MWJHd3NJbWx1WTI5dGNHeGxkR1ZmWkdWMFlXbHNjeUk2Ym5Wc2JDd2lhVzV6ZEhKMVkzUnBiMjV6SWpwdWRXeHNMQ0p0WlhSaFpHRjBZU0k2ZTMwc0ltMXZaR1ZzSWpvaWRHVnpkQ0lzSW05aWFtVmpkQ0k2SW5KbGMzQnZibk5sSWl3aWIzVjBjSFYwSWpwYmV5SnBaQ0k2SW0xeloxOTBaWE4wSWl3aVkyOXVkR1Z1ZENJNlczc2lZVzV1YjNSaGRHbHZibk1pT2x0ZExDSjBaWGgwSWpvaVNHVnNiRzhnZDI5eWJHUWhJaXdpZEhsd1pTSTZJbTkxZEhCMWRGOTBaWGgwSWl3aWJHOW5jSEp2WW5NaU9sdGRmVjBzSW5KdmJHVWlPaUpoYzNOcGMzUmhiblFpTENKemRHRjBkWE1pT2lKamIyMXdiR1YwWldRaUxDSjBlWEJsSWpvaWJXVnpjMkZuWlNKOVhTd2ljR0Z5WVd4c1pXeGZkRzl2YkY5allXeHNjeUk2ZEhKMVpTd2lkR1Z0Y0dWeVlYUjFjbVVpT2pFdU1Dd2lkRzl2YkY5amFHOXBZMlVpT2lKaGRYUnZJaXdpZEc5dmJITWlPbHRkTENKMGIzQmZjQ0k2TVM0d0xDSnpkR0YwZFhNaU9pSmpiMjF3YkdWMFpXUWlMQ0owWlhoMElqcDdJbVp2Y20xaGRDSTZleUowZVhCbElqb2lkR1Y0ZENKOWZTd2lkSEoxYm1OaGRHbHZiaUk2SW1ScGMyRmliR1ZrSWl3aWRYTmhaMlVpT25zaWFXNXdkWFJmZEc5clpXNXpJam94TUN3aWFXNXdkWFJmZEc5clpXNXpYMlJsZEdGcGJITWlPbnNpWTJGamFHVmZkM0pwZEdWZmRHOXJaVzV6SWpvd0xDSmpZV05vWldSZmRHOXJaVzV6SWpvd2ZTd2liM1YwY0hWMFgzUnZhMlZ1Y3lJNk5Td2liM1YwY0hWMFgzUnZhMlZ1YzE5a1pYUmhhV3h6SWpwN0luSmxZWE52Ym1sdVoxOTBiMnRsYm5NaU9qQjlMQ0owYjNSaGJGOTBiMnRsYm5NaU9qRTFmWDBzSW5ObGNYVmxibU5sWDI1MWJXSmxjaUk2TWl3aWRIbHdaU0k2SW5KbGMzQnZibk5sTG1OdmJYQnNaWFJsWkNKOSIsIm9mZnNldCI6Mn1dLCJuZXh0X29mZnNldCI6MywibW9yZV9yZWFkeSI6ZmFsc2V9" + } + ] + } }, - "startToCloseTimeout": "10s", - "attempt": 1 + "acceptedEventId": "19" } }, { "eventId": "29", - "eventTime": "2026-10-27T01:58:37.715473503Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", - "taskId": "1048663", - "workflowTaskStartedEventAttributes": { - "scheduledEventId": "28", - "identity": "77@7eac8aae2e9c", - "requestId": "6dc5538a-219e-4857-a066-1e09af3a7514", - "historySizeBytes": "7469", - "workerVersion": { - "buildId": "671e8fc1ab3a8d78fa6965dec647cd9b" - } - } - }, - { - "eventId": "30", - "eventTime": "2026-10-27T01:58:38.474208129Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", - "taskId": "1048668", - "workflowTaskCompletedEventAttributes": { - "scheduledEventId": "28", - "startedEventId": "29", - "identity": "77@7eac8aae2e9c", - "workerVersion": { - "buildId": "671e8fc1ab3a8d78fa6965dec647cd9b" - }, - "sdkMetadata": {}, - "meteringMetadata": {} - } - }, - { - "eventId": "31", - "eventTime": "2026-10-27T01:58:36.022983294Z", + "eventTime": "2026-10-27T03:10:47.450504340Z", "eventType": "EVENT_TYPE_ACTIVITY_TASK_STARTED", - "taskId": "1048669", + "taskId": "1048679", "activityTaskStartedEventAttributes": { "scheduledEventId": "6", - "identity": "77@7eac8aae2e9c", - "requestId": "e205acab-d828-4f30-bfab-4d403fa57eaa", + "identity": "12@cb562f37fc4d", + "requestId": "d8c0f9e3-efd0-429f-a47e-e46d76aabfbd", "attempt": 1, "workerVersion": { - "buildId": "671e8fc1ab3a8d78fa6965dec647cd9b" + "buildId": "33c39efbd03dad24ce42918880b097da" } } }, { - "eventId": "32", - "eventTime": "2026-10-27T01:58:37.946990378Z", + "eventId": "30", + "eventTime": "2026-10-27T03:10:57.523988928Z", "eventType": "EVENT_TYPE_ACTIVITY_TASK_COMPLETED", - "taskId": "1048670", + "taskId": "1048680", "activityTaskCompletedEventAttributes": { "result": { "payloads": [ @@ -628,20 +620,20 @@ "metadata": { "encoding": "anNvbi9wbGFpbg==" }, - "data": "W3siY29udGVudF9pbmRleCI6MCwiZGVsdGEiOiJwYXJ0IDAgIiwiaXRlbV9pZCI6Iml0ZW0xIiwibG9ncHJvYnMiOltdLCJvdXRwdXRfaW5kZXgiOjAsInNlcXVlbmNlX251bWJlciI6MCwidHlwZSI6InJlc3BvbnNlLm91dHB1dF90ZXh0LmRlbHRhIn0seyJjb250ZW50X2luZGV4IjowLCJkZWx0YSI6InBhcnQgMSAiLCJpdGVtX2lkIjoiaXRlbTEiLCJsb2dwcm9icyI6W10sIm91dHB1dF9pbmRleCI6MCwic2VxdWVuY2VfbnVtYmVyIjoxLCJ0eXBlIjoicmVzcG9uc2Uub3V0cHV0X3RleHQuZGVsdGEifSx7ImNvbnRlbnRfaW5kZXgiOjAsImRlbHRhIjoicGFydCAyICIsIml0ZW1faWQiOiJpdGVtMSIsImxvZ3Byb2JzIjpbXSwib3V0cHV0X2luZGV4IjowLCJzZXF1ZW5jZV9udW1iZXIiOjIsInR5cGUiOiJyZXNwb25zZS5vdXRwdXRfdGV4dC5kZWx0YSJ9LHsiY29udGVudF9pbmRleCI6MCwiZGVsdGEiOiJwYXJ0IDMgIiwiaXRlbV9pZCI6Iml0ZW0xIiwibG9ncHJvYnMiOltdLCJvdXRwdXRfaW5kZXgiOjAsInNlcXVlbmNlX251bWJlciI6MywidHlwZSI6InJlc3BvbnNlLm91dHB1dF90ZXh0LmRlbHRhIn0seyJjb250ZW50X2luZGV4IjowLCJkZWx0YSI6InBhcnQgNCAiLCJpdGVtX2lkIjoiaXRlbTEiLCJsb2dwcm9icyI6W10sIm91dHB1dF9pbmRleCI6MCwic2VxdWVuY2VfbnVtYmVyIjo0LCJ0eXBlIjoicmVzcG9uc2Uub3V0cHV0X3RleHQuZGVsdGEifSx7InJlc3BvbnNlIjp7ImlkIjoicmVzcF90ZXN0IiwiY3JlYXRlZF9hdCI6MC4wLCJlcnJvciI6bnVsbCwiaW5jb21wbGV0ZV9kZXRhaWxzIjpudWxsLCJpbnN0cnVjdGlvbnMiOm51bGwsIm1ldGFkYXRhIjp7fSwibW9kZWwiOiJ0ZXN0Iiwib2JqZWN0IjoicmVzcG9uc2UiLCJvdXRwdXQiOlt7ImlkIjoibXNnX3Rlc3QiLCJjb250ZW50IjpbeyJhbm5vdGF0aW9ucyI6W10sInRleHQiOiJIZWxsbyB3b3JsZCEiLCJ0eXBlIjoib3V0cHV0X3RleHQiLCJsb2dwcm9icyI6W119XSwicm9sZSI6ImFzc2lzdGFudCIsInN0YXR1cyI6ImNvbXBsZXRlZCIsInR5cGUiOiJtZXNzYWdlIn1dLCJwYXJhbGxlbF90b29sX2NhbGxzIjp0cnVlLCJ0ZW1wZXJhdHVyZSI6MS4wLCJ0b29sX2Nob2ljZSI6ImF1dG8iLCJ0b29scyI6W10sInRvcF9wIjoxLjAsInN0YXR1cyI6ImNvbXBsZXRlZCIsInRleHQiOnsiZm9ybWF0Ijp7InR5cGUiOiJ0ZXh0In19LCJ0cnVuY2F0aW9uIjoiZGlzYWJsZWQiLCJ1c2FnZSI6eyJpbnB1dF90b2tlbnMiOjEwLCJpbnB1dF90b2tlbnNfZGV0YWlscyI6eyJjYWNoZV93cml0ZV90b2tlbnMiOjAsImNhY2hlZF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5zIjo1LCJvdXRwdXRfdG9rZW5zX2RldGFpbHMiOnsicmVhc29uaW5nX3Rva2VucyI6MH0sInRvdGFsX3Rva2VucyI6MTV9fSwic2VxdWVuY2VfbnVtYmVyIjoyLCJ0eXBlIjoicmVzcG9uc2UuY29tcGxldGVkIn1d" + "data": "W3siY29udGVudF9pbmRleCI6MCwiZGVsdGEiOiJIZWxsbyAiLCJpdGVtX2lkIjoiaXRlbTEiLCJsb2dwcm9icyI6W10sIm91dHB1dF9pbmRleCI6MCwic2VxdWVuY2VfbnVtYmVyIjowLCJ0eXBlIjoicmVzcG9uc2Uub3V0cHV0X3RleHQuZGVsdGEifSx7ImNvbnRlbnRfaW5kZXgiOjAsImRlbHRhIjoid29ybGQhIiwiaXRlbV9pZCI6Iml0ZW0xIiwibG9ncHJvYnMiOltdLCJvdXRwdXRfaW5kZXgiOjAsInNlcXVlbmNlX251bWJlciI6MSwidHlwZSI6InJlc3BvbnNlLm91dHB1dF90ZXh0LmRlbHRhIn0seyJyZXNwb25zZSI6eyJpZCI6InJlc3BfdGVzdCIsImNyZWF0ZWRfYXQiOjAuMCwiZXJyb3IiOm51bGwsImluY29tcGxldGVfZGV0YWlscyI6bnVsbCwiaW5zdHJ1Y3Rpb25zIjpudWxsLCJtZXRhZGF0YSI6e30sIm1vZGVsIjoidGVzdCIsIm9iamVjdCI6InJlc3BvbnNlIiwib3V0cHV0IjpbeyJpZCI6Im1zZ190ZXN0IiwiY29udGVudCI6W3siYW5ub3RhdGlvbnMiOltdLCJ0ZXh0IjoiSGVsbG8gd29ybGQhIiwidHlwZSI6Im91dHB1dF90ZXh0IiwibG9ncHJvYnMiOltdfV0sInJvbGUiOiJhc3Npc3RhbnQiLCJzdGF0dXMiOiJjb21wbGV0ZWQiLCJ0eXBlIjoibWVzc2FnZSJ9XSwicGFyYWxsZWxfdG9vbF9jYWxscyI6dHJ1ZSwidGVtcGVyYXR1cmUiOjEuMCwidG9vbF9jaG9pY2UiOiJhdXRvIiwidG9vbHMiOltdLCJ0b3BfcCI6MS4wLCJzdGF0dXMiOiJjb21wbGV0ZWQiLCJ0ZXh0Ijp7ImZvcm1hdCI6eyJ0eXBlIjoidGV4dCJ9fSwidHJ1bmNhdGlvbiI6ImRpc2FibGVkIiwidXNhZ2UiOnsiaW5wdXRfdG9rZW5zIjoxMCwiaW5wdXRfdG9rZW5zX2RldGFpbHMiOnsiY2FjaGVfd3JpdGVfdG9rZW5zIjowLCJjYWNoZWRfdG9rZW5zIjowfSwib3V0cHV0X3Rva2VucyI6NSwib3V0cHV0X3Rva2Vuc19kZXRhaWxzIjp7InJlYXNvbmluZ190b2tlbnMiOjB9LCJ0b3RhbF90b2tlbnMiOjE1fX0sInNlcXVlbmNlX251bWJlciI6MiwidHlwZSI6InJlc3BvbnNlLmNvbXBsZXRlZCJ9XQ==" } ] }, "scheduledEventId": "6", - "startedEventId": "31", - "identity": "77@7eac8aae2e9c" + "startedEventId": "29", + "identity": "12@cb562f37fc4d" } }, { - "eventId": "33", - "eventTime": "2026-10-27T01:58:38.474383254Z", + "eventId": "31", + "eventTime": "2026-10-27T03:10:57.930080720Z", "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", - "taskId": "1048671", + "taskId": "1048681", "workflowTaskScheduledEventAttributes": { "taskQueue": { "name": "tq", @@ -652,62 +644,62 @@ } }, { - "eventId": "34", - "eventTime": "2026-10-27T01:58:38.476993920Z", + "eventId": "32", + "eventTime": "2026-10-27T03:10:57.931372637Z", "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", - "taskId": "1048674", + "taskId": "1048684", "workflowTaskStartedEventAttributes": { - "scheduledEventId": "33", - "identity": "77@7eac8aae2e9c", - "requestId": "41a99537-e7c9-4b6b-b5e2-891f8afcde96", - "historySizeBytes": "9362", + "scheduledEventId": "31", + "identity": "12@cb562f37fc4d", + "requestId": "2b62ce5a-33b5-4196-9474-ed97f6c230f4", + "historySizeBytes": "9467", "workerVersion": { - "buildId": "671e8fc1ab3a8d78fa6965dec647cd9b" + "buildId": "33c39efbd03dad24ce42918880b097da" } } }, { - "eventId": "35", - "eventTime": "2026-10-27T01:58:38.611814129Z", + "eventId": "33", + "eventTime": "2026-10-27T03:10:58.104904762Z", "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", - "taskId": "1048678", + "taskId": "1048689", "workflowTaskCompletedEventAttributes": { - "scheduledEventId": "33", - "startedEventId": "34", - "identity": "77@7eac8aae2e9c", + "scheduledEventId": "31", + "startedEventId": "32", + "identity": "12@cb562f37fc4d", "workerVersion": { - "buildId": "671e8fc1ab3a8d78fa6965dec647cd9b" + "buildId": "33c39efbd03dad24ce42918880b097da" }, "sdkMetadata": {}, "meteringMetadata": {} } }, { - "eventId": "36", - "eventTime": "2026-10-27T01:58:38.611845795Z", - "eventType": "EVENT_TYPE_TIMER_STARTED", - "taskId": "1048679", - "timerStartedEventAttributes": { - "timerId": "2", - "startToFireTimeout": "3s", - "workflowTaskCompletedEventId": "35" - } - }, - { - "eventId": "37", - "eventTime": "2026-10-27T01:58:55.857864345Z", - "eventType": "EVENT_TYPE_TIMER_FIRED", - "taskId": "1048685", - "timerFiredEventAttributes": { - "timerId": "2", - "startedEventId": "36" + "eventId": "34", + "eventTime": "2026-10-27T03:10:57.933569970Z", + "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_SIGNALED", + "taskId": "1048690", + "workflowExecutionSignaledEventAttributes": { + "signalName": "finish", + "identity": "12@cb562f37fc4d", + "header": { + "fields": { + "__openai_span": { + "metadata": { + "encoding": "anNvbi9wbGFpbg==" + }, + "data": "eyJzcGFuSWQiOm51bGwsInRyYWNlSWQiOm51bGwsInRyYWNlTmFtZSI6IlVua25vd24gV29ya2Zsb3cifQ==" + } + } + }, + "requestId": "7b193871-d24d-4d5f-8bbd-08c55d60c00e" } }, { - "eventId": "38", - "eventTime": "2026-10-27T01:58:55.858050762Z", + "eventId": "35", + "eventTime": "2026-10-27T03:10:58.104936053Z", "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", - "taskId": "1048686", + "taskId": "1048691", "workflowTaskScheduledEventAttributes": { "taskQueue": { "name": "tq", @@ -718,41 +710,41 @@ } }, { - "eventId": "39", - "eventTime": "2026-10-27T01:58:55.920337137Z", + "eventId": "36", + "eventTime": "2026-10-27T03:10:58.106685470Z", "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", - "taskId": "1048689", + "taskId": "1048694", "workflowTaskStartedEventAttributes": { - "scheduledEventId": "38", - "identity": "77@7eac8aae2e9c", - "requestId": "07a05d00-de7d-4ce8-8296-74276b852011", - "historySizeBytes": "9679", + "scheduledEventId": "35", + "identity": "12@cb562f37fc4d", + "requestId": "590bd072-16e1-4ea0-ab12-6bbbf58120f7", + "historySizeBytes": "9914", "workerVersion": { - "buildId": "671e8fc1ab3a8d78fa6965dec647cd9b" + "buildId": "33c39efbd03dad24ce42918880b097da" } } }, { - "eventId": "40", - "eventTime": "2026-10-27T01:58:56.450144387Z", + "eventId": "37", + "eventTime": "2026-10-27T03:10:58.299198845Z", "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", - "taskId": "1048693", + "taskId": "1048698", "workflowTaskCompletedEventAttributes": { - "scheduledEventId": "38", - "startedEventId": "39", - "identity": "77@7eac8aae2e9c", + "scheduledEventId": "35", + "startedEventId": "36", + "identity": "12@cb562f37fc4d", "workerVersion": { - "buildId": "671e8fc1ab3a8d78fa6965dec647cd9b" + "buildId": "33c39efbd03dad24ce42918880b097da" }, "sdkMetadata": {}, "meteringMetadata": {} } }, { - "eventId": "41", - "eventTime": "2026-10-27T01:58:56.451185762Z", + "eventId": "38", + "eventTime": "2026-10-27T03:10:58.299265720Z", "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_COMPLETED", - "taskId": "1048694", + "taskId": "1048699", "workflowExecutionCompletedEventAttributes": { "result": { "payloads": [ @@ -764,7 +756,7 @@ } ] }, - "workflowTaskCompletedEventId": "40" + "workflowTaskCompletedEventId": "37" } } ] diff --git a/python/openai_agents/tests/test_openai_streaming.py b/python/openai_agents/tests/test_openai_streaming.py index 16bdd37a..2e6292da 100644 --- a/python/openai_agents/tests/test_openai_streaming.py +++ b/python/openai_agents/tests/test_openai_streaming.py @@ -535,14 +535,13 @@ async def test_streamed_run_runs_sync_function_tools(client: Client): assert outputs == [5] -class SlowStreamingModel(StreamingTestModel): - """Streaming test model that sends five deltas 300 ms apart, then completes. +class HoldingStreamingModel(StreamingTestModel): + """Streaming test model that sends its first delta, then waits up to 10 s before the rest. - Records the deltas it sent and whether its stream was cancelled. + Records whether its stream was cancelled during the wait. """ def __init__(self) -> None: - self.sent: list[str] = [] self.cancelled = False async def stream_response( @@ -556,23 +555,7 @@ async def stream_response( tracing: ModelTracing, **kwargs: Any, ) -> AsyncIterator[TResponseStreamEvent]: - try: - for i in range(5): - await asyncio.sleep(0.3) - self.sent.append(f"part {i} ") - yield ResponseTextDeltaEvent( - content_index=0, - delta=f"part {i} ", - item_id="item1", - output_index=0, - sequence_number=i, - type="response.output_text.delta", - logprobs=[], - ) - except asyncio.CancelledError: - self.cancelled = True - raise - async for event in super().stream_response( + events = super().stream_response( system_instructions, input, model_settings, @@ -581,9 +564,15 @@ async def stream_response( handoffs, tracing, **kwargs, - ): - if event.type == "response.completed": - yield event + ) + yield await anext(events) + try: + await asyncio.sleep(10) + except asyncio.CancelledError: + self.cancelled = True + raise + async for event in events: + yield event @input_guardrail @@ -602,6 +591,11 @@ class StreamingInputGuardrailWorkflow: @workflow.init def __init__(self, prompt: str) -> None: self.stream = WorkflowStream() + self.subscriber_done = False + + @workflow.signal + def finish(self) -> None: + self.subscriber_done = True @workflow.run async def run(self, prompt: str) -> str: @@ -615,8 +609,8 @@ async def run(self, prompt: str) -> str: async for _ in result.stream_events(): pass except InputGuardrailTripwireTriggered: - # Stay open so the stream topic can take the activity's last events. - await workflow.sleep(3) + # Stay open until the subscriber has read the activity's last event. + await workflow.wait_condition(lambda: self.subscriber_done) return "refused" return result.final_output @@ -626,7 +620,7 @@ async def test_streaming_input_guardrail_trip_stops_the_topic(client: Client): """A parallel input guardrail that trips while the model is streaming cancels the streaming activity: topic subscribers get an error event, not the rest of the answer and ``response.completed``.""" - model = SlowStreamingModel() + model = HoldingStreamingModel() async with AgentEnvironment( model=model, model_params=ModelActivityParameters( @@ -663,9 +657,9 @@ async def collect_events() -> None: if item.data.type in ("error", "response.completed"): break - collect_task = asyncio.create_task(collect_events()) + await asyncio.wait_for(collect_events(), timeout=20.0) + await handle.signal(StreamingInputGuardrailWorkflow.finish) result = await handle.result() - await asyncio.wait_for(collect_task, timeout=10.0) assert result == "refused" published_types = [e.type for e in published] @@ -674,4 +668,3 @@ async def collect_events() -> None: assert isinstance(last, ResponseErrorEvent) assert last.code == "cancelled" assert model.cancelled - assert len(model.sent) < 5 From 45fd82c284b280c7081e006bec5172df13b9efb7 Mon Sep 17 00:00:00 2001 From: Francesc Campoy Date: Tue, 27 Oct 2026 16:56:46 -0700 Subject: [PATCH 3/7] Breaking: cancel the streaming model activity on a trip without a version 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. --- .../openai_agents/_temporal_model_stub.py | 16 +- ...ming-input-guardrail-workflow-history.json | 763 ------------------ .../openai_agents/tests/test_openai_replay.py | 21 +- 3 files changed, 3 insertions(+), 797 deletions(-) delete mode 100644 python/openai_agents/tests/histories/streaming-input-guardrail-workflow-history.json diff --git a/python/openai_agents/src/temporalio/openai_agents/_temporal_model_stub.py b/python/openai_agents/src/temporalio/openai_agents/_temporal_model_stub.py index 1057260e..17052764 100644 --- a/python/openai_agents/src/temporalio/openai_agents/_temporal_model_stub.py +++ b/python/openai_agents/src/temporalio/openai_agents/_temporal_model_stub.py @@ -55,10 +55,6 @@ ) from temporalio.openai_agents._model_parameters import ModelActivityParameters -# Runs recorded before the streaming model activity was cancelled on an input guardrail trip -# have no cancel command at that point, so the cancel is behind this patch. -_STREAM_TRIPWIRE_CANCEL_PATCH = "openai_agents.stream-tripwire-cancel" - class _StreamedRun: """Holds the ``RunResultStreaming`` of the streamed run in progress.""" @@ -358,11 +354,7 @@ async def stream_response( if guardrails is not None: result, task = guardrails triggered = result._triggered_input_guardrail_result - if ( - task.done() - and triggered is not None - and workflow.patched(_STREAM_TRIPWIRE_CANCEL_PATCH) - ): + if task.done() and triggered is not None: raise InputGuardrailTripwireTriggered(triggered) handle = workflow.start_activity_method( @@ -388,11 +380,7 @@ async def stream_response( async def cancel_on_tripwire() -> None: await workflow.wait([task]) triggered = result._triggered_input_guardrail_result - if ( - triggered is not None - and not handle.done() - and workflow.patched(_STREAM_TRIPWIRE_CANCEL_PATCH) - ): + if triggered is not None and not handle.done(): cancelled_for.append(triggered) handle.cancel() diff --git a/python/openai_agents/tests/histories/streaming-input-guardrail-workflow-history.json b/python/openai_agents/tests/histories/streaming-input-guardrail-workflow-history.json deleted file mode 100644 index 06554ca1..00000000 --- a/python/openai_agents/tests/histories/streaming-input-guardrail-workflow-history.json +++ /dev/null @@ -1,763 +0,0 @@ -{ - "events": [ - { - "eventId": "1", - "eventTime": "2026-10-27T03:10:47.376720215Z", - "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_STARTED", - "taskId": "1048587", - "workflowExecutionStartedEventAttributes": { - "workflowType": { - "name": "StreamingInputGuardrailWorkflow" - }, - "taskQueue": { - "name": "tq", - "kind": "TASK_QUEUE_KIND_NORMAL" - }, - "input": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "IkhlbGxvIg==" - } - ] - }, - "workflowExecutionTimeout": "30s", - "workflowRunTimeout": "30s", - "workflowTaskTimeout": "10s", - "originalExecutionRunId": "01a17b56-c850-7afb-a53d-ed9305a21ffa", - "identity": "12@cb562f37fc4d", - "firstExecutionRunId": "01a17b56-c850-7afb-a53d-ed9305a21ffa", - "attempt": 1, - "workflowExecutionExpirationTime": "2026-10-27T03:11:17.376Z", - "firstWorkflowTaskBackoff": "0s", - "header": { - "fields": { - "__openai_span": { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJzcGFuSWQiOm51bGwsInRyYWNlSWQiOm51bGwsInRyYWNlTmFtZSI6IlVua25vd24gV29ya2Zsb3cifQ==" - } - } - }, - "workflowId": "streaming-input-guardrail-5c5ba570-d9dd-4a3d-9d12-455452a239c2", - "priority": {} - } - }, - { - "eventId": "2", - "eventTime": "2026-10-27T03:10:47.376779673Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", - "taskId": "1048588", - "workflowTaskScheduledEventAttributes": { - "taskQueue": { - "name": "tq", - "kind": "TASK_QUEUE_KIND_NORMAL" - }, - "startToCloseTimeout": "10s", - "attempt": 1 - } - }, - { - "eventId": "3", - "eventTime": "2026-10-27T03:10:47.379806048Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", - "taskId": "1048594", - "workflowTaskStartedEventAttributes": { - "scheduledEventId": "2", - "identity": "12@cb562f37fc4d", - "requestId": "9cb0b83d-1bc2-4c07-9616-790f20db4852", - "historySizeBytes": "452", - "workerVersion": { - "buildId": "33c39efbd03dad24ce42918880b097da" - } - } - }, - { - "eventId": "4", - "eventTime": "2026-10-27T03:10:47.450209215Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", - "taskId": "1048599", - "workflowTaskCompletedEventAttributes": { - "scheduledEventId": "2", - "startedEventId": "3", - "identity": "12@cb562f37fc4d", - "workerVersion": { - "buildId": "33c39efbd03dad24ce42918880b097da" - }, - "sdkMetadata": { - "coreUsedFlags": [ - 3, - 1, - 2 - ], - "sdkName": "temporal-python", - "sdkVersion": "1.34.0" - }, - "meteringMetadata": {} - } - }, - { - "eventId": "5", - "eventTime": "2026-10-27T03:10:47.450288465Z", - "eventType": "EVENT_TYPE_TIMER_STARTED", - "taskId": "1048600", - "timerStartedEventAttributes": { - "timerId": "1", - "startToFireTimeout": "1s", - "workflowTaskCompletedEventId": "4" - } - }, - { - "eventId": "6", - "eventTime": "2026-10-27T03:10:47.450329007Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_SCHEDULED", - "taskId": "1048601", - "activityTaskScheduledEventAttributes": { - "activityId": "1", - "activityType": { - "name": "invoke_model_activity_streaming" - }, - "taskQueue": { - "name": "tq", - "kind": "TASK_QUEUE_KIND_NORMAL" - }, - "header": { - "fields": { - "__openai_span": { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJzcGFuSWQiOiJzcGFuX2U0Mzk0ZTdlZTQwZTQ4YTY4ZmQ3Zjk5YiIsInRyYWNlSWQiOiJ0cmFjZV9hZjQyYzY1MDYwYzI0MDZlOGUyMWNhYTMiLCJ0cmFjZU5hbWUiOiJBZ2VudCB3b3JrZmxvdyJ9" - } - } - }, - "input": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJtb2RlbF9uYW1lIjpudWxsLCJzeXN0ZW1faW5zdHJ1Y3Rpb25zIjoiWW91IGFyZSBhIHRlc3QgYWdlbnQuIiwiaW5wdXQiOlt7ImNvbnRlbnQiOiJIZWxsbyIsInJvbGUiOiJ1c2VyIn1dLCJtb2RlbF9zZXR0aW5ncyI6eyJ0ZW1wZXJhdHVyZSI6bnVsbCwidG9wX3AiOm51bGwsImZyZXF1ZW5jeV9wZW5hbHR5IjpudWxsLCJwcmVzZW5jZV9wZW5hbHR5IjpudWxsLCJ0b29sX2Nob2ljZSI6bnVsbCwicGFyYWxsZWxfdG9vbF9jYWxscyI6bnVsbCwidHJ1bmNhdGlvbiI6bnVsbCwibWF4X3Rva2VucyI6bnVsbCwicmVhc29uaW5nIjp7ImVmZm9ydCI6Im5vbmUifSwidmVyYm9zaXR5IjoibG93IiwibWV0YWRhdGEiOm51bGwsInN0b3JlIjpudWxsLCJwcm9tcHRfY2FjaGVfcmV0ZW50aW9uIjpudWxsLCJpbmNsdWRlX3VzYWdlIjpudWxsLCJyZXNwb25zZV9pbmNsdWRlIjpudWxsLCJ0b3BfbG9ncHJvYnMiOm51bGwsImV4dHJhX3F1ZXJ5IjpudWxsLCJleHRyYV9ib2R5IjpudWxsLCJleHRyYV9oZWFkZXJzIjpudWxsLCJleHRyYV9hcmdzIjpudWxsLCJyZXRyeSI6bnVsbCwiY29udGV4dF9tYW5hZ2VtZW50IjpudWxsLCJwcm9tcHRfY2FjaGVfb3B0aW9ucyI6bnVsbCwicHJlc2VydmVfcmF3X3VzYWdlIjpudWxsLCJ0aW1lb3V0IjpudWxsfSwidG9vbHMiOltdLCJvdXRwdXRfc2NoZW1hIjpudWxsLCJoYW5kb2ZmcyI6W10sInRyYWNpbmciOjEsInByZXZpb3VzX3Jlc3BvbnNlX2lkIjpudWxsLCJjb252ZXJzYXRpb25faWQiOm51bGwsInByb21wdCI6bnVsbCwiaW1wbGljaXRfbW9kZWxfc2V0dGluZ3MiOnRydWUsInJ1bl9tb2RlbF9zZXR0aW5ncyI6bnVsbCwic3RyZWFtaW5nX3RvcGljIjoiZXZlbnRzIiwic3RyZWFtaW5nX2JhdGNoX2ludGVydmFsIjoiUFQwLjFTIn0=" - } - ] - }, - "scheduleToCloseTimeout": "30s", - "scheduleToStartTimeout": "30s", - "startToCloseTimeout": "30s", - "heartbeatTimeout": "1s", - "workflowTaskCompletedEventId": "4", - "retryPolicy": { - "initialInterval": "1s", - "backoffCoefficient": 2.0, - "maximumInterval": "100s" - }, - "useWorkflowBuildId": true, - "priority": {} - }, - "userMetadata": { - "summary": { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "IkFzc2lzdGFudCI=" - } - } - }, - { - "eventId": "7", - "eventTime": "2026-10-27T03:10:47.450519715Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", - "taskId": "1048602", - "workflowTaskScheduledEventAttributes": { - "taskQueue": { - "name": "tq", - "kind": "TASK_QUEUE_KIND_NORMAL" - }, - "startToCloseTimeout": "10s", - "attempt": 1 - } - }, - { - "eventId": "8", - "eventTime": "2026-10-27T03:10:47.451701465Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", - "taskId": "1048608", - "workflowTaskStartedEventAttributes": { - "scheduledEventId": "7", - "identity": "12@cb562f37fc4d", - "requestId": "f6247432-c818-4778-b82f-80c6107e374a", - "historySizeBytes": "2011", - "workerVersion": { - "buildId": "33c39efbd03dad24ce42918880b097da" - } - } - }, - { - "eventId": "9", - "eventTime": "2026-10-27T03:10:47.537492382Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", - "taskId": "1048612", - "workflowTaskCompletedEventAttributes": { - "scheduledEventId": "7", - "startedEventId": "8", - "identity": "12@cb562f37fc4d", - "workerVersion": { - "buildId": "33c39efbd03dad24ce42918880b097da" - }, - "sdkMetadata": {}, - "meteringMetadata": {} - } - }, - { - "eventId": "10", - "eventTime": "2026-10-27T03:10:47.537597632Z", - "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_UPDATE_ACCEPTED", - "taskId": "1048613", - "workflowExecutionUpdateAcceptedEventAttributes": { - "protocolInstanceId": "252388f4-e387-4452-9745-4bed9260fbd6", - "acceptedRequestMessageId": "252388f4-e387-4452-9745-4bed9260fbd6/request", - "acceptedRequestSequencingEventId": "7", - "acceptedRequest": { - "meta": { - "updateId": "252388f4-e387-4452-9745-4bed9260fbd6", - "identity": "12@cb562f37fc4d" - }, - "input": { - "header": { - "fields": { - "__openai_span": { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJzcGFuSWQiOm51bGwsInRyYWNlSWQiOm51bGwsInRyYWNlTmFtZSI6IlVua25vd24gV29ya2Zsb3cifQ==" - } - } - }, - "name": "__temporal_workflow_stream_poll", - "args": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJ0b3BpY3MiOlsiZXZlbnRzIl0sImZyb21fb2Zmc2V0IjowfQ==" - } - ] - } - } - } - } - }, - { - "eventId": "11", - "eventTime": "2026-10-27T03:10:47.578171882Z", - "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_SIGNALED", - "taskId": "1048615", - "workflowExecutionSignaledEventAttributes": { - "signalName": "__temporal_workflow_stream_publish", - "input": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJpdGVtcyI6W3sidG9waWMiOiJldmVudHMiLCJkYXRhIjoiQ2hZS0NHVnVZMjlrYVc1bkVncHFjMjl1TDNCc1lXbHVFbzBCZXlKamIyNTBaVzUwWDJsdVpHVjRJam93TENKa1pXeDBZU0k2SWtobGJHeHZJQ0lzSW1sMFpXMWZhV1FpT2lKcGRHVnRNU0lzSW14dlozQnliMkp6SWpwYlhTd2liM1YwY0hWMFgybHVaR1Y0SWpvd0xDSnpaWEYxWlc1alpWOXVkVzFpWlhJaU9qQXNJblI1Y0dVaU9pSnlaWE53YjI1elpTNXZkWFJ3ZFhSZmRHVjRkQzVrWld4MFlTSjkifV0sInB1Ymxpc2hlcl9pZCI6IjJmNjRkNmMzNGQwMzRlMjUiLCJzZXF1ZW5jZSI6MX0=" - } - ] - }, - "identity": "12@cb562f37fc4d", - "header": { - "fields": { - "__openai_span": { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJzcGFuSWQiOiJzcGFuX2U1NTFiMzUyNzEyZjQ0ZDZiZjNmZGY5OSIsInRyYWNlSWQiOiJ0cmFjZV9hZjQyYzY1MDYwYzI0MDZlOGUyMWNhYTMiLCJ0cmFjZU5hbWUiOiJBZ2VudCB3b3JrZmxvdyJ9" - } - } - }, - "requestId": "52aa98e1-ff33-45bc-b637-c4865fe2ef24" - } - }, - { - "eventId": "12", - "eventTime": "2026-10-27T03:10:47.578191132Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", - "taskId": "1048616", - "workflowTaskScheduledEventAttributes": { - "taskQueue": { - "name": "tq", - "kind": "TASK_QUEUE_KIND_NORMAL" - }, - "startToCloseTimeout": "10s", - "attempt": 1 - } - }, - { - "eventId": "13", - "eventTime": "2026-10-27T03:10:47.579264590Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", - "taskId": "1048619", - "workflowTaskStartedEventAttributes": { - "scheduledEventId": "12", - "identity": "12@cb562f37fc4d", - "requestId": "bd6fe8fd-f851-4336-bcb8-2e0f78777bb7", - "historySizeBytes": "3274", - "workerVersion": { - "buildId": "33c39efbd03dad24ce42918880b097da" - } - } - }, - { - "eventId": "14", - "eventTime": "2026-10-27T03:10:47.652576882Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", - "taskId": "1048623", - "workflowTaskCompletedEventAttributes": { - "scheduledEventId": "12", - "startedEventId": "13", - "identity": "12@cb562f37fc4d", - "workerVersion": { - "buildId": "33c39efbd03dad24ce42918880b097da" - }, - "sdkMetadata": {}, - "meteringMetadata": {} - } - }, - { - "eventId": "15", - "eventTime": "2026-10-27T03:10:47.652678257Z", - "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_UPDATE_COMPLETED", - "taskId": "1048624", - "workflowExecutionUpdateCompletedEventAttributes": { - "meta": { - "updateId": "252388f4-e387-4452-9745-4bed9260fbd6", - "identity": "12@cb562f37fc4d" - }, - "outcome": { - "success": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJpdGVtcyI6W3sidG9waWMiOiJldmVudHMiLCJkYXRhIjoiQ2hZS0NHVnVZMjlrYVc1bkVncHFjMjl1TDNCc1lXbHVFbzBCZXlKamIyNTBaVzUwWDJsdVpHVjRJam93TENKa1pXeDBZU0k2SWtobGJHeHZJQ0lzSW1sMFpXMWZhV1FpT2lKcGRHVnRNU0lzSW14dlozQnliMkp6SWpwYlhTd2liM1YwY0hWMFgybHVaR1Y0SWpvd0xDSnpaWEYxWlc1alpWOXVkVzFpWlhJaU9qQXNJblI1Y0dVaU9pSnlaWE53YjI1elpTNXZkWFJ3ZFhSZmRHVjRkQzVrWld4MFlTSjkiLCJvZmZzZXQiOjB9XSwibmV4dF9vZmZzZXQiOjEsIm1vcmVfcmVhZHkiOmZhbHNlfQ==" - } - ] - } - }, - "acceptedEventId": "10" - } - }, - { - "eventId": "16", - "eventTime": "2026-10-27T03:10:47.727664257Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", - "taskId": "1048630", - "workflowTaskScheduledEventAttributes": { - "taskQueue": { - "name": "tq", - "kind": "TASK_QUEUE_KIND_NORMAL" - }, - "startToCloseTimeout": "10s", - "attempt": 1 - } - }, - { - "eventId": "17", - "eventTime": "2026-10-27T03:10:47.728270882Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", - "taskId": "1048631", - "workflowTaskStartedEventAttributes": { - "scheduledEventId": "16", - "identity": "12@cb562f37fc4d", - "requestId": "de2a93e2-69da-4dda-9c89-5de696cd73ca", - "historySizeBytes": "3917", - "workerVersion": { - "buildId": "33c39efbd03dad24ce42918880b097da" - } - } - }, - { - "eventId": "18", - "eventTime": "2026-10-27T03:10:47.794331298Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", - "taskId": "1048632", - "workflowTaskCompletedEventAttributes": { - "scheduledEventId": "16", - "startedEventId": "17", - "identity": "12@cb562f37fc4d", - "workerVersion": { - "buildId": "33c39efbd03dad24ce42918880b097da" - }, - "sdkMetadata": {}, - "meteringMetadata": {} - } - }, - { - "eventId": "19", - "eventTime": "2026-10-27T03:10:47.794387840Z", - "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_UPDATE_ACCEPTED", - "taskId": "1048633", - "workflowExecutionUpdateAcceptedEventAttributes": { - "protocolInstanceId": "c1370f64-c9b6-4022-97e8-8d95040b5925", - "acceptedRequestMessageId": "c1370f64-c9b6-4022-97e8-8d95040b5925/request", - "acceptedRequestSequencingEventId": "16", - "acceptedRequest": { - "meta": { - "updateId": "c1370f64-c9b6-4022-97e8-8d95040b5925", - "identity": "12@cb562f37fc4d" - }, - "input": { - "header": { - "fields": { - "__openai_span": { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJzcGFuSWQiOm51bGwsInRyYWNlSWQiOm51bGwsInRyYWNlTmFtZSI6IlVua25vd24gV29ya2Zsb3cifQ==" - } - } - }, - "name": "__temporal_workflow_stream_poll", - "args": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJ0b3BpY3MiOlsiZXZlbnRzIl0sImZyb21fb2Zmc2V0IjoxfQ==" - } - ] - } - } - } - } - }, - { - "eventId": "20", - "eventTime": "2026-10-27T03:10:48.453296215Z", - "eventType": "EVENT_TYPE_TIMER_FIRED", - "taskId": "1048637", - "timerFiredEventAttributes": { - "timerId": "1", - "startedEventId": "5" - } - }, - { - "eventId": "21", - "eventTime": "2026-10-27T03:10:48.453340590Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", - "taskId": "1048638", - "workflowTaskScheduledEventAttributes": { - "taskQueue": { - "name": "tq", - "kind": "TASK_QUEUE_KIND_NORMAL" - }, - "startToCloseTimeout": "10s", - "attempt": 1 - } - }, - { - "eventId": "22", - "eventTime": "2026-10-27T03:10:48.454838007Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", - "taskId": "1048642", - "workflowTaskStartedEventAttributes": { - "scheduledEventId": "21", - "identity": "12@cb562f37fc4d", - "requestId": "c503d485-db50-44a0-b7d5-bc4fc57f756f", - "historySizeBytes": "4624", - "workerVersion": { - "buildId": "33c39efbd03dad24ce42918880b097da" - } - } - }, - { - "eventId": "23", - "eventTime": "2026-10-27T03:10:48.592583590Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", - "taskId": "1048646", - "workflowTaskCompletedEventAttributes": { - "scheduledEventId": "21", - "startedEventId": "22", - "identity": "12@cb562f37fc4d", - "workerVersion": { - "buildId": "33c39efbd03dad24ce42918880b097da" - }, - "sdkMetadata": {}, - "meteringMetadata": {} - } - }, - { - "eventId": "24", - "eventTime": "2026-10-27T03:10:57.518838428Z", - "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_SIGNALED", - "taskId": "1048668", - "workflowExecutionSignaledEventAttributes": { - "signalName": "__temporal_workflow_stream_publish", - "input": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJpdGVtcyI6W3sidG9waWMiOiJldmVudHMiLCJkYXRhIjoiQ2hZS0NHVnVZMjlrYVc1bkVncHFjMjl1TDNCc1lXbHVFbzBCZXlKamIyNTBaVzUwWDJsdVpHVjRJam93TENKa1pXeDBZU0k2SW5kdmNteGtJU0lzSW1sMFpXMWZhV1FpT2lKcGRHVnRNU0lzSW14dlozQnliMkp6SWpwYlhTd2liM1YwY0hWMFgybHVaR1Y0SWpvd0xDSnpaWEYxWlc1alpWOXVkVzFpWlhJaU9qRXNJblI1Y0dVaU9pSnlaWE53YjI1elpTNXZkWFJ3ZFhSZmRHVjRkQzVrWld4MFlTSjkifSx7InRvcGljIjoiZXZlbnRzIiwiZGF0YSI6IkNoWUtDR1Z1WTI5a2FXNW5FZ3BxYzI5dUwzQnNZV2x1RXRNRmV5SnlaWE53YjI1elpTSTZleUpwWkNJNkluSmxjM0JmZEdWemRDSXNJbU55WldGMFpXUmZZWFFpT2pBdU1Dd2laWEp5YjNJaU9tNTFiR3dzSW1sdVkyOXRjR3hsZEdWZlpHVjBZV2xzY3lJNmJuVnNiQ3dpYVc1emRISjFZM1JwYjI1eklqcHVkV3hzTENKdFpYUmhaR0YwWVNJNmUzMHNJbTF2WkdWc0lqb2lkR1Z6ZENJc0ltOWlhbVZqZENJNkluSmxjM0J2Ym5ObElpd2liM1YwY0hWMElqcGJleUpwWkNJNkltMXpaMTkwWlhOMElpd2lZMjl1ZEdWdWRDSTZXM3NpWVc1dWIzUmhkR2x2Ym5NaU9sdGRMQ0owWlhoMElqb2lTR1ZzYkc4Z2QyOXliR1FoSWl3aWRIbHdaU0k2SW05MWRIQjFkRjkwWlhoMElpd2liRzluY0hKdlluTWlPbHRkZlYwc0luSnZiR1VpT2lKaGMzTnBjM1JoYm5RaUxDSnpkR0YwZFhNaU9pSmpiMjF3YkdWMFpXUWlMQ0owZVhCbElqb2liV1Z6YzJGblpTSjlYU3dpY0dGeVlXeHNaV3hmZEc5dmJGOWpZV3hzY3lJNmRISjFaU3dpZEdWdGNHVnlZWFIxY21VaU9qRXVNQ3dpZEc5dmJGOWphRzlwWTJVaU9pSmhkWFJ2SWl3aWRHOXZiSE1pT2x0ZExDSjBiM0JmY0NJNk1TNHdMQ0p6ZEdGMGRYTWlPaUpqYjIxd2JHVjBaV1FpTENKMFpYaDBJanA3SW1admNtMWhkQ0k2ZXlKMGVYQmxJam9pZEdWNGRDSjlmU3dpZEhKMWJtTmhkR2x2YmlJNkltUnBjMkZpYkdWa0lpd2lkWE5oWjJVaU9uc2lhVzV3ZFhSZmRHOXJaVzV6SWpveE1Dd2lhVzV3ZFhSZmRHOXJaVzV6WDJSbGRHRnBiSE1pT25zaVkyRmphR1ZmZDNKcGRHVmZkRzlyWlc1eklqb3dMQ0pqWVdOb1pXUmZkRzlyWlc1eklqb3dmU3dpYjNWMGNIVjBYM1J2YTJWdWN5STZOU3dpYjNWMGNIVjBYM1J2YTJWdWMxOWtaWFJoYVd4eklqcDdJbkpsWVhOdmJtbHVaMTkwYjJ0bGJuTWlPakI5TENKMGIzUmhiRjkwYjJ0bGJuTWlPakUxZlgwc0luTmxjWFZsYm1ObFgyNTFiV0psY2lJNk1pd2lkSGx3WlNJNkluSmxjM0J2Ym5ObExtTnZiWEJzWlhSbFpDSjkifV0sInB1Ymxpc2hlcl9pZCI6IjJmNjRkNmMzNGQwMzRlMjUiLCJzZXF1ZW5jZSI6Mn0=" - } - ] - }, - "identity": "12@cb562f37fc4d", - "header": { - "fields": { - "__openai_span": { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJzcGFuSWQiOiJzcGFuXzBmYzEzNzczMWI0NTRjMGQ5ZDBiZWFmMyIsInRyYWNlSWQiOiJ0cmFjZV9hZjQyYzY1MDYwYzI0MDZlOGUyMWNhYTMiLCJ0cmFjZU5hbWUiOiJBZ2VudCB3b3JrZmxvdyJ9" - } - } - }, - "requestId": "e815873e-425e-4382-80c3-0b8ff88e123f" - } - }, - { - "eventId": "25", - "eventTime": "2026-10-27T03:10:57.518863803Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", - "taskId": "1048669", - "workflowTaskScheduledEventAttributes": { - "taskQueue": { - "name": "tq", - "kind": "TASK_QUEUE_KIND_NORMAL" - }, - "startToCloseTimeout": "10s", - "attempt": 1 - } - }, - { - "eventId": "26", - "eventTime": "2026-10-27T03:10:57.520626761Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", - "taskId": "1048672", - "workflowTaskStartedEventAttributes": { - "scheduledEventId": "25", - "identity": "12@cb562f37fc4d", - "requestId": "7d34a8ea-65e5-4829-8be3-ba9ea7621e6b", - "historySizeBytes": "6530", - "workerVersion": { - "buildId": "33c39efbd03dad24ce42918880b097da" - } - } - }, - { - "eventId": "27", - "eventTime": "2026-10-27T03:10:57.929872220Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", - "taskId": "1048677", - "workflowTaskCompletedEventAttributes": { - "scheduledEventId": "25", - "startedEventId": "26", - "identity": "12@cb562f37fc4d", - "workerVersion": { - "buildId": "33c39efbd03dad24ce42918880b097da" - }, - "sdkMetadata": {}, - "meteringMetadata": {} - } - }, - { - "eventId": "28", - "eventTime": "2026-10-27T03:10:57.930031428Z", - "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_UPDATE_COMPLETED", - "taskId": "1048678", - "workflowExecutionUpdateCompletedEventAttributes": { - "meta": { - "updateId": "c1370f64-c9b6-4022-97e8-8d95040b5925", - "identity": "12@cb562f37fc4d" - }, - "outcome": { - "success": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJpdGVtcyI6W3sidG9waWMiOiJldmVudHMiLCJkYXRhIjoiQ2hZS0NHVnVZMjlrYVc1bkVncHFjMjl1TDNCc1lXbHVFbzBCZXlKamIyNTBaVzUwWDJsdVpHVjRJam93TENKa1pXeDBZU0k2SW5kdmNteGtJU0lzSW1sMFpXMWZhV1FpT2lKcGRHVnRNU0lzSW14dlozQnliMkp6SWpwYlhTd2liM1YwY0hWMFgybHVaR1Y0SWpvd0xDSnpaWEYxWlc1alpWOXVkVzFpWlhJaU9qRXNJblI1Y0dVaU9pSnlaWE53YjI1elpTNXZkWFJ3ZFhSZmRHVjRkQzVrWld4MFlTSjkiLCJvZmZzZXQiOjF9LHsidG9waWMiOiJldmVudHMiLCJkYXRhIjoiQ2hZS0NHVnVZMjlrYVc1bkVncHFjMjl1TDNCc1lXbHVFdE1GZXlKeVpYTndiMjV6WlNJNmV5SnBaQ0k2SW5KbGMzQmZkR1Z6ZENJc0ltTnlaV0YwWldSZllYUWlPakF1TUN3aVpYSnliM0lpT201MWJHd3NJbWx1WTI5dGNHeGxkR1ZmWkdWMFlXbHNjeUk2Ym5Wc2JDd2lhVzV6ZEhKMVkzUnBiMjV6SWpwdWRXeHNMQ0p0WlhSaFpHRjBZU0k2ZTMwc0ltMXZaR1ZzSWpvaWRHVnpkQ0lzSW05aWFtVmpkQ0k2SW5KbGMzQnZibk5sSWl3aWIzVjBjSFYwSWpwYmV5SnBaQ0k2SW0xeloxOTBaWE4wSWl3aVkyOXVkR1Z1ZENJNlczc2lZVzV1YjNSaGRHbHZibk1pT2x0ZExDSjBaWGgwSWpvaVNHVnNiRzhnZDI5eWJHUWhJaXdpZEhsd1pTSTZJbTkxZEhCMWRGOTBaWGgwSWl3aWJHOW5jSEp2WW5NaU9sdGRmVjBzSW5KdmJHVWlPaUpoYzNOcGMzUmhiblFpTENKemRHRjBkWE1pT2lKamIyMXdiR1YwWldRaUxDSjBlWEJsSWpvaWJXVnpjMkZuWlNKOVhTd2ljR0Z5WVd4c1pXeGZkRzl2YkY5allXeHNjeUk2ZEhKMVpTd2lkR1Z0Y0dWeVlYUjFjbVVpT2pFdU1Dd2lkRzl2YkY5amFHOXBZMlVpT2lKaGRYUnZJaXdpZEc5dmJITWlPbHRkTENKMGIzQmZjQ0k2TVM0d0xDSnpkR0YwZFhNaU9pSmpiMjF3YkdWMFpXUWlMQ0owWlhoMElqcDdJbVp2Y20xaGRDSTZleUowZVhCbElqb2lkR1Y0ZENKOWZTd2lkSEoxYm1OaGRHbHZiaUk2SW1ScGMyRmliR1ZrSWl3aWRYTmhaMlVpT25zaWFXNXdkWFJmZEc5clpXNXpJam94TUN3aWFXNXdkWFJmZEc5clpXNXpYMlJsZEdGcGJITWlPbnNpWTJGamFHVmZkM0pwZEdWZmRHOXJaVzV6SWpvd0xDSmpZV05vWldSZmRHOXJaVzV6SWpvd2ZTd2liM1YwY0hWMFgzUnZhMlZ1Y3lJNk5Td2liM1YwY0hWMFgzUnZhMlZ1YzE5a1pYUmhhV3h6SWpwN0luSmxZWE52Ym1sdVoxOTBiMnRsYm5NaU9qQjlMQ0owYjNSaGJGOTBiMnRsYm5NaU9qRTFmWDBzSW5ObGNYVmxibU5sWDI1MWJXSmxjaUk2TWl3aWRIbHdaU0k2SW5KbGMzQnZibk5sTG1OdmJYQnNaWFJsWkNKOSIsIm9mZnNldCI6Mn1dLCJuZXh0X29mZnNldCI6MywibW9yZV9yZWFkeSI6ZmFsc2V9" - } - ] - } - }, - "acceptedEventId": "19" - } - }, - { - "eventId": "29", - "eventTime": "2026-10-27T03:10:47.450504340Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_STARTED", - "taskId": "1048679", - "activityTaskStartedEventAttributes": { - "scheduledEventId": "6", - "identity": "12@cb562f37fc4d", - "requestId": "d8c0f9e3-efd0-429f-a47e-e46d76aabfbd", - "attempt": 1, - "workerVersion": { - "buildId": "33c39efbd03dad24ce42918880b097da" - } - } - }, - { - "eventId": "30", - "eventTime": "2026-10-27T03:10:57.523988928Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_COMPLETED", - "taskId": "1048680", - "activityTaskCompletedEventAttributes": { - "result": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "W3siY29udGVudF9pbmRleCI6MCwiZGVsdGEiOiJIZWxsbyAiLCJpdGVtX2lkIjoiaXRlbTEiLCJsb2dwcm9icyI6W10sIm91dHB1dF9pbmRleCI6MCwic2VxdWVuY2VfbnVtYmVyIjowLCJ0eXBlIjoicmVzcG9uc2Uub3V0cHV0X3RleHQuZGVsdGEifSx7ImNvbnRlbnRfaW5kZXgiOjAsImRlbHRhIjoid29ybGQhIiwiaXRlbV9pZCI6Iml0ZW0xIiwibG9ncHJvYnMiOltdLCJvdXRwdXRfaW5kZXgiOjAsInNlcXVlbmNlX251bWJlciI6MSwidHlwZSI6InJlc3BvbnNlLm91dHB1dF90ZXh0LmRlbHRhIn0seyJyZXNwb25zZSI6eyJpZCI6InJlc3BfdGVzdCIsImNyZWF0ZWRfYXQiOjAuMCwiZXJyb3IiOm51bGwsImluY29tcGxldGVfZGV0YWlscyI6bnVsbCwiaW5zdHJ1Y3Rpb25zIjpudWxsLCJtZXRhZGF0YSI6e30sIm1vZGVsIjoidGVzdCIsIm9iamVjdCI6InJlc3BvbnNlIiwib3V0cHV0IjpbeyJpZCI6Im1zZ190ZXN0IiwiY29udGVudCI6W3siYW5ub3RhdGlvbnMiOltdLCJ0ZXh0IjoiSGVsbG8gd29ybGQhIiwidHlwZSI6Im91dHB1dF90ZXh0IiwibG9ncHJvYnMiOltdfV0sInJvbGUiOiJhc3Npc3RhbnQiLCJzdGF0dXMiOiJjb21wbGV0ZWQiLCJ0eXBlIjoibWVzc2FnZSJ9XSwicGFyYWxsZWxfdG9vbF9jYWxscyI6dHJ1ZSwidGVtcGVyYXR1cmUiOjEuMCwidG9vbF9jaG9pY2UiOiJhdXRvIiwidG9vbHMiOltdLCJ0b3BfcCI6MS4wLCJzdGF0dXMiOiJjb21wbGV0ZWQiLCJ0ZXh0Ijp7ImZvcm1hdCI6eyJ0eXBlIjoidGV4dCJ9fSwidHJ1bmNhdGlvbiI6ImRpc2FibGVkIiwidXNhZ2UiOnsiaW5wdXRfdG9rZW5zIjoxMCwiaW5wdXRfdG9rZW5zX2RldGFpbHMiOnsiY2FjaGVfd3JpdGVfdG9rZW5zIjowLCJjYWNoZWRfdG9rZW5zIjowfSwib3V0cHV0X3Rva2VucyI6NSwib3V0cHV0X3Rva2Vuc19kZXRhaWxzIjp7InJlYXNvbmluZ190b2tlbnMiOjB9LCJ0b3RhbF90b2tlbnMiOjE1fX0sInNlcXVlbmNlX251bWJlciI6MiwidHlwZSI6InJlc3BvbnNlLmNvbXBsZXRlZCJ9XQ==" - } - ] - }, - "scheduledEventId": "6", - "startedEventId": "29", - "identity": "12@cb562f37fc4d" - } - }, - { - "eventId": "31", - "eventTime": "2026-10-27T03:10:57.930080720Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", - "taskId": "1048681", - "workflowTaskScheduledEventAttributes": { - "taskQueue": { - "name": "tq", - "kind": "TASK_QUEUE_KIND_NORMAL" - }, - "startToCloseTimeout": "10s", - "attempt": 1 - } - }, - { - "eventId": "32", - "eventTime": "2026-10-27T03:10:57.931372637Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", - "taskId": "1048684", - "workflowTaskStartedEventAttributes": { - "scheduledEventId": "31", - "identity": "12@cb562f37fc4d", - "requestId": "2b62ce5a-33b5-4196-9474-ed97f6c230f4", - "historySizeBytes": "9467", - "workerVersion": { - "buildId": "33c39efbd03dad24ce42918880b097da" - } - } - }, - { - "eventId": "33", - "eventTime": "2026-10-27T03:10:58.104904762Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", - "taskId": "1048689", - "workflowTaskCompletedEventAttributes": { - "scheduledEventId": "31", - "startedEventId": "32", - "identity": "12@cb562f37fc4d", - "workerVersion": { - "buildId": "33c39efbd03dad24ce42918880b097da" - }, - "sdkMetadata": {}, - "meteringMetadata": {} - } - }, - { - "eventId": "34", - "eventTime": "2026-10-27T03:10:57.933569970Z", - "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_SIGNALED", - "taskId": "1048690", - "workflowExecutionSignaledEventAttributes": { - "signalName": "finish", - "identity": "12@cb562f37fc4d", - "header": { - "fields": { - "__openai_span": { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJzcGFuSWQiOm51bGwsInRyYWNlSWQiOm51bGwsInRyYWNlTmFtZSI6IlVua25vd24gV29ya2Zsb3cifQ==" - } - } - }, - "requestId": "7b193871-d24d-4d5f-8bbd-08c55d60c00e" - } - }, - { - "eventId": "35", - "eventTime": "2026-10-27T03:10:58.104936053Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", - "taskId": "1048691", - "workflowTaskScheduledEventAttributes": { - "taskQueue": { - "name": "tq", - "kind": "TASK_QUEUE_KIND_NORMAL" - }, - "startToCloseTimeout": "10s", - "attempt": 1 - } - }, - { - "eventId": "36", - "eventTime": "2026-10-27T03:10:58.106685470Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", - "taskId": "1048694", - "workflowTaskStartedEventAttributes": { - "scheduledEventId": "35", - "identity": "12@cb562f37fc4d", - "requestId": "590bd072-16e1-4ea0-ab12-6bbbf58120f7", - "historySizeBytes": "9914", - "workerVersion": { - "buildId": "33c39efbd03dad24ce42918880b097da" - } - } - }, - { - "eventId": "37", - "eventTime": "2026-10-27T03:10:58.299198845Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", - "taskId": "1048698", - "workflowTaskCompletedEventAttributes": { - "scheduledEventId": "35", - "startedEventId": "36", - "identity": "12@cb562f37fc4d", - "workerVersion": { - "buildId": "33c39efbd03dad24ce42918880b097da" - }, - "sdkMetadata": {}, - "meteringMetadata": {} - } - }, - { - "eventId": "38", - "eventTime": "2026-10-27T03:10:58.299265720Z", - "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_COMPLETED", - "taskId": "1048699", - "workflowExecutionCompletedEventAttributes": { - "result": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "InJlZnVzZWQi" - } - ] - }, - "workflowTaskCompletedEventId": "37" - } - } - ] -} diff --git a/python/openai_agents/tests/test_openai_replay.py b/python/openai_agents/tests/test_openai_replay.py index 7e9be419..7ebcb6c3 100644 --- a/python/openai_agents/tests/test_openai_replay.py +++ b/python/openai_agents/tests/test_openai_replay.py @@ -4,7 +4,7 @@ import pytest from temporalio.client import WorkflowHistory -from temporalio.openai_agents import ModelActivityParameters, OpenAIAgentsPlugin +from temporalio.openai_agents import OpenAIAgentsPlugin from temporalio.worker import Replayer from tests.test_openai import ( AgentsAsToolsWorkflow, @@ -16,7 +16,6 @@ RunModelProviderWorkflow, ToolsWorkflow, ) -from tests.test_openai_streaming import StreamingInputGuardrailWorkflow @pytest.mark.parametrize( @@ -60,21 +59,3 @@ async def test_replay(file_name: str) -> None: for w in recorder if "was imported after initial workflow load" in str(w.message) ] - - -async def test_replay_streaming_input_guardrail_trip() -> None: - # Recorded before the streaming model activity was cancelled on a tripwire: the parallel - # input guardrail tripped mid-stream and the activity ran to completion. Streamed runs - # import modules during activations, so this case does not check sandbox imports. - history_json = ( - Path(__file__).with_name("histories") - / "streaming-input-guardrail-workflow-history.json" - ).read_text() - await Replayer( - workflows=[StreamingInputGuardrailWorkflow], - plugins=[ - OpenAIAgentsPlugin( - model_params=ModelActivityParameters(streaming_topic="events") - ) - ], - ).replay_workflow(WorkflowHistory.from_json("fake", history_json)) From 08fd775ec1c89d4499e3e3e38399d57dabb42a21 Mon Sep 17 00:00:00 2001 From: Francesc Campoy Date: Sat, 31 Oct 2026 10:47:29 -0700 Subject: [PATCH 4/7] Address review findings --- python/openai_agents/README.md | 12 +- python/openai_agents/pyproject.toml | 2 +- .../openai_agents/_invoke_model_activity.py | 23 +- .../openai_agents/_temporal_model_stub.py | 16 +- .../tests/test_openai_streaming.py | 285 +++++++++++++++--- python/openai_agents/uv.lock | 20 +- 6 files changed, 294 insertions(+), 64 deletions(-) diff --git a/python/openai_agents/README.md b/python/openai_agents/README.md index 336ac696..1903afa4 100644 --- a/python/openai_agents/README.md +++ b/python/openai_agents/README.md @@ -10,7 +10,7 @@ We welcome questions and feedback in the [#python-sdk](https://temporalio.slack. uv add temporalio-openai-agents ``` -This integration requires Temporal 1.34 or later. The SDK extends its package +This integration requires Temporal 1.35 or later. The SDK extends its package search path to support split-directory installations, including editable installs, separate user and system sites, layered deployments, and `--target` installs. @@ -828,6 +828,16 @@ Subscribers can still get the events the model sent before that, as a native `stream_events()` consumer can. When the guardrail has already tripped, the plugin does not start the model call. +The workflow must still be open when the activity publishes that +`error` event. With the default `cancellation_type` (`TRY_CANCEL`), the +run raises without waiting for the activity to stop, so a workflow that +completes right after the trip can close first, and subscribers then see +the stream end without the event. Set +`cancellation_type=ActivityCancellationType.WAIT_CANCELLATION_COMPLETED` +to make the run raise only after the activity has published it. Other +kinds of cancel, such as a Worker shutdown or a heartbeat timeout, +publish no `error` event, because the activity's retry can still answer. + ## Feature Support This integration is presently subject to certain limitations. diff --git a/python/openai_agents/pyproject.toml b/python/openai_agents/pyproject.toml index b558ea88..e6dfc5b3 100644 --- a/python/openai_agents/pyproject.toml +++ b/python/openai_agents/pyproject.toml @@ -19,7 +19,7 @@ classifiers = [ dependencies = [ # opentelemetry and pydantic extras: the plugin imports temporalio.contrib.opentelemetry and # temporalio.contrib.pydantic at module level. - "temporalio[opentelemetry,pydantic]>=1.34.0,<2", + "temporalio[opentelemetry,pydantic]>=1.35.0,<2", "openai-agents>=0.22,<0.23", "openai>=3,<4", # imports openai._models.construct_type and openai.types.responses "mcp>=1.24,<3", diff --git a/python/openai_agents/src/temporalio/openai_agents/_invoke_model_activity.py b/python/openai_agents/src/temporalio/openai_agents/_invoke_model_activity.py index 27c558c4..c438dd7a 100644 --- a/python/openai_agents/src/temporalio/openai_agents/_invoke_model_activity.py +++ b/python/openai_agents/src/temporalio/openai_agents/_invoke_model_activity.py @@ -519,16 +519,21 @@ async def invoke_model_activity_streaming( except APIStatusError as e: _raise_for_openai_status(e) except asyncio.CancelledError: - # Tell subscribers the response ends here, for example because a parallel - # input guardrail tripped. Leaving the context manager flushes it. - events_topic.publish( - ResponseErrorEvent( - type="error", - code="cancelled", - message="The model call was cancelled.", - sequence_number=len(events), + # When the workflow cancelled the call, for example because a parallel + # input guardrail tripped, tell subscribers the response ends here. + # Leaving the context manager flushes it. Other cancels, such as a + # Worker shutdown or a heartbeat timeout, can be retried, so publish + # nothing and let the retry answer. + details = activity.cancellation_details() + if details is not None and details.cancel_requested: + events_topic.publish( + ResponseErrorEvent( + type="error", + code="cancelled", + message="The model call was cancelled.", + sequence_number=len(events), + ) ) - ) raise return events diff --git a/python/openai_agents/src/temporalio/openai_agents/_temporal_model_stub.py b/python/openai_agents/src/temporalio/openai_agents/_temporal_model_stub.py index 17052764..c3428b0b 100644 --- a/python/openai_agents/src/temporalio/openai_agents/_temporal_model_stub.py +++ b/python/openai_agents/src/temporalio/openai_agents/_temporal_model_stub.py @@ -349,12 +349,13 @@ async def stream_response( # The activity publishes each event to the topic as the model sends it, but the # framework only checks the parallel input guardrails against the events it yields # after the activity returns. So once one of them trips, don't start the activity, - # or cancel it: it then publishes an error event and stops. + # or cancel it: it then publishes an error event and stops. The framework records + # the trip before it waits for the other guardrails to clean up, so act on the + # record without waiting for the guardrails task to finish. guardrails = _parallel_input_guardrails() if guardrails is not None: - result, task = guardrails - triggered = result._triggered_input_guardrail_result - if task.done() and triggered is not None: + triggered = guardrails[0]._triggered_input_guardrail_result + if triggered is not None: raise InputGuardrailTripwireTriggered(triggered) handle = workflow.start_activity_method( @@ -378,7 +379,12 @@ async def stream_response( result, task = guardrails async def cancel_on_tripwire() -> None: - await workflow.wait([task]) + await workflow.wait_condition( + lambda: ( + result._triggered_input_guardrail_result is not None + or task.done() + ) + ) triggered = result._triggered_input_guardrail_result if triggered is not None and not handle.done(): cancelled_for.append(triggered) diff --git a/python/openai_agents/tests/test_openai_streaming.py b/python/openai_agents/tests/test_openai_streaming.py index 2e6292da..d11c2596 100644 --- a/python/openai_agents/tests/test_openai_streaming.py +++ b/python/openai_agents/tests/test_openai_streaming.py @@ -10,12 +10,15 @@ import logging import uuid from collections.abc import AsyncIterator +from dataclasses import dataclass from datetime import timedelta from typing import Any import pytest from agents import ( Agent, + AgentHookContext, + AgentHooks, AgentOutputSchemaBase, GuardrailFunctionOutput, Handoff, @@ -54,12 +57,13 @@ from openai.types.shared import Reasoning from openai.types.shared.response_format_text import ResponseFormatText -from temporalio import workflow -from temporalio.client import Client, WorkflowFailureError +from temporalio import activity, workflow +from temporalio.client import Client, WorkflowFailureError, WorkflowHandle from temporalio.contrib.workflow_streams import WorkflowStream, WorkflowStreamClient from temporalio.openai_agents import ModelActivityParameters from temporalio.openai_agents.testing import AgentEnvironment -from tests.helpers import new_worker +from temporalio.workflow import ActivityCancellationType +from tests.helpers import assert_eventually, new_worker logger = logging.getLogger(__name__) @@ -536,13 +540,15 @@ async def test_streamed_run_runs_sync_function_tools(client: Client): class HoldingStreamingModel(StreamingTestModel): - """Streaming test model that sends its first delta, then waits up to 10 s before the rest. + """Streaming test model that sends its first delta, then, on the activity's first + attempt, waits up to 10 s before the rest. - Records whether its stream was cancelled during the wait. + Records the attempts and whether its stream was cancelled during the wait. """ def __init__(self) -> None: self.cancelled = False + self.attempts: list[int] = [] async def stream_response( self, @@ -565,12 +571,15 @@ async def stream_response( tracing, **kwargs, ) + attempt = activity.info().attempt + self.attempts.append(attempt) yield await anext(events) - try: - await asyncio.sleep(10) - except asyncio.CancelledError: - self.cancelled = True - raise + if attempt == 1: + try: + await asyncio.sleep(10) + except asyncio.CancelledError: + self.cancelled = True + raise async for event in events: yield event @@ -586,10 +595,41 @@ async def refuse_after_a_second( return GuardrailFunctionOutput(output_info=None, tripwire_triggered=True) +@input_guardrail +async def clean_up_slowly( + _context: RunContextWrapper[None], + _agent: Agent, + _input: str | list[TResponseInputItem], +) -> GuardrailFunctionOutput: + """A parallel guardrail that takes 12 s to clean up when the framework cancels it.""" + try: + await workflow.sleep(60) + except asyncio.CancelledError: + await workflow.sleep(12) + raise + return GuardrailFunctionOutput(output_info=None, tripwire_triggered=False) + + +class DelayModelCall(AgentHooks[None]): + """Delays the model call by two seconds, so the guardrail trips before it.""" + + async def on_start(self, context: AgentHookContext[None], agent: Agent) -> None: + await workflow.sleep(2) + + +@dataclass +class GuardrailRun: + trip: bool = True + sibling: bool = False + delay_model: bool = False + # Stay open until the subscriber has read the activity's last event. + hold_open: bool = True + + @workflow.defn class StreamingInputGuardrailWorkflow: @workflow.init - def __init__(self, prompt: str) -> None: + def __init__(self, run: GuardrailRun) -> None: self.stream = WorkflowStream() self.subscriber_done = False @@ -598,34 +638,77 @@ def finish(self) -> None: self.subscriber_done = True @workflow.run - async def run(self, prompt: str) -> str: + async def run(self, run: GuardrailRun) -> str: + guardrails = [refuse_after_a_second] if run.trip else [] + if run.sibling: + guardrails.append(clean_up_slowly) agent = Agent[None]( name="Assistant", instructions="You are a test agent.", - input_guardrails=[refuse_after_a_second], + input_guardrails=guardrails, + hooks=DelayModelCall() if run.delay_model else None, ) - result = Runner.run_streamed(starting_agent=agent, input=prompt) + result = Runner.run_streamed(starting_agent=agent, input="Hello") try: async for _ in result.stream_events(): pass except InputGuardrailTripwireTriggered: - # Stay open until the subscriber has read the activity's last event. + output = "refused" + else: + output = str(result.final_output) + if run.hold_open: await workflow.wait_condition(lambda: self.subscriber_done) - return "refused" - return result.final_output + return output + + +async def collect_until_the_end( + client: Client, + workflow_id: str, + published: list[TResponseStreamEvent] | None = None, +) -> list[TResponseStreamEvent]: + """Read the topic into ``published`` until an ``error`` or ``response.completed`` event.""" + stream = WorkflowStreamClient.create(client, workflow_id) + if published is None: + published = [] + async for item in stream.subscribe( + ["events"], + from_offset=0, + result_type=TResponseStreamEvent, # type: ignore[arg-type,call-overload] + poll_cooldown=timedelta(milliseconds=50), + ): + published.append(item.data) + if item.data.type in ("error", "response.completed"): + break + return published + + +async def streaming_activity_count(handle: WorkflowHandle[Any, Any]) -> int: + count = 0 + async for event in handle.fetch_history_events(): + if ( + event.HasField("activity_task_scheduled_event_attributes") + and event.activity_task_scheduled_event_attributes.activity_type.name + == "invoke_model_activity_streaming" + ): + count += 1 + return count @pytest.mark.asyncio -async def test_streaming_input_guardrail_trip_stops_the_topic(client: Client): +@pytest.mark.parametrize("sibling", [False, True], ids=["alone", "sibling-cleaning-up"]) +async def test_streaming_input_guardrail_trip_stops_the_topic( + client: Client, sibling: bool +): """A parallel input guardrail that trips while the model is streaming cancels the streaming activity: topic subscribers get an error event, not the rest of the answer - and ``response.completed``.""" + and ``response.completed``. The cancel does not wait for the other parallel + guardrails to finish cleaning up.""" model = HoldingStreamingModel() async with AgentEnvironment( model=model, model_params=ModelActivityParameters( start_to_close_timeout=timedelta(seconds=30), - heartbeat_timeout=timedelta(seconds=1), + heartbeat_timeout=timedelta(seconds=3), streaming_topic="events", ), ) as env: @@ -637,27 +720,14 @@ async def test_streaming_input_guardrail_trip_stops_the_topic(client: Client): ) as worker: handle = await client.start_workflow( StreamingInputGuardrailWorkflow.run, - "Hello", + GuardrailRun(sibling=sibling), id=workflow_id, task_queue=worker.task_queue, - execution_timeout=timedelta(seconds=30), + execution_timeout=timedelta(seconds=50), + ) + published = await asyncio.wait_for( + collect_until_the_end(client, workflow_id), timeout=20.0 ) - - stream = WorkflowStreamClient.create(client, workflow_id) - published: list[TResponseStreamEvent] = [] - - async def collect_events() -> None: - async for item in stream.subscribe( - ["events"], - from_offset=0, - result_type=TResponseStreamEvent, # type: ignore[arg-type,call-overload] - poll_cooldown=timedelta(milliseconds=50), - ): - published.append(item.data) - if item.data.type in ("error", "response.completed"): - break - - await asyncio.wait_for(collect_events(), timeout=20.0) await handle.signal(StreamingInputGuardrailWorkflow.finish) result = await handle.result() @@ -668,3 +738,142 @@ async def collect_events() -> None: assert isinstance(last, ResponseErrorEvent) assert last.code == "cancelled" assert model.cancelled + assert model.attempts == [1] + + +@pytest.mark.asyncio +@pytest.mark.parametrize("sibling", [False, True], ids=["alone", "sibling-cleaning-up"]) +async def test_streaming_input_guardrail_known_trip_skips_the_model( + client: Client, sibling: bool +): + """When a parallel input guardrail has already tripped as the model is called, the + streaming activity is not scheduled, even while other guardrails are still cleaning + up.""" + model = HoldingStreamingModel() + async with AgentEnvironment( + model=model, + model_params=ModelActivityParameters( + start_to_close_timeout=timedelta(seconds=30), + heartbeat_timeout=timedelta(seconds=3), + streaming_topic="events", + ), + ) as env: + client = env.applied_on_client(client) + async with new_worker( + client, StreamingInputGuardrailWorkflow, max_cached_workflows=0 + ) as worker: + handle = await client.start_workflow( + StreamingInputGuardrailWorkflow.run, + GuardrailRun(sibling=sibling, delay_model=True, hold_open=False), + id=f"openai-streaming-known-trip-{uuid.uuid4()}", + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=50), + ) + result = await handle.result() + scheduled = await streaming_activity_count(handle) + + assert result == "refused" + assert scheduled == 0 + assert model.attempts == [] + + +@pytest.mark.asyncio +@pytest.mark.parametrize("max_cached_workflows", [1000, 0], ids=["cached", "uncached"]) +async def test_streaming_input_guardrail_trip_waits_for_the_error_event( + client: Client, max_cached_workflows: int +): + """With ``WAIT_CANCELLATION_COMPLETED``, a workflow that returns right after the trip + still has the error event in its stream, and the Worker shuts down after the cancel, + with or without a Workflow cache.""" + model = HoldingStreamingModel() + async with AgentEnvironment( + model=model, + model_params=ModelActivityParameters( + start_to_close_timeout=timedelta(seconds=30), + heartbeat_timeout=timedelta(seconds=3), + streaming_topic="events", + cancellation_type=ActivityCancellationType.WAIT_CANCELLATION_COMPLETED, + ), + ) as env: + client = env.applied_on_client(client) + workflow_id = f"openai-streaming-guardrail-wait-{uuid.uuid4()}" + async with new_worker( + client, + StreamingInputGuardrailWorkflow, + max_cached_workflows=max_cached_workflows, + ) as worker: + result = await client.execute_workflow( + StreamingInputGuardrailWorkflow.run, + GuardrailRun(hold_open=False), + id=workflow_id, + task_queue=worker.task_queue, + execution_timeout=timedelta(seconds=30), + ) + # The model held after its first delta and was cancelled, so the stream + # holds that delta and the error event. + offset = await WorkflowStreamClient.create(client, workflow_id).get_offset() + + assert result == "refused" + assert model.cancelled + assert offset == 2 + + +@pytest.mark.asyncio +async def test_streaming_worker_shutdown_publishes_no_error_event(client: Client): + """A Worker shutdown cancels the streaming activity, but its retry can still answer, + so subscribers get no error event before the retry's events.""" + model = HoldingStreamingModel() + async with AgentEnvironment( + model=model, + model_params=ModelActivityParameters( + start_to_close_timeout=timedelta(seconds=30), + heartbeat_timeout=timedelta(seconds=3), + streaming_topic="events", + ), + ) as env: + client = env.applied_on_client(client) + workflow_id = f"openai-streaming-shutdown-{uuid.uuid4()}" + task_queue = str(uuid.uuid4()) + + # Only one Worker per task queue can be registered in a process at a time. + async with new_worker( + client, + StreamingInputGuardrailWorkflow, + task_queue=task_queue, + max_cached_workflows=0, + ): + handle = await client.start_workflow( + StreamingInputGuardrailWorkflow.run, + GuardrailRun(trip=False), + id=workflow_id, + task_queue=task_queue, + execution_timeout=timedelta(seconds=50), + ) + published: list[TResponseStreamEvent] = [] + collect_task = asyncio.create_task( + collect_until_the_end(client, workflow_id, published) + ) + + async def first_delta_read() -> None: + assert published + + # Shut the Worker down once the subscriber has the first attempt's delta. + await assert_eventually(first_delta_read, timeout=timedelta(seconds=20)) + async with new_worker( + client, + StreamingInputGuardrailWorkflow, + task_queue=task_queue, + max_cached_workflows=0, + ): + await asyncio.wait_for(collect_task, timeout=30.0) + await handle.signal(StreamingInputGuardrailWorkflow.finish) + result = await handle.result() + + assert result == "Hello world!" + assert model.attempts == [1, 2] + assert [e.type for e in published] == [ + "response.output_text.delta", + "response.output_text.delta", + "response.output_text.delta", + "response.completed", + ] diff --git a/python/openai_agents/uv.lock b/python/openai_agents/uv.lock index 636c9aee..01066cff 100644 --- a/python/openai_agents/uv.lock +++ b/python/openai_agents/uv.lock @@ -2942,7 +2942,7 @@ wheels = [ [[package]] name = "temporalio" -version = "1.34.0" +version = "1.35.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nexus-rpc" }, @@ -2951,15 +2951,15 @@ dependencies = [ { name = "types-protobuf" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/13/85/9ece6f400552ed21951c8c5c534056e7d015cb065e3384470ec23ac24f28/temporalio-1.34.0.tar.gz", hash = "sha256:6453cb20e18df485e16578b22c82a9c4bcb1cf7eedd94147dfd373551d80f5b6", size = 3163890, upload-time = "2026-09-30T20:22:56.056Z" } +sdist = { url = "https://files.pythonhosted.org/packages/69/38/03641a02eb18e2b16f58c1c136deafdffeb30828dcd83ebd8ac41eb39fd7/temporalio-1.35.0.tar.gz", hash = "sha256:c7a14f29909f8d847f78de49305af89c0ec5aea39272cd4050124386ac3ca295", size = 2879693, upload-time = "2026-10-28T20:13:38.726Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/71/e55380e7820819357afada375d4e5c33d141bb94a08fda35aa0551b4369b/temporalio-1.34.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:87118447ad13e1062b79bfc8b44b1695e8328ac9c6be1776e426e87b501b135a", size = 14064102, upload-time = "2026-09-30T20:22:35.601Z" }, - { url = "https://files.pythonhosted.org/packages/db/78/356c8e2f0ec1678b757d4c5e8a1b0b1c65b15b8afb2335ad2944c158ea10/temporalio-1.34.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:023fff9cd9dd21860061e003880dcf95250700f5afab96c030c9cb258504dab8", size = 13763219, upload-time = "2026-09-30T20:22:38.774Z" }, - { url = "https://files.pythonhosted.org/packages/e9/00/4a4b4e018c4c12b4691211aca1d97db8707ed4c9e31f40dcf09784c9913d/temporalio-1.34.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdb6e2fb525ea5635afa4a3f1ae4c992c16dbe29dc92939a85153baeb69d95a2", size = 14121901, upload-time = "2026-09-30T20:22:41.428Z" }, - { url = "https://files.pythonhosted.org/packages/c1/20/b032d4a0df51d466fd929d8205e1288e83d97a890f021479f43af9bd6f9b/temporalio-1.34.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:540761f738bdfe5cb5bd7240b659e116a0b5094b94282aef09b8d8c2d66e9c52", size = 14457183, upload-time = "2026-09-30T20:22:44.215Z" }, - { url = "https://files.pythonhosted.org/packages/b6/0d/b08ffeca93bbc29200a5bd85a45cca3c6be0a123de605ecd1798b617df4f/temporalio-1.34.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:87647f87f42ecd45efb675642e2ec8ca34aa42359f5968f31f5391f1db5ebcbe", size = 14179836, upload-time = "2026-09-30T20:22:47.474Z" }, - { url = "https://files.pythonhosted.org/packages/81/55/acde4d1b7c9f23434e263752ef0a3ef58adeb3b95ef8472607414798dfaa/temporalio-1.34.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:71caa4b9061628b22a457c87c3b16da40f7ac6ac1a26ece9aa1ee6b4934404d7", size = 14570248, upload-time = "2026-09-30T20:22:50.569Z" }, - { url = "https://files.pythonhosted.org/packages/cc/c5/6bdc3b02ec483093a71e9f887e8a50ae20fcc79e0578ed7435a81d4271b1/temporalio-1.34.0-cp310-abi3-win_amd64.whl", hash = "sha256:03bd86561188c18d88425178bc690fe0791104b78566dcebd98f59cbdbce0952", size = 15415684, upload-time = "2026-09-30T20:22:53.342Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d9/8bda464beda3adb897d8d9f0e5fa26b28f2bf63c831c6800cbd73f7728cb/temporalio-1.35.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:ac3ab920cc05e24a1d420688a49aabed7b2be8f25dd275200dfa839be30980fd", size = 13989719, upload-time = "2026-10-28T20:13:16.897Z" }, + { url = "https://files.pythonhosted.org/packages/26/c1/94861d72cbe5e1778fcd4685db63231af01d40ad99f0375b4df6d4e60d06/temporalio-1.35.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:51bae017d484450084daa19afafad12fa0af1a00af351ba304104d7b97b44f9a", size = 13552491, upload-time = "2026-10-28T20:13:19.725Z" }, + { url = "https://files.pythonhosted.org/packages/3c/ed/a776afec1033f749ec8f4c0c5264167aedc56a108a5adebfa4084af8aa82/temporalio-1.35.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b93845a2200d23b8edc9a8b0bf78b58bdb5e78721602b3f64072867e07e28504", size = 13927445, upload-time = "2026-10-28T20:13:21.984Z" }, + { url = "https://files.pythonhosted.org/packages/ca/0b/bf0ed1f1b623e1ffd5c21c9e65a331a5c1c4f77f3829dc99ceae99b69f39/temporalio-1.35.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45b5438f41ab154eda4eea40069df2fa8779cb8f5c27f266b917a700bfba8bb8", size = 14386743, upload-time = "2026-10-28T20:13:28.201Z" }, + { url = "https://files.pythonhosted.org/packages/76/d6/373d5ffe28263fc56ea0013bdd45715b4ff84f97d1052519b7c0d9ecd437/temporalio-1.35.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:8a5ce8b1e9c9e2df5fb9f17a345c0ef0a132c787cef8cf5a6593b5d707648253", size = 13988280, upload-time = "2026-10-28T20:13:31.667Z" }, + { url = "https://files.pythonhosted.org/packages/9c/41/47ad6f26002f84d8293d99098c414f75e9e8ff7955d034b7121bd211649b/temporalio-1.35.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:96310ae91b098c9fe2194f0892cef25381e8f870e48d5c069230312bbd1c6262", size = 14494297, upload-time = "2026-10-28T20:13:33.988Z" }, + { url = "https://files.pythonhosted.org/packages/db/33/30942b2d568df2c50089e381ee63b2d141f2ebfb7b4787bac3540f5cf617/temporalio-1.35.0-cp310-abi3-win_amd64.whl", hash = "sha256:6f434c0d3b6d049f4473f9f5d3e80b5e80c986440fd0490f2ee70cc631cbcddd", size = 15317073, upload-time = "2026-10-28T20:13:36.195Z" }, ] [package.optional-dependencies] @@ -3045,7 +3045,7 @@ requires-dist = [ { name = "openinference-instrumentation-openai-agents", marker = "extra == 'openinference'", specifier = ">=2,<3" }, { name = "opentelemetry-sdk", marker = "extra == 'openinference'", specifier = ">=1.26,<2" }, { name = "pydantic", specifier = ">=2,<3" }, - { name = "temporalio", extras = ["opentelemetry", "pydantic"], specifier = ">=1.34.0,<2" }, + { name = "temporalio", extras = ["opentelemetry", "pydantic"], specifier = ">=1.35.0,<2" }, { name = "temporalio-mcp", marker = "extra == 'mcp'", specifier = ">=0.2.0,<0.3" }, { name = "typing-extensions", specifier = ">=4.2,<5" }, ] From b8a943e3ca2b77b7199b77f7a631669a01eb5b38 Mon Sep 17 00:00:00 2001 From: Francesc Campoy Date: Sat, 31 Oct 2026 11:11:52 -0700 Subject: [PATCH 5/7] Address review findings --- python/openai_agents/README.md | 6 +- .../openai_agents/_invoke_model_activity.py | 77 ++++++++++--------- .../tests/test_openai_streaming.py | 67 +++++++++++++--- 3 files changed, 102 insertions(+), 48 deletions(-) diff --git a/python/openai_agents/README.md b/python/openai_agents/README.md index 1903afa4..f4b6b84c 100644 --- a/python/openai_agents/README.md +++ b/python/openai_agents/README.md @@ -825,8 +825,10 @@ activity, and the run raises `InputGuardrailTripwireTriggered` as heartbeat, so set `heartbeat_timeout`: the activity then stops and publishes an `error` event with the code `cancelled` to the topic. Subscribers can still get the events the model sent before that, as a -native `stream_events()` consumer can. When the guardrail has already -tripped, the plugin does not start the model call. +native `stream_events()` consumer can. If the model has already finished +but the activity is still publishing its events when the cancel arrives, +the `error` event comes after `response.completed`. When the guardrail +has already tripped, the plugin does not start the model call. The workflow must still be open when the activity publishes that `error` event. With the default `cancellation_type` (`TRY_CANCEL`), the diff --git a/python/openai_agents/src/temporalio/openai_agents/_invoke_model_activity.py b/python/openai_agents/src/temporalio/openai_agents/_invoke_model_activity.py index c438dd7a..1fa81095 100644 --- a/python/openai_agents/src/temporalio/openai_agents/_invoke_model_activity.py +++ b/python/openai_agents/src/temporalio/openai_agents/_invoke_model_activity.py @@ -497,43 +497,46 @@ async def invoke_model_activity_streaming( # typed decode can pass result_type=TResponseStreamEvent on # their own subscribe call. events_topic = stream.topic(topic) - async with stream: - try: - async for event in model.stream_response( - system_instructions=input.get("system_instructions"), - input=input["input"], - model_settings=model_settings, - tools=tools, - output_schema=input.get("output_schema"), - handoffs=handoffs, - tracing=ModelTracing(input["tracing"]), - previous_response_id=input.get("previous_response_id"), - conversation_id=input.get("conversation_id"), - prompt=input.get("prompt"), - ): - # OpenAI models set defer_build=True, so an event's pydantic - # schema may still be an unbuilt placeholder. - type(event).model_rebuild() - events.append(event) - events_topic.publish(event) - except APIStatusError as e: - _raise_for_openai_status(e) - except asyncio.CancelledError: - # When the workflow cancelled the call, for example because a parallel - # input guardrail tripped, tell subscribers the response ends here. - # Leaving the context manager flushes it. Other cancels, such as a - # Worker shutdown or a heartbeat timeout, can be retried, so publish - # nothing and let the retry answer. - details = activity.cancellation_details() - if details is not None and details.cancel_requested: - events_topic.publish( - ResponseErrorEvent( - type="error", - code="cancelled", - message="The model call was cancelled.", - sequence_number=len(events), - ) + try: + async with stream: + try: + async for event in model.stream_response( + system_instructions=input.get("system_instructions"), + input=input["input"], + model_settings=model_settings, + tools=tools, + output_schema=input.get("output_schema"), + handoffs=handoffs, + tracing=ModelTracing(input["tracing"]), + previous_response_id=input.get("previous_response_id"), + conversation_id=input.get("conversation_id"), + prompt=input.get("prompt"), + ): + # OpenAI models set defer_build=True, so an event's pydantic + # schema may still be an unbuilt placeholder. + type(event).model_rebuild() + events.append(event) + events_topic.publish(event) + except APIStatusError as e: + _raise_for_openai_status(e) + except asyncio.CancelledError: + # When the workflow cancelled the call, for example because a parallel + # input guardrail tripped, tell subscribers the response ends here. The + # cancel can arrive while the model streams or while leaving the context + # manager flushes the events, so publish after both, behind the events + # still buffered. Other cancels, such as a Worker shutdown or a heartbeat + # timeout, can be retried, so publish nothing and let the retry answer. + details = activity.cancellation_details() + if details is not None and details.cancel_requested: + events_topic.publish( + ResponseErrorEvent( + type="error", + code="cancelled", + message="The model call was cancelled.", + sequence_number=len(events), ) - raise + ) + await stream.flush() + raise return events diff --git a/python/openai_agents/tests/test_openai_streaming.py b/python/openai_agents/tests/test_openai_streaming.py index d11c2596..5dcb97c3 100644 --- a/python/openai_agents/tests/test_openai_streaming.py +++ b/python/openai_agents/tests/test_openai_streaming.py @@ -777,15 +777,49 @@ async def test_streaming_input_guardrail_known_trip_skips_the_model( assert model.attempts == [] +def delay_stream_publishing(monkeypatch: pytest.MonkeyPatch, seconds: float) -> None: + """Hold every stream publish signal until ``seconds`` after the first one is sent.""" + signal = WorkflowHandle.signal + deadline: float | None = None + + async def delayed_signal( + self: WorkflowHandle[Any, Any], name: Any, *args: Any, **kwargs: Any + ) -> None: + nonlocal deadline + if name == "__temporal_workflow_stream_publish": + loop = asyncio.get_running_loop() + if deadline is None: + deadline = loop.time() + seconds + await asyncio.sleep(max(0.0, deadline - loop.time())) + await signal(self, name, *args, **kwargs) + + monkeypatch.setattr(WorkflowHandle, "signal", delayed_signal) + + @pytest.mark.asyncio @pytest.mark.parametrize("max_cached_workflows", [1000, 0], ids=["cached", "uncached"]) +@pytest.mark.parametrize( + "cancelled_while", ["streaming", "publishing"], ids=["streaming", "publishing"] +) async def test_streaming_input_guardrail_trip_waits_for_the_error_event( - client: Client, max_cached_workflows: int + client: Client, + monkeypatch: pytest.MonkeyPatch, + max_cached_workflows: int, + cancelled_while: str, ): """With ``WAIT_CANCELLATION_COMPLETED``, a workflow that returns right after the trip - still has the error event in its stream, and the Worker shuts down after the cancel, - with or without a Workflow cache.""" - model = HoldingStreamingModel() + still has the error event at the end of its stream, and the Worker shuts down after + the cancel, with or without a Workflow cache. That holds whether the cancel reaches + the activity while the model streams or after the model finished, while the activity + still publishes its events.""" + model: StreamingTestModel + if cancelled_while == "streaming": + model = HoldingStreamingModel() + else: + # The model finishes at once, but the topic gets nothing for 8 s, so the cancel + # arrives while the activity is still publishing. + model = StreamingTestModel() + delay_stream_publishing(monkeypatch, 8) async with AgentEnvironment( model=model, model_params=ModelActivityParameters( @@ -809,13 +843,28 @@ async def test_streaming_input_guardrail_trip_waits_for_the_error_event( task_queue=worker.task_queue, execution_timeout=timedelta(seconds=30), ) - # The model held after its first delta and was cancelled, so the stream - # holds that delta and the error event. - offset = await WorkflowStreamClient.create(client, workflow_id).get_offset() + stream = WorkflowStreamClient.create(client, workflow_id) + offset = await stream.get_offset() + last: TResponseStreamEvent | None = None + async for item in stream.subscribe( + ["events"], + from_offset=offset - 1, + result_type=TResponseStreamEvent, # type: ignore[arg-type,call-overload] + ): + last = item.data + break assert result == "refused" - assert model.cancelled - assert offset == 2 + assert isinstance(last, ResponseErrorEvent) + assert last.code == "cancelled" + if isinstance(model, HoldingStreamingModel): + # The model held after its first delta and was cancelled, so the stream holds + # that delta and the error event. + assert model.cancelled + assert offset == 2 + else: + # Two deltas, response.completed, and the error event. + assert offset == 4 @pytest.mark.asyncio From fb5db8ca6aad4826dab3b072958c42dbef72e86a Mon Sep 17 00:00:00 2001 From: Francesc Campoy Date: Sat, 31 Oct 2026 11:55:04 -0700 Subject: [PATCH 6/7] Address review findings --- .../openai_agents/_invoke_model_activity.py | 13 ++++- .../tests/test_openai_streaming.py | 50 ++++++++++++++++--- 2 files changed, 54 insertions(+), 9 deletions(-) diff --git a/python/openai_agents/src/temporalio/openai_agents/_invoke_model_activity.py b/python/openai_agents/src/temporalio/openai_agents/_invoke_model_activity.py index 1fa81095..1b0dd56f 100644 --- a/python/openai_agents/src/temporalio/openai_agents/_invoke_model_activity.py +++ b/python/openai_agents/src/temporalio/openai_agents/_invoke_model_activity.py @@ -401,6 +401,12 @@ def _raise_for_openai_status(e: APIStatusError) -> NoReturn: ) from e +def _cancel_requested() -> bool: + """Whether the workflow asked to cancel the running activity.""" + details = activity.cancellation_details() + return details is not None and details.cancel_requested + + def _default_openai_client() -> AsyncOpenAI: """Return the framework's default client as it is, or a client on its default key. @@ -519,6 +525,10 @@ async def invoke_model_activity_streaming( events_topic.publish(event) except APIStatusError as e: _raise_for_openai_status(e) + # Leaving the context manager swallows a cancel that arrives while it + # stops the stream's background flusher, so raise that cancel here. + if _cancel_requested(): + raise asyncio.CancelledError() except asyncio.CancelledError: # When the workflow cancelled the call, for example because a parallel # input guardrail tripped, tell subscribers the response ends here. The @@ -526,8 +536,7 @@ async def invoke_model_activity_streaming( # manager flushes the events, so publish after both, behind the events # still buffered. Other cancels, such as a Worker shutdown or a heartbeat # timeout, can be retried, so publish nothing and let the retry answer. - details = activity.cancellation_details() - if details is not None and details.cancel_requested: + if _cancel_requested(): events_topic.publish( ResponseErrorEvent( type="error", diff --git a/python/openai_agents/tests/test_openai_streaming.py b/python/openai_agents/tests/test_openai_streaming.py index 5dcb97c3..3cd24504 100644 --- a/python/openai_agents/tests/test_openai_streaming.py +++ b/python/openai_agents/tests/test_openai_streaming.py @@ -541,12 +541,13 @@ async def test_streamed_run_runs_sync_function_tools(client: Client): class HoldingStreamingModel(StreamingTestModel): """Streaming test model that sends its first delta, then, on the activity's first - attempt, waits up to 10 s before the rest. + attempt, waits up to ``hold`` seconds (10 by default) before the rest. Records the attempts and whether its stream was cancelled during the wait. """ - def __init__(self) -> None: + def __init__(self, hold: float = 10) -> None: + self.hold = hold self.cancelled = False self.attempts: list[int] = [] @@ -576,7 +577,7 @@ async def stream_response( yield await anext(events) if attempt == 1: try: - await asyncio.sleep(10) + await asyncio.sleep(self.hold) except asyncio.CancelledError: self.cancelled = True raise @@ -796,10 +797,36 @@ async def delayed_signal( monkeypatch.setattr(WorkflowHandle, "signal", delayed_signal) +def clean_up_first_publish_slowly( + monkeypatch: pytest.MonkeyPatch, seconds: float +) -> None: + """Hold the first stream publish signal until it is cancelled, then take ``seconds`` + to clean up before it raises.""" + signal = WorkflowHandle.signal + first = True + + async def slow_cleanup_signal( + self: WorkflowHandle[Any, Any], name: Any, *args: Any, **kwargs: Any + ) -> None: + nonlocal first + if name == "__temporal_workflow_stream_publish" and first: + first = False + try: + await asyncio.sleep(60) + except asyncio.CancelledError: + await asyncio.sleep(seconds) + raise + await signal(self, name, *args, **kwargs) + + monkeypatch.setattr(WorkflowHandle, "signal", slow_cleanup_signal) + + @pytest.mark.asyncio @pytest.mark.parametrize("max_cached_workflows", [1000, 0], ids=["cached", "uncached"]) @pytest.mark.parametrize( - "cancelled_while", ["streaming", "publishing"], ids=["streaming", "publishing"] + "cancelled_while", + ["streaming", "publishing", "stopping-the-flusher"], + ids=["streaming", "publishing", "stopping-the-flusher"], ) async def test_streaming_input_guardrail_trip_waits_for_the_error_event( client: Client, @@ -811,15 +838,21 @@ async def test_streaming_input_guardrail_trip_waits_for_the_error_event( still has the error event at the end of its stream, and the Worker shuts down after the cancel, with or without a Workflow cache. That holds whether the cancel reaches the activity while the model streams or after the model finished, while the activity - still publishes its events.""" + still publishes its events or stops the stream's background flusher.""" model: StreamingTestModel if cancelled_while == "streaming": model = HoldingStreamingModel() - else: + elif cancelled_while == "publishing": # The model finishes at once, but the topic gets nothing for 8 s, so the cancel # arrives while the activity is still publishing. model = StreamingTestModel() delay_stream_publishing(monkeypatch, 8) + else: + # The background flusher's first publish hangs. The model finishes after + # 0.3 s, and stopping the flusher takes 8 s, so the cancel arrives while the + # stream's context manager waits for the flusher. + model = HoldingStreamingModel(hold=0.3) + clean_up_first_publish_slowly(monkeypatch, 8) async with AgentEnvironment( model=model, model_params=ModelActivityParameters( @@ -857,13 +890,16 @@ async def test_streaming_input_guardrail_trip_waits_for_the_error_event( assert result == "refused" assert isinstance(last, ResponseErrorEvent) assert last.code == "cancelled" - if isinstance(model, HoldingStreamingModel): + if cancelled_while == "streaming": # The model held after its first delta and was cancelled, so the stream holds # that delta and the error event. + assert isinstance(model, HoldingStreamingModel) assert model.cancelled assert offset == 2 else: # Two deltas, response.completed, and the error event. + if isinstance(model, HoldingStreamingModel): + assert not model.cancelled assert offset == 4 From 9d4dd6ac959ebed6e51a2f916330729cc9fd2253 Mon Sep 17 00:00:00 2001 From: Francesc Campoy Date: Sat, 31 Oct 2026 12:15:32 -0700 Subject: [PATCH 7/7] Address review findings --- python/openai_agents/README.md | 3 +- .../openai_agents/_heartbeat_decorator.py | 47 +++++++----- .../openai_agents/_invoke_model_activity.py | 75 ++++++++++--------- .../tests/test_openai_streaming.py | 36 +++++++-- 4 files changed, 103 insertions(+), 58 deletions(-) diff --git a/python/openai_agents/README.md b/python/openai_agents/README.md index f4b6b84c..44b42260 100644 --- a/python/openai_agents/README.md +++ b/python/openai_agents/README.md @@ -827,7 +827,8 @@ publishes an `error` event with the code `cancelled` to the topic. Subscribers can still get the events the model sent before that, as a native `stream_events()` consumer can. If the model has already finished but the activity is still publishing its events when the cancel arrives, -the `error` event comes after `response.completed`. When the guardrail +the `error` event comes after `response.completed`. If the activity has +already returned, it publishes no `error` event. When the guardrail has already tripped, the plugin does not start the model call. The workflow must still be open when the activity publishes that diff --git a/python/openai_agents/src/temporalio/openai_agents/_heartbeat_decorator.py b/python/openai_agents/src/temporalio/openai_agents/_heartbeat_decorator.py index ad926718..2b3b6ae3 100644 --- a/python/openai_agents/src/temporalio/openai_agents/_heartbeat_decorator.py +++ b/python/openai_agents/src/temporalio/openai_agents/_heartbeat_decorator.py @@ -1,5 +1,6 @@ import asyncio -from collections.abc import Awaitable, Callable +from collections.abc import AsyncGenerator, Awaitable, Callable +from contextlib import asynccontextmanager from functools import wraps from typing import Any, TypeVar, cast, overload @@ -29,29 +30,41 @@ def auto_heartbeater( def decorate(fn: F) -> F: @wraps(fn) async def wrapper(*args: Any, **kwargs: Any) -> Any: - delay = interval - if delay is None: - heartbeat_timeout = activity.info().heartbeat_timeout - if heartbeat_timeout: - delay = heartbeat_timeout.total_seconds() / 2 - heartbeat_task = None - if delay: - heartbeat_task = asyncio.create_task(_heartbeat_every(delay)) - try: + async with heartbeating(interval): return await fn(*args, **kwargs) - finally: - if heartbeat_task: - heartbeat_task.cancel() - try: - await heartbeat_task - except asyncio.CancelledError: - pass return cast(F, wrapper) return decorate(fn) if fn is not None else decorate +@asynccontextmanager +async def heartbeating(interval: float | None = None) -> AsyncGenerator[None]: + """Heartbeat in the background while the block runs, as ``auto_heartbeater`` does. + + Stopping the heartbeats on exit awaits the heartbeat task and swallows a cancel + that arrives meanwhile. An activity that must act on every requested cancel checks + ``activity.cancellation_details()`` after the block. + """ + delay = interval + if delay is None: + heartbeat_timeout = activity.info().heartbeat_timeout + if heartbeat_timeout: + delay = heartbeat_timeout.total_seconds() / 2 + heartbeat_task = None + if delay: + heartbeat_task = asyncio.create_task(_heartbeat_every(delay)) + try: + yield + finally: + if heartbeat_task: + heartbeat_task.cancel() + try: + await heartbeat_task + except asyncio.CancelledError: + pass + + async def _heartbeat_every(delay: float) -> None: while True: await asyncio.sleep(delay) diff --git a/python/openai_agents/src/temporalio/openai_agents/_invoke_model_activity.py b/python/openai_agents/src/temporalio/openai_agents/_invoke_model_activity.py index 1b0dd56f..57e83b71 100644 --- a/python/openai_agents/src/temporalio/openai_agents/_invoke_model_activity.py +++ b/python/openai_agents/src/temporalio/openai_agents/_invoke_model_activity.py @@ -52,7 +52,10 @@ from temporalio import activity from temporalio.contrib.workflow_streams import WorkflowStreamClient from temporalio.exceptions import ApplicationError -from temporalio.openai_agents._heartbeat_decorator import auto_heartbeater +from temporalio.openai_agents._heartbeat_decorator import ( + auto_heartbeater, + heartbeating, +) from temporalio.openai_agents._temporal_worker_env_ref import ( AllowAllWorkerEnvVars, _WorkerEnvRefResolver, @@ -460,7 +463,6 @@ async def invoke_model_activity(self, input: ActivityModelInput) -> ModelRespons _raise_for_openai_status(e) @activity.defn - @auto_heartbeater async def invoke_model_activity_streaming( self, input: StreamingActivityModelInput ) -> list[TResponseStreamEvent]: @@ -480,7 +482,7 @@ async def invoke_model_activity_streaming( ``streaming_topic`` so external consumers (UIs, tracing, etc.) can observe events as they arrive. - Heartbeats run on a background task via ``auto_heartbeater`` so + Heartbeats run on a background task via ``heartbeating`` so long initial-token latency or long pauses between chunks do not trip ``heartbeat_timeout``. """ @@ -504,48 +506,51 @@ async def invoke_model_activity_streaming( # their own subscribe call. events_topic = stream.topic(topic) try: - async with stream: - try: - async for event in model.stream_response( - system_instructions=input.get("system_instructions"), - input=input["input"], - model_settings=model_settings, - tools=tools, - output_schema=input.get("output_schema"), - handoffs=handoffs, - tracing=ModelTracing(input["tracing"]), - previous_response_id=input.get("previous_response_id"), - conversation_id=input.get("conversation_id"), - prompt=input.get("prompt"), - ): - # OpenAI models set defer_build=True, so an event's pydantic - # schema may still be an unbuilt placeholder. - type(event).model_rebuild() - events.append(event) - events_topic.publish(event) - except APIStatusError as e: - _raise_for_openai_status(e) - # Leaving the context manager swallows a cancel that arrives while it - # stops the stream's background flusher, so raise that cancel here. + async with heartbeating(): + async with stream: + try: + async for event in model.stream_response( + system_instructions=input.get("system_instructions"), + input=input["input"], + model_settings=model_settings, + tools=tools, + output_schema=input.get("output_schema"), + handoffs=handoffs, + tracing=ModelTracing(input["tracing"]), + previous_response_id=input.get("previous_response_id"), + conversation_id=input.get("conversation_id"), + prompt=input.get("prompt"), + ): + # OpenAI models set defer_build=True, so an event's pydantic + # schema may still be an unbuilt placeholder. + type(event).model_rebuild() + events.append(event) + events_topic.publish(event) + except APIStatusError as e: + _raise_for_openai_status(e) + # Leaving the context managers swallows a cancel that arrives while they + # stop the stream's background flusher or the heartbeats, so raise that + # cancel here. if _cancel_requested(): raise asyncio.CancelledError() except asyncio.CancelledError: # When the workflow cancelled the call, for example because a parallel # input guardrail tripped, tell subscribers the response ends here. The # cancel can arrive while the model streams or while leaving the context - # manager flushes the events, so publish after both, behind the events + # managers flushes the events, so publish after both, behind the events # still buffered. Other cancels, such as a Worker shutdown or a heartbeat # timeout, can be retried, so publish nothing and let the retry answer. if _cancel_requested(): - events_topic.publish( - ResponseErrorEvent( - type="error", - code="cancelled", - message="The model call was cancelled.", - sequence_number=len(events), + async with heartbeating(): + events_topic.publish( + ResponseErrorEvent( + type="error", + code="cancelled", + message="The model call was cancelled.", + sequence_number=len(events), + ) ) - ) - await stream.flush() + await stream.flush() raise return events diff --git a/python/openai_agents/tests/test_openai_streaming.py b/python/openai_agents/tests/test_openai_streaming.py index 3cd24504..0369c16d 100644 --- a/python/openai_agents/tests/test_openai_streaming.py +++ b/python/openai_agents/tests/test_openai_streaming.py @@ -60,7 +60,7 @@ from temporalio import activity, workflow from temporalio.client import Client, WorkflowFailureError, WorkflowHandle from temporalio.contrib.workflow_streams import WorkflowStream, WorkflowStreamClient -from temporalio.openai_agents import ModelActivityParameters +from temporalio.openai_agents import ModelActivityParameters, _heartbeat_decorator from temporalio.openai_agents.testing import AgentEnvironment from temporalio.workflow import ActivityCancellationType from tests.helpers import assert_eventually, new_worker @@ -821,12 +821,31 @@ async def slow_cleanup_signal( monkeypatch.setattr(WorkflowHandle, "signal", slow_cleanup_signal) +def stop_heartbeats_slowly(monkeypatch: pytest.MonkeyPatch, seconds: float) -> None: + """Make stopping the activity's heartbeats take ``seconds``, still heartbeating + meanwhile so that a cancel reaches the activity.""" + heartbeat_every = _heartbeat_decorator._heartbeat_every + + async def slow_to_stop(delay: float) -> None: + try: + await heartbeat_every(delay) + except asyncio.CancelledError: + loop = asyncio.get_running_loop() + deadline = loop.time() + seconds + while loop.time() < deadline: + activity.heartbeat() + await asyncio.sleep(0.5) + raise + + monkeypatch.setattr(_heartbeat_decorator, "_heartbeat_every", slow_to_stop) + + @pytest.mark.asyncio @pytest.mark.parametrize("max_cached_workflows", [1000, 0], ids=["cached", "uncached"]) @pytest.mark.parametrize( "cancelled_while", - ["streaming", "publishing", "stopping-the-flusher"], - ids=["streaming", "publishing", "stopping-the-flusher"], + ["streaming", "publishing", "stopping-the-flusher", "stopping-the-heartbeats"], + ids=["streaming", "publishing", "stopping-the-flusher", "stopping-the-heartbeats"], ) async def test_streaming_input_guardrail_trip_waits_for_the_error_event( client: Client, @@ -838,7 +857,8 @@ async def test_streaming_input_guardrail_trip_waits_for_the_error_event( still has the error event at the end of its stream, and the Worker shuts down after the cancel, with or without a Workflow cache. That holds whether the cancel reaches the activity while the model streams or after the model finished, while the activity - still publishes its events or stops the stream's background flusher.""" + still publishes its events, stops the stream's background flusher or stops its + heartbeats.""" model: StreamingTestModel if cancelled_while == "streaming": model = HoldingStreamingModel() @@ -847,12 +867,18 @@ async def test_streaming_input_guardrail_trip_waits_for_the_error_event( # arrives while the activity is still publishing. model = StreamingTestModel() delay_stream_publishing(monkeypatch, 8) - else: + elif cancelled_while == "stopping-the-flusher": # The background flusher's first publish hangs. The model finishes after # 0.3 s, and stopping the flusher takes 8 s, so the cancel arrives while the # stream's context manager waits for the flusher. model = HoldingStreamingModel(hold=0.3) clean_up_first_publish_slowly(monkeypatch, 8) + else: + # The model finishes after 0.3 s and its events are published, but stopping + # the heartbeats takes 8 s, so the cancel arrives while the activity waits for + # its heartbeat task. + model = HoldingStreamingModel(hold=0.3) + stop_heartbeats_slowly(monkeypatch, 8) async with AgentEnvironment( model=model, model_params=ModelActivityParameters(