From 45e7dac87f84653cea98667258bc7f0dd527a611 Mon Sep 17 00:00:00 2001 From: Brian Strauch Date: Tue, 15 Sep 2026 15:19:01 -0700 Subject: [PATCH 01/16] Remove OpenAI Agents integration --- .github/CODEOWNERS | 2 - .github/workflows/ci.yml | 6 - CHANGELOG.md | 6 + README.md | 2 - pyproject.toml | 4 - temporalio/contrib/google_adk_agents/_mcp.py | 5 +- temporalio/contrib/openai_agents/README.md | 1107 ------ temporalio/contrib/openai_agents/__init__.py | 43 - temporalio/contrib/openai_agents/_errors.py | 11 - .../openai_agents/_heartbeat_decorator.py | 38 - .../openai_agents/_invoke_model_activity.py | 447 --- temporalio/contrib/openai_agents/_mcp.py | 539 --- .../openai_agents/_model_parameters.py | 96 - .../contrib/openai_agents/_openai_runner.py | 381 --- .../openai_agents/_otel_trace_interceptor.py | 88 - .../openai_agents/_temporal_model_stub.py | 296 -- .../openai_agents/_temporal_openai_agents.py | 482 --- .../openai_agents/_temporal_trace_provider.py | 211 -- .../openai_agents/_temporal_worker_env_ref.py | 177 - .../openai_agents/_trace_interceptor.py | 445 --- .../contrib/openai_agents/sandbox/__init__.py | 1 - .../sandbox/_sandbox_client_provider.py | 304 -- .../sandbox/_temporal_activity_models.py | 218 -- .../sandbox/_temporal_sandbox_client.py | 140 - .../sandbox/_temporal_sandbox_session.py | 239 -- .../sandbox/_temporal_worker_env_value.py | 88 - temporalio/contrib/openai_agents/testing.py | 277 -- temporalio/contrib/openai_agents/workflow.py | 382 --- .../agents-as-tools-workflow-history.json | 651 ---- .../customer-service-workflow-history.json | 1356 -------- .../histories/hello-workflow-history.json | 239 -- .../input-guardrail-workflow-history.json | 577 ---- .../output-guardrail-workflow-history.json | 238 -- .../histories/research-workflow-history.json | 1755 ---------- .../histories/tools-workflow-history.json | 506 --- .../research_agents/planner_agent.py | 29 - .../openai_agents/research_agents/printer.py | 45 - .../research_agents/research_manager.py | 73 - .../research_agents/search_agent.py | 20 - .../research_agents/writer_agent.py | 33 - tests/contrib/openai_agents/test_openai.py | 2999 ----------------- .../openai_agents/test_openai_replay.py | 58 - .../openai_agents/test_openai_sandbox.py | 1575 --------- .../test_openai_sandbox_worker_env_value.py | 295 -- .../openai_agents/test_openai_streaming.py | 350 -- .../openai_agents/test_openai_tool_secrets.py | 908 ----- .../openai_agents/test_openai_tracing.py | 955 ------ uv.lock | 118 +- 48 files changed, 36 insertions(+), 18779 deletions(-) delete mode 100644 temporalio/contrib/openai_agents/README.md delete mode 100644 temporalio/contrib/openai_agents/__init__.py delete mode 100644 temporalio/contrib/openai_agents/_errors.py delete mode 100644 temporalio/contrib/openai_agents/_heartbeat_decorator.py delete mode 100644 temporalio/contrib/openai_agents/_invoke_model_activity.py delete mode 100644 temporalio/contrib/openai_agents/_mcp.py delete mode 100644 temporalio/contrib/openai_agents/_model_parameters.py delete mode 100644 temporalio/contrib/openai_agents/_openai_runner.py delete mode 100644 temporalio/contrib/openai_agents/_otel_trace_interceptor.py delete mode 100644 temporalio/contrib/openai_agents/_temporal_model_stub.py delete mode 100644 temporalio/contrib/openai_agents/_temporal_openai_agents.py delete mode 100644 temporalio/contrib/openai_agents/_temporal_trace_provider.py delete mode 100644 temporalio/contrib/openai_agents/_temporal_worker_env_ref.py delete mode 100644 temporalio/contrib/openai_agents/_trace_interceptor.py delete mode 100644 temporalio/contrib/openai_agents/sandbox/__init__.py delete mode 100644 temporalio/contrib/openai_agents/sandbox/_sandbox_client_provider.py delete mode 100644 temporalio/contrib/openai_agents/sandbox/_temporal_activity_models.py delete mode 100644 temporalio/contrib/openai_agents/sandbox/_temporal_sandbox_client.py delete mode 100644 temporalio/contrib/openai_agents/sandbox/_temporal_sandbox_session.py delete mode 100644 temporalio/contrib/openai_agents/sandbox/_temporal_worker_env_value.py delete mode 100644 temporalio/contrib/openai_agents/testing.py delete mode 100644 temporalio/contrib/openai_agents/workflow.py delete mode 100644 tests/contrib/openai_agents/histories/agents-as-tools-workflow-history.json delete mode 100644 tests/contrib/openai_agents/histories/customer-service-workflow-history.json delete mode 100644 tests/contrib/openai_agents/histories/hello-workflow-history.json delete mode 100644 tests/contrib/openai_agents/histories/input-guardrail-workflow-history.json delete mode 100644 tests/contrib/openai_agents/histories/output-guardrail-workflow-history.json delete mode 100644 tests/contrib/openai_agents/histories/research-workflow-history.json delete mode 100644 tests/contrib/openai_agents/histories/tools-workflow-history.json delete mode 100644 tests/contrib/openai_agents/research_agents/planner_agent.py delete mode 100644 tests/contrib/openai_agents/research_agents/printer.py delete mode 100644 tests/contrib/openai_agents/research_agents/research_manager.py delete mode 100644 tests/contrib/openai_agents/research_agents/search_agent.py delete mode 100644 tests/contrib/openai_agents/research_agents/writer_agent.py delete mode 100644 tests/contrib/openai_agents/test_openai.py delete mode 100644 tests/contrib/openai_agents/test_openai_replay.py delete mode 100644 tests/contrib/openai_agents/test_openai_sandbox.py delete mode 100644 tests/contrib/openai_agents/test_openai_sandbox_worker_env_value.py delete mode 100644 tests/contrib/openai_agents/test_openai_streaming.py delete mode 100644 tests/contrib/openai_agents/test_openai_tool_secrets.py delete mode 100644 tests/contrib/openai_agents/test_openai_tracing.py diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 7a5c2ab46..e3dcbd9f7 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -15,7 +15,6 @@ /temporalio/contrib/google_genai/ @temporalio/ai-sdk @temporalio/sdk /temporalio/contrib/langgraph/ @temporalio/ai-sdk @temporalio/sdk /temporalio/contrib/langsmith/ @temporalio/ai-sdk @temporalio/sdk -/temporalio/contrib/openai_agents/ @temporalio/ai-sdk @temporalio/sdk /temporalio/contrib/strands/ @temporalio/ai-sdk @temporalio/sdk /temporalio/contrib/workflow_streams/ @temporalio/ai-sdk @temporalio/sdk /tests/contrib/deepagents/ @temporalio/ai-sdk @temporalio/sdk @@ -23,6 +22,5 @@ /tests/contrib/google_genai/ @temporalio/ai-sdk @temporalio/sdk /tests/contrib/langgraph/ @temporalio/ai-sdk @temporalio/sdk /tests/contrib/langsmith/ @temporalio/ai-sdk @temporalio/sdk -/tests/contrib/openai_agents/ @temporalio/ai-sdk @temporalio/sdk /tests/contrib/strands/ @temporalio/ai-sdk @temporalio/sdk /tests/contrib/workflow_streams/ @temporalio/ai-sdk @temporalio/sdk diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f5393b0ef..3105c764b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,7 +26,6 @@ jobs: - os: ubuntu-latest python: "3.14" docsTarget: true - openaiTestTarget: true clippyLinter: true - python: "3.10" pytestExtraArgs: '--reruns 3 --only-rerun "RuntimeError: Failed validating workflow"' @@ -68,11 +67,6 @@ jobs: - if: ${{ !endsWith(matrix.os, '-arm') }} run: poe test ${{matrix.pytestExtraArgs}} -s --workflow-environment time-skipping --junit-xml=junit-xml/${{ matrix.python }}--${{ matrix.os }}--time-skipping.xml timeout-minutes: 10 - - if: ${{ matrix.openaiTestTarget && (github.event.pull_request.head.repo.full_name == '' || github.event.pull_request.head.repo.full_name == 'temporalio/sdk-python') }} - run: poe test tests/contrib/openai_agents/test_openai.py ${{matrix.pytestExtraArgs}} -s --junit-xml=junit-xml/${{ matrix.python }}--${{ matrix.os }}--openai.xml - timeout-minutes: 10 - env: - OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - name: "Upload junit-xml artifacts" uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 if: always() diff --git a/CHANGELOG.md b/CHANGELOG.md index 87dafcbe2..91b3e9b14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,12 @@ to include examples, links to docs, or any other relevant information. ### :boom: Breaking Changes +- The OpenAI Agents integration has moved to the independently versioned + [`temporalio-openai-agents`](https://pypi.org/project/temporalio-openai-agents/) + package. Install it with `uv add temporalio-openai-agents`; the + `temporalio[openai-agents]` extra and bundled `temporalio.contrib.openai_agents` + module have been removed. + ### Fixed ### Security diff --git a/README.md b/README.md index fc1faf83b..470c87caa 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,6 @@ [![PyPI](https://img.shields.io/pypi/v/temporalio.svg?style=for-the-badge)](https://pypi.org/project/temporalio) [![MIT](https://img.shields.io/pypi/l/temporalio.svg?style=for-the-badge)](LICENSE) -**📣 News: Integration between OpenAI Agents SDK and Temporal is now in public preview. [Learn more](temporalio/contrib/openai_agents/README.md).** - [Temporal](https://temporal.io/) is a distributed, scalable, durable, and highly available orchestration engine used to execute asynchronous, long-running business logic in a scalable and resilient way. diff --git a/pyproject.toml b/pyproject.toml index b7077785f..779c9afb9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,6 @@ classifiers = [ grpc = ["grpcio>=1.48.2,<2"] opentelemetry = ["opentelemetry-api>=1.26,<2", "opentelemetry-sdk>=1.26,<2"] pydantic = ["pydantic>=2.0.0,<3"] -openai-agents = ["openai-agents>=0.19.2,<0.20", "mcp>=1.9.4, <2"] google-adk = ["google-adk>=2.2.0,<3", "mcp>=1.24,<2"] langgraph = ["langgraph>=1.1.0"] langsmith = ["langsmith>=0.7.34,<0.9"] @@ -83,12 +82,9 @@ dev = [ "toml>=0.10.2,<0.11", "twine>=4.0.1,<5", "maturin>=1.8.2", - "openinference-instrumentation-openai-agents>=0.1.0", "pytest-cov>=6.1.1", "httpx>=0.28.1", "pytest-pretty>=1.3.0", - "openai-agents>=0.19.2,<0.20; python_version >= '3.14'", - "openai-agents[litellm]>=0.19.2,<0.20; python_version < '3.14'", "litellm>=1.83.0", "openinference-instrumentation-google-adk>=0.1.11", "googleapis-common-protos>=1.75.0,<2", diff --git a/temporalio/contrib/google_adk_agents/_mcp.py b/temporalio/contrib/google_adk_agents/_mcp.py index b342c9c3c..60d8adc8f 100644 --- a/temporalio/contrib/google_adk_agents/_mcp.py +++ b/temporalio/contrib/google_adk_agents/_mcp.py @@ -333,9 +333,8 @@ def _handle_worker_failure(func: Callable) -> Callable: state, so we re-raise as an ``ApplicationError`` of type ``"DedicatedWorkerFailure"`` for the caller to handle. - Duplicated (rather than shared) from ``openai_agents._mcp`` on purpose: - these two contribs do not currently share internal code, and importing - across them would create an unwanted dependency. + Kept local rather than shared with the separately distributed OpenAI Agents + integration so the two packages do not depend on each other's internals. """ @functools.wraps(func) diff --git a/temporalio/contrib/openai_agents/README.md b/temporalio/contrib/openai_agents/README.md deleted file mode 100644 index 38cc7146a..000000000 --- a/temporalio/contrib/openai_agents/README.md +++ /dev/null @@ -1,1107 +0,0 @@ -# OpenAI Agents SDK Integration for Temporal - -We welcome questions and feedback in the [#python-sdk](https://temporalio.slack.com/archives/CTT84RS0P) Slack channel at [temporalio.slack.com](https://temporalio.slack.com/). - -## Introduction - -This integration combines [OpenAI Agents SDK](https://github.com/openai/openai-agents-python) with [Temporal's durable execution](https://docs.temporal.io/evaluate/understanding-temporal#durable-execution). -It allows you to build durable agents that never lose their progress and handle long-running, asynchronous, and human-in-the-loop workflows with production-grade reliability. - -Temporal and OpenAI Agents SDK are complementary technologies, both of which contribute to simplifying what it takes to build highly capable, high-quality AI systems. -Temporal provides a crash-proof system foundation, taking care of the distributed systems challenges inherent to production agentic systems. -OpenAI Agents SDK offers a lightweight yet powerful framework for defining those agents. - -This document is organized as follows: - -- **[Hello World Durable Agent](#hello-world-durable-agent).** Your first durable agent example. -- **[Background Concepts](#core-concepts).** Background on durable execution and AI agents. -- **[Full Example](#full-example)** Running the Hello World Durable Agent example. -- **[Tool Calling](#tool-calling).** Calling agent Tools in Temporal. -- **[Sandbox Support](#sandbox-support).** Running sandbox agents in Temporal. -- **[Feature Support](#feature-support).** Compatibility matrix. - -The [samples repository](https://github.com/temporalio/samples-python/tree/main/openai_agents) contains examples including basic usage, common agent patterns, and more complete samples. - -## Hello World Durable Agent - -The code below shows how to wrap an agent for durable execution. - -### File 1: Durable Agent (`hello_world.py`) - -```python -from temporalio import workflow -from agents import Agent, Runner - -@workflow.defn -class HelloWorldAgent: - @workflow.run - async def run(self, prompt: str) -> str: - agent = Agent( - name="Assistant", - instructions="You only respond in haikus.", - ) - - result = await Runner.run(agent, input=prompt) - return result.final_output -``` - -In this example, Temporal provides the durable execution wrapper: the `HelloWorldAgent.run` method. -The content of that method, is regular OpenAI Agents SDK code. - -If you are familiar with Temporal and with Open AI Agents SDK, this code will look very familiar. -The `@workflow.defn` annotation on the `HelloWorldAgent` indicates that this class will contain durable execution logic. The `@workflow.run` annotation defines the entry point. -We use the `Agent` class from OpenAI Agents SDK to define a simple agent, instructing it to always respond with haikus. -We then run that agent, using the `Runner` class from OpenAI Agents SDK, passing through `prompt` as an argument. - -We will [complete this example below](#full-example). -Before digging further into the code, we will review some background that will make it easier to understand. - -## Background Concepts - -We encourage you to review this section thoroughly to gain a solid understanding of AI agents and durable execution with Temporal. -This knowledge will make it easier to design and build durable agents. -If you are already well versed in these topics, feel free to skim this section or skip ahead. - -### AI Agents - -In the OpenAI Agents SDK, an agent is an AI model configured with instructions, tools, MCP servers, guardrails, handoffs, context, and more. - -We describe each of these briefly: - -- _AI model_. An LLM such as OpenAI's GPT, Google's Gemini, or one of many others. -- _Instructions_. Also known as a system prompt, the instructions contain the initial input to the model, which configures it for the job it will do. -- _Tools_. Typically, Python functions that the model may choose to invoke. Tools are functions with text-descriptions that explain their functionality to the model. -- _MCP servers_. Best known for providing tools, MCP offers a pluggable standard for interoperability, including file-like resources, prompt templates, and human approvals. MCP servers may be accessed over the network or run in a local process. -- _Guardrails_. Checks on the input or the output of an agent to ensure compliance or safety. Guardrails may be implemented as regular code or as AI agents. -- _Handoffs_. A handoff occurs when an agent delegates a task to another agent. During a handoff the conversation history remains the same, and passes to a new agent with its own model, instructions, tools. -- _Context_. This is an overloaded term. Here, context refers to a framework object that is shared across tools and other code, but is not passed to the model. - -Now, let's see how these components work together. -In a common pattern, the model first receives user input and then reasons about which tool to invoke. -The tool's response is passed back to the model, which may call additional tools, repeating this loop until the task is complete. - -The diagram below illustrates this flow. - -```text - +-------------------+ - | User Input | - +-------------------+ - | - v - +---------------------+ - | Reasoning (Model) | <--+ - +---------------------+ | - | | - (decides which action) | - v | - +---------------------+ | - | Action | | - | (e.g., use a Tool) | | - +---------------------+ | - | | - v | - +---------------------+ | - | Observation | | - | (Tool Output) | | - +---------------------+ | - | | - +----------------+ - (loop: uses new info to reason - again, until task is complete) -``` - -Even in a simple example like this, there are many places where things can go wrong. -Tools call APIs that sometimes fail, while models can encounter rate limits, requiring retries. -The longer the agent runs, the more costly it is to start the job over. -We next describe durable execution, which handles such failures seamlessly. - -### Durable Execution - -In Temporal's durable execution implementation, a program that crashes or encounters an exception while interacting with a model or API will retry until it can successfully complete. - -Temporal relies primarily on a replay mechanism to recover from failures. -As the program makes progress, Temporal saves key inputs and decisions, allowing a re-started program to pick up right where it left off. - -The key to making this work is to separate the applications repeatable (deterministic) and non-repeatable (non-deterministic) parts: - -1. Deterministic pieces, termed _workflows_, execute the same way when re-run with the same inputs. -2. Non-deterministic pieces, termed _activities_, can run arbitrary code, performing I/O and any other operations. - -Workflow code can run for extended periods and, if interrupted, resume exactly where it left off. -Activity code faces no restrictions on I/O or external interactions, but if it fails part-way through it restarts from the beginning. - -In this integration, model invocations are automatically routed through Temporal activities, while the logic that coordinates them lives in the workflow. -Tools that perform I/O or other non-deterministic work should run as Temporal activities, while deterministic, workflow-safe tools can run directly in the workflow. -This pattern generalizes to more sophisticated agents. -We refer to the coordinating logic as _agent orchestration_. - -The diagram below shows the overall architecture of an agentic application in Temporal. -The Temporal Server is responsible to tracking program execution and making sure associated state is preserved reliably (i.e., stored to a database, possibly replicated across cloud regions). -Temporal Server manages data in encrypted form, so all data processing occurs on the Worker, which runs the workflow and activities. - -```text - +---------------------+ - | Temporal Server | (Stores workflow state, - +---------------------+ schedules activities, - ^ persists progress) - | - Save state, | Schedule Tasks, - progress, | load state on resume - timeouts | - | -+------------------------------------------------------+ -| Worker | -| +----------------------------------------------+ | -| | Workflow Code | | -| | (Agent orchestration + deterministic tools) | | -| +----------------------------------------------+ | -| | | | | -| v v v | -| +-----------+ +-----------+ +-------------+ | -| | Activity | | Activity | | Activity | | -| | (I/O Tool | | (I/O Tool | | (Model API) | | -| | 1) | | 2) | | | | -| +-----------+ +-----------+ +-------------+ | -| | | | | -+------------------------------------------------------+ - | | | - v v v - [External APIs, services, databases, etc.] -``` - -See the [Temporal documentation](https://docs.temporal.io/evaluate/understanding-temporal#temporal-application-the-building-blocks) for more information. - -## Complete Example - -To make the [Hello World durable agent](#hello-world-durable-agent) shown earlier available in Temporal, we need to create a worker program. -To see it run, we also need a client to launch it. -We show these files below. - -### File 2: Launch Worker (`run_worker.py`) - -```python -# File: run_worker.py - -import asyncio -from datetime import timedelta - -from temporalio.client import Client -from temporalio.contrib.openai_agents import OpenAIAgentsPlugin, ModelActivityParameters -from temporalio.worker import Worker - -from hello_world_workflow import HelloWorldAgent - - -async def worker_main(): - # Use the plugin to configure Temporal for use with OpenAI Agents SDK - client = await Client.connect( - "localhost:7233", - plugins=[ - OpenAIAgentsPlugin( - model_params=ModelActivityParameters( - start_to_close_timeout=timedelta(seconds=30) - ) - ), - ], - ) - - worker = Worker( - client, - task_queue="my-task-queue", - workflows=[HelloWorldAgent], - ) - await worker.run() - - -if __name__ == "__main__": - asyncio.run(worker_main()) -``` - -We use the `OpenAIAgentsPlugin` to configure Temporal for use with OpenAI Agents SDK. -The plugin automatically handles several important setup tasks: - -- Ensures proper serialization of Pydantic types -- Propagates context for [OpenAI Agents tracing](https://openai.github.io/openai-agents-python/tracing/). -- Registers an activity for invoking model calls with the Temporal worker. -- Configures OpenAI Agents SDK to run model calls as Temporal activities. - -### File 3: Client Execution (`run_hello_world_workflow.py`) - -```python -# File: run_hello_world_workflow.py - -import asyncio - -from temporalio.client import Client -from temporalio.common import WorkflowIDReusePolicy -from temporalio.contrib.openai_agents import OpenAIAgentsPlugin - -from hello_world_workflow import HelloWorldAgent - -async def main(): - # Create client connected to server at the given address - client = await Client.connect( - "localhost:7233", - plugins=[OpenAIAgentsPlugin()], - ) - - # Execute a workflow - result = await client.execute_workflow( - HelloWorldAgent.run, - "Tell me about recursion in programming.", - id="my-workflow-id", - task_queue="my-task-queue", - id_reuse_policy=WorkflowIDReusePolicy.ALLOW_DUPLICATE, - id_conflict_policy=WorkflowIDConflictPolicy.TERMINATE_EXISTING, - ) - print(f"Result: {result}") - -if __name__ == "__main__": - asyncio.run(main()) -``` - -This file is a standard Temporal launch script. -We also configure the client with the `OpenAIAgentsPlugin` to ensure serialization is compatible with the worker. - -To run this example, see the detailed instructions in the [Temporal Python Samples Repository](https://github.com/temporalio/samples-python/tree/main/openai_agents). - -## Tool Calling - -Model invocations are automatically routed through Temporal activities. -OpenAI-hosted tools are passed through the model invocation and executed by the model provider. -User-defined `FunctionTool`s, including tools created with `@function_tool`, are not automatically converted into Temporal activities; they execute in the workflow unless explicitly backed by a Temporal activity. -Where a tool executes depends on how it is defined: - -| Tool | Execution | Use for | -| --- | --- | --- | -| `activity_as_tool()` | Temporal activity | External I/O and non-deterministic operations | -| `FunctionTool` / `@function_tool` | Workflow | Deterministic, workflow-safe computation | -| OpenAI-hosted tool | Model provider | Provider-hosted features executed as part of the model invocation | - -### Temporal Activities as OpenAI Agents Tools - -One of the powerful features of this integration is the ability to convert Temporal activities into agent tools using `activity_as_tool`. -This allows your agent to leverage Temporal's durable execution for tool calls. -`activity_as_tool()` creates an OpenAI Agents `FunctionTool` whose invocation schedules the underlying Temporal activity. - -In the example below, we apply the `@activity.defn` decorator to the `get_weather` function to create a Temporal activity. -We then pass this through the `activity_as_tool` helper function to create an OpenAI Agents tool that is passed to the `Agent`. - -```python -from dataclasses import dataclass -from datetime import timedelta -from temporalio import activity, workflow -from temporalio.contrib import openai_agents -from agents import Agent, Runner - -@dataclass -class Weather: - city: str - temperature_range: str - conditions: str - -@activity.defn -async def get_weather(city: str) -> Weather: - """Get the weather for a given city.""" - return Weather(city=city, temperature_range="14-20C", conditions="Sunny with wind.") - -@workflow.defn -class WeatherAgent: - @workflow.run - async def run(self, question: str) -> str: - agent = Agent( - name="Weather Assistant", - instructions="You are a helpful weather agent.", - tools=[ - openai_agents.workflow.activity_as_tool( - get_weather, - start_to_close_timeout=timedelta(seconds=10) - ) - ], - ) - result = await Runner.run(starting_agent=agent, input=question) - return result.final_output -``` - -The activity must also be registered with a Worker. -`activity_as_tool()` controls how the Agent invokes the activity; it does not register the activity with the Worker. - -```python -from temporalio.worker import Worker - -worker = Worker( - client, - task_queue="my-task-queue", - workflows=[WeatherAgent], - activities=[get_weather], -) -``` - -### Calling OpenAI Agents Tools inside Temporal Workflows - -For simple computations that don't involve external calls, you can call the tool directly from the workflow by using the standard OpenAI Agents SDK `@function_tool` decorator. - -```python -from temporalio import workflow -from agents import Agent, Runner -from agents import function_tool - -@function_tool -def calculate_circle_area(radius: float) -> float: - """Calculate the area of a circle given its radius.""" - import math - return math.pi * radius ** 2 - -@workflow.defn -class MathAssistantAgent: - @workflow.run - async def run(self, message: str) -> str: - agent = Agent( - name="Math Assistant", - instructions="You are a helpful math assistant. Use the available tools to help with calculations.", - tools=[calculate_circle_area], - ) - result = await Runner.run(agent, input=message) - return result.final_output -``` - -Use regular `@function_tool` tools only for deterministic, workflow-safe logic. -Do not perform network, database, filesystem, or other external I/O directly from these tools. -Use a Temporal activity with `activity_as_tool()` instead. -Code running in the workflow can also invoke a Temporal activity directly when needed. - -Tools that run in the workflow can also update OpenAI Agents context, which is read-only for tools run as Temporal activities. - -## MCP Support - -This integration provides support for Model Context Protocol (MCP) servers through two wrapper approaches designed to handle different implications of failures. - -While Temporal provides durable execution for your workflows, this durability does not extend to MCP servers, which operate independently of the workflow and must provide their own durability. The integration handles this by offering stateless and stateful wrappers that you can choose based on your MCP server's design. - -### Stateless vs Stateful MCP Servers - -You need to understand your MCP server's behavior to choose the correct wrapper: - -**Stateless MCP servers** treat each operation independently. For example, a weather server with a `get_weather(location)` tool is stateless because each call is self-contained and includes all necessary information. These servers can be safely restarted or reconnected to without changing their behavior. - -**Stateful MCP servers** maintain session state between calls. For example, a weather server that requires calling `set_location(location)` followed by `get_weather()` is stateful because it remembers the configured location and uses it for subsequent calls. If the session or the server is restarted, state crucial for operation is lost. Temporal identifies such failures and raises an `ApplicationError` to signal the need for application-level failure handling. - -### Usage Example (Stateless MCP) - -The code below gives an example of using a stateless MCP server. - -#### Worker Configuration - -```python -import asyncio -from datetime import timedelta -from agents.mcp import MCPServerStdio -from temporalio.client import Client -from temporalio.contrib.openai_agents import ( - ModelActivityParameters, - OpenAIAgentsPlugin, - StatelessMCPServerProvider, -) -from temporalio.worker import Worker - - -async def main(): - # Create the MCP server provider - filesystem_server = StatelessMCPServerProvider( - lambda: MCPServerStdio( - name="FileSystemServer", - params={ - "command": "npx", - "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/files"], - }, - ) - ) - - # Register the MCP server with the OpenAI Agents plugin - client = await Client.connect( - "localhost:7233", - plugins=[ - OpenAIAgentsPlugin( - model_params=ModelActivityParameters( - start_to_close_timeout=timedelta(seconds=60) - ), - mcp_server_providers=[filesystem_server], - ), - ], - ) - - worker = Worker( - client, - task_queue="my-task-queue", - workflows=[FileSystemWorkflow], - ) - await worker.run() - - -if __name__ == "__main__": - asyncio.run(main()) -``` - -#### Workflow Implementation - -```python -from temporalio import workflow -from temporalio.contrib import openai_agents -from agents import Agent, Runner - -@workflow.defn -class FileSystemWorkflow: - @workflow.run - async def run(self, query: str) -> str: - # Reference the MCP server by name (matches name in worker configuration) - server = openai_agents.workflow.stateless_mcp_server("FileSystemServer") - - agent = Agent( - name="File Assistant", - instructions="Use the filesystem tools to read files and answer questions.", - mcp_servers=[server], - ) - - result = await Runner.run(agent, input=query) - return result.final_output -``` - -The `StatelessMCPServerProvider` takes a factory function that creates new MCP server instances. The server name used in `stateless_mcp_server()` must match the name configured in the MCP server instance. In this example, the name is `FileSystemServer`. - -### Stateful MCP Servers - -For implementation details and examples, see the [samples repository](https://github.com/temporalio/samples-python/tree/main/openai_agents/mcp). - -When using stateful servers, the dedicated worker maintaining the connection may fail due to network issues or server problems. When this happens, Temporal raises an `ApplicationError` and cannot automatically recover because it cannot restore the lost server state. -To recover from such failures, you need to implement your own application-level retry logic. - -### Factory Arguments - -Both `stateless_mcp_server()` and `stateful_mcp_server()` accept an optional `factory_argument`, which is passed to the registered server factory when the MCP server is created. - -A stateless factory that declares no parameters — like the `lambda: MCPServerStdio(...)` example above — ignores the value, but it is still recorded in history. - -**Do not pass secrets, credentials, or API keys through `factory_argument`.** It is an activity argument, so it is recorded in workflow history and, without a payload codec, visible in the web UI. Resolve credentials worker-side inside the server factory instead. - -### Hosted MCP Tool - -For network-accessible MCP servers, you can also use `HostedMCPTool` from the OpenAI Agents SDK, which uses an MCP client hosted by OpenAI. - -## Secrets from the Worker's Environment - -⚠️ **Experimental** - This functionality is subject to change prior to General Availability. - -A credential an agent needs can stay in the worker process's environment instead of being written into your workflow. Where the value would otherwise go, you name the environment variable that holds it, and the worker reads that variable when the value is actually needed. - -There are two forms, and which one you use follows from where the value goes: - -- For a hosted tool credential, use `temporal_worker_env_ref()`. It is substituted only in the fields listed under [Hosted Tool Credentials](#hosted-tool-credentials). -- For a sandbox environment variable, use `TemporalWorkerEnvValue`. - -Both are gated by `resolvable_worker_env_vars`, an allowlist of the variable names a worker is willing to read. On every worker that runs model or sandbox activities, set the variable and add its name to that list: - -```python -plugin = OpenAIAgentsPlugin(resolvable_worker_env_vars=["MY_MCP_TOKEN"]) -``` - -Names are matched exactly, with no globbing. Passing `AllowAllWorkerEnvVars()` in place of the list makes every environment variable on the worker resolvable, so a workflow-authored sandbox manifest can name any variable on the worker and have its value land inside the container. - -```python -from temporalio.contrib.openai_agents import AllowAllWorkerEnvVars - -plugin = OpenAIAgentsPlugin(resolvable_worker_env_vars=AllowAllWorkerEnvVars()) -``` - -The reference form never raises. A name the worker does not allow is sent on as the reference string, and a name it allows resolves to whatever the variable holds — an empty string when that variable is unset or empty. - -### Hosted Tool Credentials - -Pass `temporal_worker_env_ref()` the *name of an environment variable*, in place of the credential itself: - -```python -from agents import HostedMCPTool -from temporalio.contrib.openai_agents import temporal_worker_env_ref - -tool = HostedMCPTool( - tool_config={ - "type": "mcp", - "server_label": "my_server", - "server_url": "https://example.com/mcp", - "authorization": temporal_worker_env_ref("MY_MCP_TOKEN"), - } -) -``` - -A reference can sit inside a larger value: in `"Bearer " + temporal_worker_env_ref("MY_MCP_TOKEN")`, the reference is replaced in place and the rest of the string is sent unchanged. - -The environment variable's value is substituted in these fields and no others: - -- `authorization`, and the value of each entry in `headers`, in a `HostedMCPTool`'s `tool_config` -- `value` in each entry of `network_policy.domain_secrets` under a hosted `ShellTool`'s `environment` -- `value` in each entry of `network_policy.domain_secrets` under the `container` in a `CodeInterpreterTool`'s `tool_config` - -### Sandbox Environment Variables - -Put a `TemporalWorkerEnvValue` in the environment of a [sandbox](#sandbox-support) manifest, in place of the value itself: - -```python -from agents.sandbox import Manifest -from agents.sandbox.manifest import Environment - -from temporalio.contrib.openai_agents import TemporalWorkerEnvValue - -manifest = Manifest( - environment=Environment( - value={ - "OPENAI_API_KEY": TemporalWorkerEnvValue(name="PROD_OPENAI_KEY"), - "REGION": "us-west-2", - } - ) -) -``` - -Pass that manifest to `SandboxRunConfig(manifest=...)`. This reads `PROD_OPENAI_KEY` on the worker and sets `OPENAI_API_KEY` inside the sandbox, so the two names need not match. - -## Sandbox Support - -⚠️ **Pre-release** - This functionality is subject to change prior to General Availability. - -The sandbox integration lets `SandboxAgent` from the OpenAI Agents SDK execute inside a remote or local sandbox (Daytona, Docker, E2B, local Unix, etc.) while keeping all coordination durable in Temporal. - -Every sandbox operation — creating a session, running commands, reading/writing files, PTY interactions — is dispatched as a Temporal activity. This means sandbox work is fully observable, retryable, and recoverable like any other activity, and sandbox session state is serialized with the workflow so it survives worker restarts. - -### Architecture - -```text -Workflow Code - ↓ -temporal_sandbox_client("daytona") [returns TemporalSandboxClient] - ↓ -SandboxAgent.run(run_config=RunConfig(sandbox=SandboxRunConfig(client=...))) - ↓ -sandbox agent calls session.exec / session.read / session.write / … - ↓ -TemporalSandboxSession routes each call as a Temporal activity -("daytona-sandbox_session_exec", "daytona-sandbox_session_read", …) - ↓ -SandboxClientProvider activities on the worker call the real sandbox client - ↓ -Actual sandbox backend (Daytona, Docker, local, …) -``` - -### Worker Configuration - -Register one or more `SandboxClientProvider` instances with the plugin. Each provider pairs a unique name with a real `BaseSandboxClient` implementation. The plugin automatically registers all required activities on the worker. - -```python -import asyncio -from datetime import timedelta -from temporalio.client import Client -from temporalio.worker import Worker -from temporalio.contrib.openai_agents import OpenAIAgentsPlugin, SandboxClientProvider, ModelActivityParameters -from agents.extensions.sandbox.daytona import DaytonaSandboxClient -from agents.sandbox.sandboxes.unix_local import UnixLocalSandboxClient - -async def main(): - client = await Client.connect( - "localhost:7233", - plugins=[ - OpenAIAgentsPlugin( - model_params=ModelActivityParameters( - start_to_close_timeout=timedelta(seconds=30) - ), - sandbox_clients=[ - SandboxClientProvider("daytona", DaytonaSandboxClient()), - SandboxClientProvider("local", UnixLocalSandboxClient()), - ], - ), - ], - ) - - worker = Worker( - client, - task_queue="my-task-queue", - workflows=[MyWorkflow], - ) - await worker.run() -``` - -Provider names must be unique. Each name becomes the prefix for that backend's activities, allowing multiple backends to coexist on a single worker. - -### Workflow Usage - -In the workflow, use `temporal_sandbox_client()` to create a reference to a registered backend by name. Pass it to `SandboxRunConfig` inside `RunConfig`: - -```python -from temporalio import workflow -from temporalio.contrib.openai_agents.workflow import temporal_sandbox_client -from agents import Runner -from agents.sandbox import SandboxAgent, SandboxRunConfig -from agents.run import RunConfig -from agents.extensions.sandbox.daytona import DaytonaSandboxClientOptions - -@workflow.defn -class MyWorkflow: - @workflow.run - async def run(self, prompt: str) -> str: - agent = SandboxAgent( - name="Coding Assistant", - instructions="You are a helpful coding assistant with access to a sandbox.", - ) - - result = await Runner.run( - agent, - prompt, - run_config=RunConfig( - sandbox=SandboxRunConfig( - client=temporal_sandbox_client("daytona"), - options=DaytonaSandboxClientOptions(pause_on_exit=False), - ), - ), - ) - return result.final_output -``` - -The name passed to `temporal_sandbox_client()` must exactly match the name used in `SandboxClientProvider` on the worker. - -### Multiple Backends - -A single workflow can target different backends by name. Register all backends on the worker and reference each by name in the workflow: - -```python -# Run a task on the "daytona" backend -result = await Runner.run( - agent, prompt, - run_config=RunConfig(sandbox=SandboxRunConfig( - client=temporal_sandbox_client("daytona"), - options=DaytonaSandboxClientOptions(pause_on_exit=False), - )), -) - -# Run a different task on the "local" backend -result = await Runner.run( - agent, prompt, - run_config=RunConfig(sandbox=SandboxRunConfig( - client=temporal_sandbox_client("local"), - options=UnixLocalSandboxClientOptions(), - )), -) -``` - -## Streaming - -⚠️ **Experimental** - This functionality is subject to change prior to General Availability. - -The integration supports streaming model responses via the SDK-native -`Runner.run_streamed` API. Inside a workflow, model calls execute as a -streaming activity (`invoke_model_activity_streaming`) that consumes -`Model.stream_response` and returns the collected list of native OpenAI -response events. The workflow surfaces those events to the caller -through `RunResultStreaming.stream_events()`, which wraps them in the -agents-SDK `StreamEvent` union (so raw model events arrive as -`RawResponsesStreamEvent.data`). - -External consumers (UIs, tracing pipelines, etc.) observe events as -they arrive by hosting a [`WorkflowStream`](../workflow_streams/README.md) -in the workflow and subscribing with `WorkflowStreamClient`. The -streaming activity publishes each event to the topic configured on -`ModelActivityParameters.streaming_topic`. The topic is required -when using `Runner.run_streamed`; calling it without a configured topic -raises before any activity is scheduled. - -Example workflow consuming events via `stream_events()` while the -streaming activity publishes to the `"events"` topic: - -```python -from agents import Agent, Runner -from agents.stream_events import RawResponsesStreamEvent - -from temporalio import workflow - -@workflow.defn -class MyAgent: - @workflow.run - async def run(self, prompt: str) -> str: - agent = Agent(name="Assistant", instructions="...") - result = Runner.run_streamed(agent, prompt) - async for event in result.stream_events(): - if isinstance(event, RawResponsesStreamEvent): - raw_event = event.data # native OpenAI ResponseStreamEvent - ... - return result.final_output -``` - -To publish raw model events to external subscribers, host a -`WorkflowStream` in the workflow and configure -`OpenAIAgentsPlugin(model_params=ModelActivityParameters(streaming_topic="events"))`. See [`temporalio.contrib.workflow_streams`](../workflow_streams/README.md) for the -publisher and subscriber API. - -`RunResultStreaming.stream_events()` yields the agents-SDK -`StreamEvent` union (`RawResponsesStreamEvent`, `RunItemStreamEvent`, -`AgentUpdatedStreamEvent`); native OpenAI response events arrive -wrapped as `RawResponsesStreamEvent.data`. Workflow-stream subscribers, -by contrast, receive the unwrapped native events directly because the -streaming activity publishes them straight from `Model.stream_response`. - -Streaming is incompatible with `use_local_activity` because local -activities support neither activity heartbeats nor the workflow stream -signal channel. - -Activity retries surface to workflow-stream subscribers but not to -`RunResultStreaming.stream_events()`. Events are published to the -stream as `Model.stream_response` produces them, so a partial attempt -that fails mid-response leaves its emitted events on the stream and the -retry attempt publishes a second sequence. `stream_events()` only sees -the final successful attempt's collected events because it consumes the -activity's return value. Workflow-stream subscribers should treat -retries the same way as any other workflow_streams publisher — see -[Delivery semantics](../workflow_streams/README.md) for the trade and -the conventional `RETRY` event pattern for surfacing the transition to -consumers. - -## Feature Support - -This integration is presently subject to certain limitations. -Realtime agents are not supported. Streaming is supported via -`Runner.run_streamed` — see [Streaming](#streaming) above. -Certain tools are not suitable for a distributed computing environment, so these have been disabled as well. - -### Model Providers - -| Model Provider | Supported | -| :------------- | :-------: | -| OpenAI | Yes | -| LiteLLM | Yes | - -### Model Response format - -| Model Response | Supported | -| :------------- | :------------------: | -| Get Response | Yes | -| Streaming | Yes (experimental) | - -### Tools - -#### Tool Type - -`LocalShellTool` and `ComputerTool` are not suited to a distributed computing setting. - -| Tool Type | Supported | -| :------------------ | :-------: | -| FunctionTool | Yes | -| LocalShellTool | No | -| WebSearchTool | Yes | -| FileSearchTool | Yes | -| HostedMCPTool | Yes | -| ImageGenerationTool | Yes | -| CodeInterpreterTool | Yes | -| ShellTool | Yes | -| ComputerTool | No | - -#### Tool Context - -As described in [Tool Calling](#tool-calling), context propagation is read-only when Temporal activities are used as tools. - -| Context Propagation | Supported | -| :------------------------------------- | :-------: | -| Activity Tool receives copy of context | Yes | -| Activity Tool can update context | No | -| Function Tool received context | Yes | -| Function Tool can update context | Yes | - -### MCP - -The MCP protocol is stateful, but many MCP servers are stateless. -We let you choose between two MCP wrappers, one designed for stateless MCP servers and one for stateful MCP servers. -These wrappers work with all transport varieties. - -Note that when using network-accessible MCP servers, you also can also use the tool `HostedMCPTool`, which is part of the OpenAI Responses API and uses an MCP client hosted by OpenAI. - -| MCP Class | Supported | -| :---------------------- | :-------: | -| MCPServerStdio | Yes | -| MCPServerSse | Yes | -| MCPServerStreamableHttp | Yes | - -### Guardrails - -| Guardrail Type | Supported | -| :------------- | :-------: | -| Code | Yes | -| Agent | Yes | - -### Sessions - -SQLite storage is not suited to a distributed environment. - -| Feature | Supported | -| :------------ | :-------: | -| SQLiteSession | No | - -### Tracing - -| Tracing Provider | Supported | -| :--------------- | :-------: | -| OpenAI platform | Yes | - -## OpenTelemetry Integration - -⚠️ **Public Preview** - This functionality is subject to change prior to General Availability. - -This integration provides seamless export of OpenAI agent telemetry to OpenTelemetry (OTEL) endpoints for observability and monitoring. The integration automatically handles workflow replay semantics, ensuring spans are only exported when workflows actually complete. - -### Quick Start - -To enable OTEL telemetry export, you need to set up a global `ReplaySafeTracerProvider` and enable the integration in the `OpenAIAgentsPlugin`: - -```python -from datetime import timedelta -from temporalio.client import Client -from temporalio.contrib.openai_agents import OpenAIAgentsPlugin, ModelActivityParameters -from temporalio.contrib.opentelemetry import create_tracer_provider -from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter -from opentelemetry import trace -from opentelemetry.sdk.trace.export import SimpleSpanProcessor - -# Configure your OTEL exporters - -# Set up the global tracer provider -tracer_provider = create_tracer_provider() -tracer_provider.add_span_processor(SimpleSpanProcessor(OTLPSpanExporter(endpoint="http://localhost:4317"))) -trace.set_tracer_provider(tracer_provider) - -client = await Client.connect( - "localhost:7233", - plugins=[ - OpenAIAgentsPlugin( - use_otel_instrumentation=True, # Enable OTEL integration - model_params=ModelActivityParameters( - start_to_close_timeout=timedelta(seconds=30) - ) - ), - ], -) -``` - -### Features - -- **Multiple Exporters**: Send telemetry to multiple OTEL endpoints simultaneously via the global tracer provider -- **Replay-Safe**: Spans are only exported when workflows actually complete, not during replays -- **Deterministic IDs**: Consistent span IDs across workflow replays for reliable correlation -- **Automatic Setup**: No manual instrumentation required - just enable the flag and set up the global tracer provider -- **Graceful Degradation**: Works seamlessly whether OTEL dependencies are installed or not - -### Dependencies - -OTEL integration requires additional dependencies: - -```bash -pip install openinference-instrumentation-openai-agents opentelemetry-sdk -``` - -Choose the appropriate OTEL exporter for your monitoring system: - -```bash -# For OTLP (works with most OTEL collectors and monitoring systems) -pip install opentelemetry-exporter-otlp - -# Other exporters available for specific systems -pip install opentelemetry-exporter- -``` - -`ConsoleSpanExporter` (development/debugging) ships with `opentelemetry-sdk`, so it needs no extra package. - -### Example: Multiple Exporters - -```python -from temporalio.contrib.opentelemetry import create_tracer_provider -from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter -from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter -from opentelemetry import trace - -exporters = [ - # Production monitoring system - OTLPSpanExporter( - endpoint="https://your-monitoring-system:4317", - headers={"api-key": "your-api-key"} - ), - - # Secondary monitoring endpoint - OTLPSpanExporter(endpoint="https://backup-collector:4317"), - - # Development debugging - ConsoleSpanExporter(), -] - -# Set up the global tracer provider with one span processor per exporter -tracer_provider = create_tracer_provider() -for exporter in exporters: - tracer_provider.add_span_processor(BatchSpanProcessor(exporter)) -trace.set_tracer_provider(tracer_provider) - -plugin = OpenAIAgentsPlugin(use_otel_instrumentation=True) -``` - -### Error Handling - -If you enable OTEL instrumentation but the required dependencies are not installed, you'll receive a clear error message: - -``` -ImportError: OTEL dependencies not available. Install with: pip install openinference-instrumentation-openai-agents opentelemetry-sdk -``` - -If you enable OTEL instrumentation but don't have a proper global tracer provider set up, you'll get: - -``` -ValueError: Global tracer provider must a ReplaySafeTracerProvider. Use temporalio.contrib.opentelemtry.create_trace_provider to create one. -``` - -### Direct OpenTelemetry API Calls in Workflows - -When using direct OpenTelemetry API calls within workflows (e.g., `opentelemetry.trace.get_tracer(__name__).start_as_current_span()`), you need to ensure proper context bridging and sandbox configuration. - -#### Sandbox Configuration - -Workflows run in a sandbox that restricts module access. To use direct OTEL API calls, you must explicitly allow OpenTelemetry passthrough: - -```python -from temporalio.worker import Worker -from temporalio.worker.workflow_sandbox import SandboxedWorkflowRunner, SandboxRestrictions - -# Configure worker with OpenTelemetry passthrough -worker = Worker( - client, - task_queue="my-task-queue", - workflows=[MyWorkflow], - workflow_runner=SandboxedWorkflowRunner( - SandboxRestrictions.default.with_passthrough_modules("opentelemetry") - ) -) -``` - -#### Context Bridging Pattern - -Direct OTEL spans must be created within an active OpenAI Agents SDK span to ensure proper parenting: - -```python -import opentelemetry.trace -from agents import custom_span -from temporalio import workflow - -@workflow.defn -class MyWorkflow: - @workflow.run - async def run(self) -> str: - # Start an SDK span first to establish OTEL context bridge - with custom_span("Workflow coordination"): - # Now direct OTEL spans will be properly parented - tracer = opentelemetry.trace.get_tracer(__name__) - with tracer.start_as_current_span("Custom workflow span"): - # Your workflow logic here - result = await self.do_work() - return result -``` - -#### Why This Pattern is Required - -- **OpenInference instrumentation** bridges OpenAI Agents SDK spans to OpenTelemetry context -- **Direct OTEL API calls** without an active SDK span become root spans with no parent -- **SDK spans** (`custom_span()`) establish the context bridge that allows subsequent direct OTEL spans to inherit proper trace parenting - -#### Complete Example - -```python -import opentelemetry.trace -from agents import custom_span -from temporalio import workflow -from temporalio.worker import Worker -from temporalio.worker.workflow_sandbox import SandboxedWorkflowRunner, SandboxRestrictions - -@workflow.defn -class TracedWorkflow: - @workflow.run - async def run(self) -> str: - # Establish OTEL context with SDK span - with custom_span("Main workflow"): - # Create direct OTEL spans for fine-grained tracing - tracer = opentelemetry.trace.get_tracer(__name__) - - with tracer.start_as_current_span("Data processing"): - data = await self.process_data() - - with tracer.start_as_current_span("Business logic"): - result = await self.execute_business_logic(data) - - return result - -# Worker configuration -worker = Worker( - client, - task_queue="traced-workflows", - workflows=[TracedWorkflow], - workflow_runner=SandboxedWorkflowRunner( - SandboxRestrictions.default.with_passthrough_modules("opentelemetry") - ) -) -``` - -This ensures your direct OTEL spans are properly parented within the trace hierarchy initiated by your client SDK traces. - -### Client-Side Trace Initialization - -You can also start an Agents SDK trace on the client side before executing a workflow. This is useful when you want the entire workflow execution to be part of a larger trace context: - -```python -from agents import trace, custom_span -from temporalio.contrib.openai_agents import OpenAIAgentsPlugin - -# Set up the plugin with OTEL integration -plugin = OpenAIAgentsPlugin(use_otel_instrumentation=True) - -# Client setup -client = await Client.connect( - "localhost:7233", - plugins=[plugin] -) - -# Start a trace on the client side -with plugin.tracing_context(): - with trace("Customer support workflow"): - with custom_span("Workflow execution"): - # Execute workflow within the trace context - result = await client.execute_workflow( - CustomerSupportAgent.run, - "Help me with my order", - id="customer-support-123", - task_queue="my-task-queue", - ) - print(f"Result: {result}") -``` - -The `plugin.tracing_context()` is required when starting traces outside of a worker context. This ensures proper instrumentation setup and trace propagation into the workflow execution. - -If OTEL instrumentation is not enabled, the integration works normally without any OTEL setup. - -### Voice - -| Mode | Supported | -| :----------------------- | :-----------: | -| Voice agents (pipelines) | Yes [^voice] | -| Realtime agents | No | - -[^voice]: `VoicePipeline` runs in your process and delegates the agent - step (`VoiceWorkflowBase.run`) to a Temporal workflow that uses - `Runner.run` or `Runner.run_streamed`. STT and TTS run outside - Temporal; the agent loop is durable. - -### Utilities - -The REPL utility is not suitable for a distributed setting. - -| Utility | Supported | -| :------ | :-------: | -| REPL | No | - -## Additional Examples - -You can find additional examples in the [Temporal Python Samples Repository](https://github.com/temporalio/samples-python/tree/main/openai_agents). diff --git a/temporalio/contrib/openai_agents/__init__.py b/temporalio/contrib/openai_agents/__init__.py deleted file mode 100644 index b00620fb5..000000000 --- a/temporalio/contrib/openai_agents/__init__.py +++ /dev/null @@ -1,43 +0,0 @@ -"""Support for using the OpenAI Agents SDK as part of Temporal workflows. - -This module provides compatibility between the -`OpenAI Agents SDK `_ and Temporal workflows. -""" - -from temporalio.contrib.openai_agents._errors import AgentsWorkflowError -from temporalio.contrib.openai_agents._mcp import ( - StatefulMCPServerProvider, - StatelessMCPServerProvider, -) -from temporalio.contrib.openai_agents._model_parameters import ModelActivityParameters -from temporalio.contrib.openai_agents._temporal_openai_agents import ( - OpenAIAgentsPlugin, - OpenAIPayloadConverter, -) -from temporalio.contrib.openai_agents._temporal_worker_env_ref import ( - AllowAllWorkerEnvVars, - temporal_worker_env_ref, -) -from temporalio.contrib.openai_agents.sandbox._sandbox_client_provider import ( - SandboxClientProvider, -) -from temporalio.contrib.openai_agents.sandbox._temporal_worker_env_value import ( - TemporalWorkerEnvValue, -) - -from . import testing, workflow - -__all__ = [ - "AgentsWorkflowError", - "AllowAllWorkerEnvVars", - "ModelActivityParameters", - "OpenAIAgentsPlugin", - "OpenAIPayloadConverter", - "SandboxClientProvider", - "StatelessMCPServerProvider", - "StatefulMCPServerProvider", - "TemporalWorkerEnvValue", - "temporal_worker_env_ref", - "testing", - "workflow", -] diff --git a/temporalio/contrib/openai_agents/_errors.py b/temporalio/contrib/openai_agents/_errors.py deleted file mode 100644 index 5ca502376..000000000 --- a/temporalio/contrib/openai_agents/_errors.py +++ /dev/null @@ -1,11 +0,0 @@ -"""Error types for the OpenAI Agents SDK Temporal integration.""" - -from temporalio.exceptions import TemporalError - - -class AgentsWorkflowError(TemporalError): - """Error that terminates the calling workflow or update. - - Raised when the agents SDK raises an error which should terminate, or when - the plugin rejects an unsupported configuration. - """ diff --git a/temporalio/contrib/openai_agents/_heartbeat_decorator.py b/temporalio/contrib/openai_agents/_heartbeat_decorator.py deleted file mode 100644 index 7c5b9193d..000000000 --- a/temporalio/contrib/openai_agents/_heartbeat_decorator.py +++ /dev/null @@ -1,38 +0,0 @@ -import asyncio -from collections.abc import Awaitable, Callable -from functools import wraps -from typing import Any, TypeVar, cast - -from temporalio import activity - -F = TypeVar("F", bound=Callable[..., Awaitable[Any]]) - - -def auto_heartbeater(fn: F) -> F: - """Decorator that heartbeats at half the activity's heartbeat timeout.""" - - @wraps(fn) - async def wrapper(*args: Any, **kwargs: Any) -> Any: - heartbeat_timeout = activity.info().heartbeat_timeout - heartbeat_task = None - if heartbeat_timeout: - heartbeat_task = asyncio.create_task( - _heartbeat_every(heartbeat_timeout.total_seconds() / 2) - ) - try: - return await fn(*args, **kwargs) - finally: - if heartbeat_task: - heartbeat_task.cancel() - try: - await heartbeat_task - except asyncio.CancelledError: - pass - - return cast(F, wrapper) - - -async def _heartbeat_every(delay: float) -> None: - while True: - await asyncio.sleep(delay) - activity.heartbeat() diff --git a/temporalio/contrib/openai_agents/_invoke_model_activity.py b/temporalio/contrib/openai_agents/_invoke_model_activity.py deleted file mode 100644 index 9f4bfe93c..000000000 --- a/temporalio/contrib/openai_agents/_invoke_model_activity.py +++ /dev/null @@ -1,447 +0,0 @@ -"""A temporal activity that invokes a LLM model. - -Implements mapping of OpenAI datastructures to Pydantic friendly types. -""" - -import enum -from collections.abc import Collection -from dataclasses import dataclass -from datetime import timedelta -from typing import Any, NoReturn - -from agents import ( - AgentOutputSchemaBase, - CodeInterpreterTool, - FileSearchTool, - FunctionTool, - Handoff, - HostedMCPTool, - ImageGenerationTool, - ModelProvider, - ModelResponse, - ModelSettings, - ModelTracing, - OpenAIProvider, - RunContextWrapper, - Tool, - TResponseInputItem, - UserError, - WebSearchTool, -) -from agents.items import TResponseStreamEvent -from agents.tool import ( - ApplyPatchTool, - CustomTool, - LocalShellTool, - ShellTool, - ShellToolEnvironment, - ToolSearchTool, -) -from openai import ( - APIStatusError, - AsyncOpenAI, -) -from openai.types.responses import CustomToolParam -from openai.types.responses.tool_param import Mcp -from typing_extensions import Required, TypedDict - -from temporalio import activity -from temporalio.contrib.openai_agents._heartbeat_decorator import auto_heartbeater -from temporalio.contrib.openai_agents._temporal_worker_env_ref import ( - AllowAllWorkerEnvVars, - _WorkerEnvRefResolver, -) -from temporalio.contrib.workflow_streams import WorkflowStreamClient -from temporalio.exceptions import ApplicationError - - -@dataclass -class HandoffInput: - """Data conversion friendly representation of a Handoff. Contains only the fields which are needed by the model - execution to determine what to handoff to, not the actual handoff invocation, which remains in the workflow context. - """ - - tool_name: str - tool_description: str - input_json_schema: dict[str, Any] - agent_name: str - strict_json_schema: bool = True - - -@dataclass -class FunctionToolInput: - """Data conversion friendly representation of a FunctionTool. Contains only the fields which are needed by the model - execution to determine what tool to call, not the actual tool invocation, which remains in the workflow context. - """ - - name: str - description: str - params_json_schema: dict[str, Any] - strict_json_schema: bool = True - - -@dataclass -class HostedMCPToolInput: - """Data conversion friendly representation of a HostedMCPTool. Contains only the fields which are needed by the model - execution to determine what tool to call, not the actual tool invocation, which remains in the workflow context. - """ - - tool_config: Mcp - - -@dataclass -class ShellToolInput: - """Data conversion friendly representation of a ShellTool. Contains only the fields which are needed by the model - execution to determine what tool to call, not the actual tool invocation, which remains in the workflow context. - """ - - name: str = "shell" - environment: ShellToolEnvironment | None = None - - -class _NoopApplyPatchEditor: - """Satisfies the ApplyPatchEditor protocol for tool reconstruction during model calls.""" - - def create_file(self, operation: Any) -> None: # type: ignore[reportUnusedParameter] - return None - - def update_file(self, operation: Any) -> None: # type: ignore[reportUnusedParameter] - return None - - def delete_file(self, operation: Any) -> None: # type: ignore[reportUnusedParameter] - return None - - -@dataclass -class ApplyPatchToolInput: - """Data conversion friendly representation of an ApplyPatchTool.""" - - name: str = "apply_patch" - - -@dataclass -class CustomToolInput: - """Data conversion friendly representation of a CustomTool. Contains only the fields which are needed by the model - execution to determine what tool to call, not the actual tool invocation, which remains in the workflow context. - """ - - tool_config: CustomToolParam - - -ToolInput = ( - FunctionToolInput - | FileSearchTool - | WebSearchTool - | ImageGenerationTool - | CodeInterpreterTool - | HostedMCPToolInput - | ShellToolInput - | LocalShellTool - | ApplyPatchToolInput - | CustomToolInput - | ToolSearchTool -) - - -@dataclass -class AgentOutputSchemaInput(AgentOutputSchemaBase): - """Data conversion friendly representation of AgentOutputSchema.""" - - output_type_name: str | None - is_wrapped: bool - output_schema: dict[str, Any] | None - strict_json_schema: bool - - def is_plain_text(self) -> bool: - """Whether the output type is plain text (versus a JSON object).""" - return self.output_type_name is None or self.output_type_name == "str" - - def is_strict_json_schema(self) -> bool: - """Whether the JSON schema is in strict mode.""" - return self.strict_json_schema - - def json_schema(self) -> dict[str, Any]: - """The JSON schema of the output type.""" - if self.is_plain_text(): - raise UserError("Output type is plain text, so no JSON schema is available") - if self.output_schema is None: - raise UserError("Output schema is not defined") - return self.output_schema - - def validate_json(self, json_str: str) -> Any: - """Validate the JSON string against the schema.""" - raise NotImplementedError() - - def name(self) -> str: - """Get the name of the output type.""" - if self.output_type_name is None: - raise ValueError("output_type_name is None") - return self.output_type_name - - -class ModelTracingInput(enum.IntEnum): - """Conversion friendly representation of ModelTracing. - - Needed as ModelTracing is enum.Enum instead of IntEnum - """ - - DISABLED = 0 - ENABLED = 1 - ENABLED_WITHOUT_DATA = 2 - - -class ActivityModelInput(TypedDict, total=False): - """Input for the invoke_model_activity activity.""" - - model_name: str | None - system_instructions: str | None - input: Required[str | list[TResponseInputItem]] - model_settings: Required[ModelSettings] - tools: list[ToolInput] - output_schema: AgentOutputSchemaInput | None - handoffs: list[HandoffInput] - tracing: Required[ModelTracingInput] - previous_response_id: str | None - conversation_id: str | None - prompt: Any | None - - -class StreamingActivityModelInput(ActivityModelInput, total=False): - """Input for the invoke_model_activity_streaming activity. - - Adds the streaming-only fields on top of :class:`ActivityModelInput`. - """ - - streaming_topic: Required[str] - streaming_batch_interval: timedelta - - -async def _empty_on_invoke_tool(_ctx: RunContextWrapper[Any], _input: str) -> str: - return "" - - -async def _empty_on_invoke_handoff(_ctx: RunContextWrapper[Any], _input: str) -> Any: - return None - - -async def _noop_shell_executor(*_a: Any, **_kw: Any) -> str: - return "" - - -def _build_tool(tool: ToolInput, env_refs: _WorkerEnvRefResolver) -> Tool: - """Reconstruct a Tool from its data-conversion-friendly input form.""" - if isinstance( - tool, - ( - FileSearchTool, - WebSearchTool, - ImageGenerationTool, - LocalShellTool, - ToolSearchTool, - ), - ): - return tool - elif isinstance(tool, CodeInterpreterTool): - return CodeInterpreterTool( - tool_config=env_refs.resolve_code_interpreter_tool_config(tool.tool_config) - ) - elif isinstance(tool, ShellToolInput): - environment = env_refs.resolve_shell_tool_environment(tool.environment) - # Only a local environment takes an executor. - return ShellTool( - name=tool.name, - environment=environment, - executor=_noop_shell_executor if environment["type"] == "local" else None, - ) - elif isinstance(tool, ApplyPatchToolInput): - return ApplyPatchTool(name=tool.name, editor=_NoopApplyPatchEditor()) - elif isinstance(tool, HostedMCPToolInput): - return HostedMCPTool( - tool_config=env_refs.resolve_mcp_tool_config(tool.tool_config) - ) - elif isinstance(tool, CustomToolInput): - return CustomTool( - name=tool.tool_config["name"], - description=tool.tool_config.get("description", ""), - on_invoke_tool=_empty_on_invoke_tool, - format=tool.tool_config.get("format"), - defer_loading=tool.tool_config.get("defer_loading", False), - ) - elif isinstance(tool, FunctionToolInput): - return FunctionTool( - name=tool.name, - description=tool.description, - params_json_schema=tool.params_json_schema, - on_invoke_tool=_empty_on_invoke_tool, - strict_json_schema=tool.strict_json_schema, - ) - else: - raise UserError(f"Unknown tool type: {tool.name}") # type:ignore[reportUnreachable] - - -def _build_tools_and_handoffs( - input: ActivityModelInput, - env_refs: _WorkerEnvRefResolver, -) -> tuple[list[Tool], list[Handoff[Any, Any]]]: - tools = [_build_tool(x, env_refs) for x in input.get("tools", [])] - handoffs: list[Handoff[Any, Any]] = [ - Handoff( - tool_name=x.tool_name, - tool_description=x.tool_description, - input_json_schema=x.input_json_schema, - agent_name=x.agent_name, - strict_json_schema=x.strict_json_schema, - on_invoke_handoff=_empty_on_invoke_handoff, - ) - for x in input.get("handoffs", []) - ] - return tools, handoffs - - -def _raise_for_openai_status(e: APIStatusError) -> NoReturn: - """Translate an OpenAI APIStatusError into the right retry posture.""" - retry_after: timedelta | None = None - retry_after_ms_header = e.response.headers.get("retry-after-ms") - if retry_after_ms_header is not None: - retry_after = timedelta(milliseconds=float(retry_after_ms_header)) - - if retry_after is None: - retry_after_header = e.response.headers.get("retry-after") - if retry_after_header is not None: - retry_after = timedelta(seconds=float(retry_after_header)) - - should_retry_header = e.response.headers.get("x-should-retry") - if should_retry_header == "false": - raise ApplicationError( - message="Non retryable OpenAI error", - type=APIStatusError.__name__, - non_retryable=True, - next_retry_delay=retry_after, - ) from e - - # Retry on 408 (Request Timeout), 409 (Conflict / often transient - # state mismatch), 429 (Too Many Requests / rate-limited), and any - # 5xx (server-side errors). All other 4xx codes are caller errors - # that won't recover on retry, unless the server explicitly asks for - # a retry via x-should-retry. - retryable = ( - should_retry_header == "true" - or e.response.status_code in [408, 409, 429] - or e.response.status_code >= 500 - ) - raise ApplicationError( - message=( - f"{'Retryable' if retryable else 'Non retryable'} OpenAI status code: " - f"{e.response.status_code}" - ), - type=APIStatusError.__name__, - non_retryable=not retryable, - next_retry_delay=retry_after, - ) from e - - -class ModelActivity: - """Class wrapper for model invocation activities to allow model customization. By default, we use an OpenAIProvider with retries disabled. - Disabling retries in your model of choice is recommended to allow activity retries to define the retry model. - """ - - def __init__( - self, - model_provider: ModelProvider | None = None, - resolvable_worker_env_vars: Collection[str] | AllowAllWorkerEnvVars = (), - ): - """Initialize the activity with a model provider.""" - self._model_provider = model_provider or OpenAIProvider( - openai_client=AsyncOpenAI(max_retries=0) - ) - self._env_refs = _WorkerEnvRefResolver(resolvable_worker_env_vars) - - @activity.defn - @auto_heartbeater - async def invoke_model_activity(self, input: ActivityModelInput) -> ModelResponse: - """Activity that invokes a model with the given input.""" - model = self._model_provider.get_model(input.get("model_name")) - tools, handoffs = _build_tools_and_handoffs(input, self._env_refs) - - try: - return await model.get_response( - system_instructions=input.get("system_instructions"), - input=input["input"], - model_settings=input["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"), - ) - except APIStatusError as e: - _raise_for_openai_status(e) - - @activity.defn - @auto_heartbeater - async def invoke_model_activity_streaming( - self, input: StreamingActivityModelInput - ) -> list[TResponseStreamEvent]: - """Streaming-aware model activity. - - .. warning:: - Streaming support is experimental and may change in future - versions. - - Calls ``model.stream_response()`` and returns the collected list - of native OpenAI stream events. The workflow's - ``Model.stream_response`` stub yields these to the agents - framework, which builds the final ``ModelResponse`` from the - terminal ``ResponseCompletedEvent``. - - Each event is also published to the workflow's stream on - ``streaming_topic`` so external consumers (UIs, tracing, - etc.) can observe events as they arrive. - - Heartbeats run on a background task via ``auto_heartbeater`` so - long initial-token latency or long pauses between chunks do not - trip ``heartbeat_timeout``. - """ - model = self._model_provider.get_model(input.get("model_name")) - tools, handoffs = _build_tools_and_handoffs(input, self._env_refs) - - topic = input["streaming_topic"] - batch_interval = input.get( - "streaming_batch_interval", timedelta(milliseconds=100) - ) - events: list[TResponseStreamEvent] = [] - - stream = WorkflowStreamClient.from_within_activity( - batch_interval=batch_interval - ) - # TResponseStreamEvent is a typing.Annotated[Union[...]] — a typing - # special form, not a class — so it cannot be passed as type[T]. - # Leave the topic untyped (default Any); subscribers that want - # 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=input["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) - - return events diff --git a/temporalio/contrib/openai_agents/_mcp.py b/temporalio/contrib/openai_agents/_mcp.py deleted file mode 100644 index 8f5294c42..000000000 --- a/temporalio/contrib/openai_agents/_mcp.py +++ /dev/null @@ -1,539 +0,0 @@ -import asyncio -import dataclasses -import functools -import inspect -from collections.abc import Callable, Sequence -from contextlib import AbstractAsyncContextManager -from datetime import timedelta -from types import TracebackType -from typing import Any, cast - -from agents import AgentBase, RunContextWrapper -from agents.mcp import MCPServer -from mcp import GetPromptResult, ListPromptsResult # type:ignore -from mcp import Tool as MCPTool # type:ignore -from mcp.types import CallToolResult # type:ignore - -from temporalio import activity, workflow -from temporalio.api.enums.v1.workflow_pb2 import ( - TIMEOUT_TYPE_HEARTBEAT, - TIMEOUT_TYPE_SCHEDULE_TO_START, -) -from temporalio.exceptions import ( - ActivityError, - ApplicationError, - is_cancelled_exception, -) -from temporalio.worker import PollerBehaviorSimpleMaximum, Worker -from temporalio.workflow import ActivityConfig, ActivityHandle - - -@dataclasses.dataclass -class _StatelessListToolsArguments: - factory_argument: Any | None - - -@dataclasses.dataclass -class _StatelessCallToolsArguments: - tool_name: str - arguments: dict[str, Any] | None - factory_argument: Any | None - meta: dict[str, Any] | None = None - - -@dataclasses.dataclass -class _StatelessListPromptsArguments: - factory_argument: Any | None - - -@dataclasses.dataclass -class _StatelessGetPromptArguments: - name: str - arguments: dict[str, Any] | None - factory_argument: Any | None - - -class _StatelessMCPServerReference(MCPServer): # type:ignore[reportUnusedClass] - def __init__( - self, - server: str, - config: ActivityConfig | None, - cache_tools_list: bool, - factory_argument: Any | None = None, - ): - self._name = server + "-stateless" - self._config = config or ActivityConfig( - start_to_close_timeout=timedelta(minutes=1) - ) - self._cache_tools_list = cache_tools_list - self._tools = None - self._factory_argument = factory_argument - super().__init__() - - @property - def name(self) -> str: - return self._name - - async def connect(self) -> None: - pass - - async def cleanup(self) -> None: - pass - - async def list_tools( - self, - run_context: RunContextWrapper[Any] | None = None, - agent: AgentBase | None = None, - ) -> list[MCPTool]: - if self._tools: - return self._tools - tools = await workflow.execute_activity( - self.name + "-list-tools", - _StatelessListToolsArguments(self._factory_argument), - result_type=list[MCPTool], - **self._config, - ) - if self._cache_tools_list: - self._tools = tools - return tools - - async def call_tool( - self, - tool_name: str, - arguments: dict[str, Any] | None, - meta: dict[str, Any] | None = None, - ) -> CallToolResult: - return await workflow.execute_activity( - self.name + "-call-tool-v2", - _StatelessCallToolsArguments( - tool_name, arguments, self._factory_argument, meta - ), - result_type=CallToolResult, - **self._config, - ) - - async def list_prompts(self) -> ListPromptsResult: - return await workflow.execute_activity( - self.name + "-list-prompts", - _StatelessListPromptsArguments(self._factory_argument), - result_type=ListPromptsResult, - **self._config, - ) - - async def get_prompt( - self, name: str, arguments: dict[str, Any] | None = None - ) -> GetPromptResult: - return await workflow.execute_activity( - self.name + "-get-prompt-v2", - _StatelessGetPromptArguments(name, arguments, self._factory_argument), - result_type=GetPromptResult, - **self._config, - ) - - -class StatelessMCPServerProvider: - """A stateless MCP server implementation for Temporal workflows. - - This class wraps a function to create MCP servers to make them stateless by executing each MCP operation - as a separate Temporal activity. Each operation (list_tools, call_tool, etc.) will - connect to the underlying server, execute the operation, and then clean up the connection. - - This approach will not maintain state across calls. If the desired MCPServer needs persistent state in order to - function, this cannot be used. - """ - - def __init__( - self, - name: str, - server_factory: (Callable[[], MCPServer] | Callable[[Any | None], MCPServer]), - ): - """Initialize the stateless temporal MCP server. - - Args: - name: The name of the MCP server. - server_factory: A function which will produce MCPServer instances. It should return a new server each time - so that state is not shared between workflow runs. It may accept a single positional parameter, which - receives a ``factory_argument`` from the workflow. - """ - self._server_factory = server_factory - - # Cache whether the server factory needs to be provided with arguments - sig = inspect.signature(self._server_factory) - self._server_accepts_arguments = len(sig.parameters) != 0 - - self._name = name + "-stateless" - super().__init__() - - def _create_server(self, factory_argument: Any | None) -> MCPServer: - if self._server_accepts_arguments: - return cast(Callable[[Any | None], MCPServer], self._server_factory)( - factory_argument - ) - else: - return cast(Callable[[], MCPServer], self._server_factory)() - - @property - def name(self) -> str: - """Get the server name.""" - return self._name - - def _get_activities(self) -> Sequence[Callable]: - @activity.defn(name=self.name + "-list-tools") - async def list_tools( - args: _StatelessListToolsArguments | None = None, - ) -> list[MCPTool]: - server = self._create_server(args.factory_argument if args else None) - try: - await server.connect() - return await server.list_tools() - finally: - await server.cleanup() - - @activity.defn(name=self.name + "-call-tool-v2") - async def call_tool(args: _StatelessCallToolsArguments) -> CallToolResult: - server = self._create_server(args.factory_argument) - try: - await server.connect() - return await server.call_tool(args.tool_name, args.arguments, args.meta) - finally: - await server.cleanup() - - @activity.defn(name=self.name + "-list-prompts") - async def list_prompts( - args: _StatelessListPromptsArguments | None = None, - ) -> ListPromptsResult: - server = self._create_server(args.factory_argument if args else None) - try: - await server.connect() - return await server.list_prompts() - finally: - await server.cleanup() - - @activity.defn(name=self.name + "-get-prompt-v2") - async def get_prompt(args: _StatelessGetPromptArguments) -> GetPromptResult: - server = self._create_server(args.factory_argument) - try: - await server.connect() - return await server.get_prompt(args.name, args.arguments) - finally: - await server.cleanup() - - @activity.defn(name=self.name + "-call-tool") - async def call_tool_deprecated( - tool_name: str, - arguments: dict[str, Any] | None, - ) -> CallToolResult: - return await call_tool( - _StatelessCallToolsArguments(tool_name, arguments, None) - ) - - @activity.defn(name=self.name + "-get-prompt") - async def get_prompt_deprecated( - name: str, - arguments: dict[str, Any] | None, - ) -> GetPromptResult: - return await get_prompt(_StatelessGetPromptArguments(name, arguments, None)) - - return ( - list_tools, - call_tool, - list_prompts, - get_prompt, - call_tool_deprecated, - get_prompt_deprecated, - ) - - -def _handle_worker_failure(func: Callable) -> Callable: - @functools.wraps(func) - async def wrapper(*args: Any, **kwargs: Any): - try: - return await func(*args, **kwargs) - except ActivityError as e: - failure = e.failure - if failure: - cause = failure.cause - if cause: - if ( - cause.timeout_failure_info.timeout_type - == TIMEOUT_TYPE_SCHEDULE_TO_START - ): - raise ApplicationError( - "MCP Stateful Server Worker failed to schedule activity.", - type="DedicatedWorkerFailure", - ) from e - if ( - cause.timeout_failure_info.timeout_type - == TIMEOUT_TYPE_HEARTBEAT - ): - raise ApplicationError( - "MCP Stateful Server Worker failed to heartbeat.", - type="DedicatedWorkerFailure", - ) from e - raise e - - return wrapper - - -@dataclasses.dataclass -class _StatefulCallToolsArguments: - tool_name: str - arguments: dict[str, Any] | None - meta: dict[str, Any] | None = None - - -@dataclasses.dataclass -class _StatefulGetPromptArguments: - name: str - arguments: dict[str, Any] | None - - -@dataclasses.dataclass -class _StatefulServerSessionArguments: - factory_argument: Any | None - - -class _StatefulMCPServerReference(MCPServer, AbstractAsyncContextManager): # type:ignore[reportUnusedClass] - def __init__( - self, - server: str, - config: ActivityConfig | None, - server_session_config: ActivityConfig | None, - factory_argument: Any | None, - ): - self._name = server + "-stateful" - self._config = config or ActivityConfig( - start_to_close_timeout=timedelta(minutes=1), - schedule_to_start_timeout=timedelta(seconds=30), - ) - self._server_session_config = server_session_config or ActivityConfig( - start_to_close_timeout=timedelta(hours=1), - ) - self._connect_handle: ActivityHandle | None = None - self._factory_argument = factory_argument - super().__init__() - - @property - def name(self) -> str: - return self._name - - async def connect(self) -> None: - self._config["task_queue"] = self.name + "@" + workflow.info().run_id - self._connect_handle = workflow.start_activity( - self.name + "-server-session", - _StatefulServerSessionArguments(self._factory_argument), - **self._server_session_config, - ) - - async def cleanup(self) -> None: - if self._connect_handle: - self._connect_handle.cancel() - try: - await self._connect_handle - except Exception as e: - if is_cancelled_exception(e): - pass - else: - raise - - async def __aenter__(self): - await self.connect() - return self - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_value: BaseException | None, - traceback: TracebackType | None, - ) -> None: - await self.cleanup() - - @_handle_worker_failure - async def list_tools( - self, - run_context: RunContextWrapper[Any] | None = None, - agent: AgentBase | None = None, - ) -> list[MCPTool]: - if not self._connect_handle: - raise ApplicationError( - "Stateful MCP Server not connected. Call connect first." - ) - return await workflow.execute_activity( - self.name + "-list-tools", - args=[], - result_type=list[MCPTool], - **self._config, - ) - - @_handle_worker_failure - async def call_tool( - self, - tool_name: str, - arguments: dict[str, Any] | None, - meta: dict[str, Any] | None = None, - ) -> CallToolResult: - if not self._connect_handle: - raise ApplicationError( - "Stateful MCP Server not connected. Call connect first." - ) - return await workflow.execute_activity( - self.name + "-call-tool-v2", - _StatefulCallToolsArguments(tool_name, arguments, meta), - result_type=CallToolResult, - **self._config, - ) - - @_handle_worker_failure - async def list_prompts(self) -> ListPromptsResult: - if not self._connect_handle: - raise ApplicationError( - "Stateful MCP Server not connected. Call connect first." - ) - return await workflow.execute_activity( - self.name + "-list-prompts", - args=[], - result_type=ListPromptsResult, - **self._config, - ) - - @_handle_worker_failure - async def get_prompt( - self, name: str, arguments: dict[str, Any] | None = None - ) -> GetPromptResult: - if not self._connect_handle: - raise ApplicationError( - "Stateful MCP Server not connected. Call connect first." - ) - return await workflow.execute_activity( - self.name + "-get-prompt-v2", - _StatefulGetPromptArguments(name, arguments), - result_type=GetPromptResult, - **self._config, - ) - - -class StatefulMCPServerProvider: - """A stateful MCP server implementation for Temporal workflows. - - This class wraps a function to create MCP servers to maintain a persistent connection throughout - the workflow execution. It creates a dedicated worker that stays connected to - the MCP server and processes operations on a dedicated task queue. - - This approach will allow the MCPServer to maintain state across calls if needed, but the caller - will have to handle cases where the dedicated worker fails, as Temporal is unable to seamlessly - recreate any lost state in that case. It is discouraged to use this approach unless necessary. - - Handling dedicated worker failure will entail catching ApplicationError with type "DedicatedWorkerFailure". - Depending on the usage pattern, the caller will then have to either restart from the point at which the Stateful - server was needed or handle continuing from that loss of state in some other way. - """ - - def __init__( - self, - name: str, - server_factory: Callable[[Any | None], MCPServer], - ): - """Initialize the stateful temporal MCP server. - - Args: - name: The name of the MCP server. - server_factory: A function which will produce MCPServer instances. It should return a new server each time - so that state is not shared between workflow runs. It receives an optional ``factory_argument`` from the - workflow. - """ - self._server_factory = server_factory - self._name = name + "-stateful" - self._connect_handle: ActivityHandle | None = None - self._servers: dict[str, MCPServer] = {} - super().__init__() - - @property - def name(self) -> str: - """Get the server name.""" - return self._name - - def _get_activities(self) -> Sequence[Callable]: - def _server_id(): - return self.name + "@" + (activity.info().workflow_run_id or "") - - @activity.defn(name=self.name + "-list-tools") - async def list_tools() -> list[MCPTool]: - return await self._servers[_server_id()].list_tools() - - @activity.defn(name=self.name + "-call-tool") - async def call_tool_deprecated( - tool_name: str, arguments: dict[str, Any] | None - ) -> CallToolResult: - return await self._servers[_server_id()].call_tool(tool_name, arguments) - - @activity.defn(name=self.name + "-call-tool-v2") - async def call_tool(args: _StatefulCallToolsArguments) -> CallToolResult: - return await self._servers[_server_id()].call_tool( - args.tool_name, args.arguments, args.meta - ) - - @activity.defn(name=self.name + "-list-prompts") - async def list_prompts() -> ListPromptsResult: - return await self._servers[_server_id()].list_prompts() - - @activity.defn(name=self.name + "-get-prompt") - async def get_prompt_deprecated( - name: str, arguments: dict[str, Any] | None - ) -> GetPromptResult: - return await self._servers[_server_id()].get_prompt(name, arguments) - - @activity.defn(name=self.name + "-get-prompt-v2") - async def get_prompt(args: _StatefulGetPromptArguments) -> GetPromptResult: - return await self._servers[_server_id()].get_prompt( - args.name, args.arguments - ) - - async def heartbeat_every(delay: float, *details: Any) -> None: - """Heartbeat every so often while not cancelled""" - while True: - await asyncio.sleep(delay) - activity.heartbeat(*details) - - @activity.defn(name=self.name + "-server-session") - async def connect( - args: _StatefulServerSessionArguments | None = None, - ) -> None: - heartbeat_task = asyncio.create_task(heartbeat_every(30)) - - server_id = self.name + "@" + (activity.info().workflow_run_id or "") - if server_id in self._servers: - raise ApplicationError( - "Cannot connect to an already running server. Use a distinct name if running multiple servers in one workflow." - ) - server = self._server_factory(args.factory_argument if args else None) - try: - self._servers[server_id] = server - try: - await server.connect() - - worker = Worker( - activity.client(), - task_queue=server_id, - activities=[ - list_tools, - call_tool, - list_prompts, - get_prompt, - call_tool_deprecated, - get_prompt_deprecated, - ], - activity_task_poller_behavior=PollerBehaviorSimpleMaximum(1), - ) - - await worker.run() - finally: - await server.cleanup() - heartbeat_task.cancel() - try: - await heartbeat_task - except asyncio.CancelledError: - pass - finally: - del self._servers[server_id] - - return (connect,) diff --git a/temporalio/contrib/openai_agents/_model_parameters.py b/temporalio/contrib/openai_agents/_model_parameters.py deleted file mode 100644 index c7dcf0a35..000000000 --- a/temporalio/contrib/openai_agents/_model_parameters.py +++ /dev/null @@ -1,96 +0,0 @@ -"""Parameters for configuring Temporal activity execution for model calls.""" - -from abc import ABC, abstractmethod -from dataclasses import dataclass -from datetime import timedelta -from typing import Any - -from agents import Agent, TResponseInputItem - -from temporalio.common import Priority, RetryPolicy -from temporalio.workflow import ActivityCancellationType, VersioningIntent - - -class ModelSummaryProvider(ABC): - """Abstract base class for providing model summaries. Essentially just a callable, - but the arguments are sufficiently complex to benefit from names. - """ - - @abstractmethod - def provide( - self, - agent: Agent[Any] | None, - instructions: str | None, - input: str | list[TResponseInputItem], - ) -> str: - """Given the provided information, produce a summary for the model invocation activity.""" - pass - - -@dataclass -class ModelActivityParameters: - """Parameters for configuring Temporal activity execution for model calls. - - This class encapsulates all the parameters that can be used to configure - how Temporal activities are executed when making model calls through the - OpenAI Agents integration. - """ - - task_queue: str | None = None - """Specific task queue to use for model activities.""" - - schedule_to_close_timeout: timedelta | None = None - """Maximum time from scheduling to completion.""" - - schedule_to_start_timeout: timedelta | None = None - """Maximum time from scheduling to starting.""" - - start_to_close_timeout: timedelta | None = timedelta(seconds=60) - """Maximum time for the activity to complete.""" - - heartbeat_timeout: timedelta | None = None - """Maximum time between heartbeats. For streaming - (``Runner.run_streamed``), set this lower than - ``start_to_close_timeout`` so a stuck model call is detected before the - overall activity timeout fires.""" - - retry_policy: RetryPolicy | None = None - """Policy for retrying failed activities.""" - - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL - """How the activity handles cancellation.""" - - versioning_intent: VersioningIntent | None = None - """Versioning intent for the activity.""" - - summary_override: None | (str | ModelSummaryProvider) = None - """Summary for the activity execution.""" - - priority: Priority = Priority.default - """Priority for the activity execution.""" - - use_local_activity: bool = False - """Whether to use a local activity. If changed during a workflow execution, that would break determinism.""" - - streaming_topic: str | None = None - """Stream topic to publish raw model stream events to when the workflow - calls ``Runner.run_streamed``. Required for ``Runner.run_streamed``; - if left as ``None``, ``run_streamed`` raises before scheduling any - activity. The workflow must host a - :class:`temporalio.contrib.workflow_streams.WorkflowStream` to receive - the publishes; otherwise the signals are unhandled and dropped. - - Streaming is incompatible with ``use_local_activity`` (local activities - do not support heartbeats or the workflow stream signal channel). - - .. warning:: - Streaming support is experimental and may change in future - versions.""" - - streaming_batch_interval: timedelta = timedelta(milliseconds=100) - """Interval between automatic flushes for the stream publisher used - by the streaming activity. - - .. warning:: - Streaming support is experimental and may change in future - versions.""" diff --git a/temporalio/contrib/openai_agents/_openai_runner.py b/temporalio/contrib/openai_agents/_openai_runner.py deleted file mode 100644 index 369b1d345..000000000 --- a/temporalio/contrib/openai_agents/_openai_runner.py +++ /dev/null @@ -1,381 +0,0 @@ -import dataclasses -from collections.abc import AsyncIterator, Awaitable -from typing import Any, Callable - -from agents import ( - Agent, - AgentsException, - Handoff, - RunConfig, - RunContextWrapper, - RunResult, - RunResultStreaming, - RunState, - SQLiteSession, - TContext, - TResponseInputItem, -) -from agents.run import DEFAULT_AGENT_RUNNER, AgentRunner, RunOptions -from agents.sandbox import SandboxAgent -from typing_extensions import Unpack - -from temporalio import workflow -from temporalio.contrib.openai_agents._errors import AgentsWorkflowError -from temporalio.contrib.openai_agents._model_parameters import ModelActivityParameters -from temporalio.contrib.openai_agents._temporal_model_stub import _TemporalModelStub -from temporalio.contrib.openai_agents.sandbox._temporal_sandbox_client import ( - TemporalSandboxClient, -) - - -# Recursively replace models in all agents -def _convert_agent( - model_params: ModelActivityParameters, - agent: Agent[Any], - seen: dict[int, Agent] | None, -) -> Agent[Any]: - if seen is None: - seen = dict() - - # Short circuit if this model was already seen to prevent looping from circular handoffs - if id(agent) in seen: - return seen[id(agent)] - - # This agent has already been processed in some other run - if isinstance(agent.model, _TemporalModelStub): - return agent - - # Save the new version of the agent so that we can replace loops - new_agent = dataclasses.replace(agent) - seen[id(agent)] = new_agent - - name = _model_name(agent) - - new_handoffs: list[Agent | Handoff] = [] - for handoff in agent.handoffs: - if isinstance(handoff, Agent): - new_handoffs.append(_convert_agent(model_params, handoff, seen)) - elif isinstance(handoff, Handoff): - original_invoke = handoff.on_invoke_handoff - - # Use default parameter to capture original_invoke by value, not reference - async def on_invoke( - context: RunContextWrapper[Any], - args: str, - invoke_func: Callable[ - [RunContextWrapper[Any], str], Awaitable[Any] - ] = original_invoke, - ) -> Agent: - handoff_agent = await invoke_func(context, args) - return _convert_agent(model_params, handoff_agent, seen) - - new_handoffs.append( - dataclasses.replace(handoff, on_invoke_handoff=on_invoke) - ) - else: - raise TypeError(f"Unknown handoff type: {type(handoff)}") - - new_agent.model = _TemporalModelStub( - model_name=name, - model_params=model_params, - agent=agent, - ) - new_agent.handoffs = new_handoffs - return new_agent - - -def _has_sandbox_agent(agent: Agent[Any], seen: set[int] | None = None) -> bool: - """Check if any agent in the graph (following direct Agent handoffs) is a SandboxAgent.""" - if seen is None: - seen = set() - if id(agent) in seen: - return False - seen.add(id(agent)) - if isinstance(agent, SandboxAgent): - return True - for handoff in agent.handoffs: - if isinstance(handoff, Agent) and _has_sandbox_agent(handoff, seen): - return True - return False - - -def _coerce_run_config(value: object) -> RunConfig: - """openai-agents >= 0.19 also accepts a plain dict for ``run_config``. - - This function normalizes to a RunConfig instance. - """ - if isinstance(value, RunConfig): - return value - if not isinstance(value, dict): - raise TypeError( - f"run_config must be a RunConfig instance or a dict, got {type(value).__name__}" - ) - field_names = { - config_field.name - for config_field in dataclasses.fields(RunConfig) - if config_field.init - } - unknown_fields = sorted(str(name) for name in value if name not in field_names) - if unknown_fields: - raise TypeError(f"Unknown run_config settings: {', '.join(unknown_fields)}") - return RunConfig(**value) - - -class TemporalOpenAIRunner(AgentRunner): - """Temporal Runner for OpenAI agents. - - Forwards model calls to a Temporal activity. - - """ - - def __init__( - self, - model_params: ModelActivityParameters, - ) -> None: - """Initialize the Temporal OpenAI Runner.""" - self._runner = DEFAULT_AGENT_RUNNER or AgentRunner() - self.model_params = model_params - - def _prepare_workflow_run( - self, - starting_agent: Agent[TContext], - kwargs: RunOptions[TContext], - ) -> Agent[Any]: - """Workflow-only validation and ``kwargs`` rewrite shared by ``run()`` and ``run_streamed()``.""" - for t in starting_agent.tools: - if callable(t): - raise ValueError( - "Provided tool is not a tool type. If using an activity, make sure to wrap it with openai_agents.workflow.activity_as_tool." - ) - - if starting_agent.mcp_servers: - from temporalio.contrib.openai_agents._mcp import ( - _StatefulMCPServerReference, - _StatelessMCPServerReference, - ) - - for s in starting_agent.mcp_servers: - if not isinstance( - s, - ( - _StatelessMCPServerReference, - _StatefulMCPServerReference, - ), - ): - raise ValueError( - f"Unknown mcp_server type {type(s)} may not work durably." - ) - - if isinstance(kwargs.get("session"), SQLiteSession): - raise ValueError("Temporal workflows don't support SQLite sessions.") - - run_config = kwargs.get("run_config") - run_config = ( - RunConfig() if run_config is None else _coerce_run_config(run_config) - ) - - if run_config.model and not isinstance(run_config.model, _TemporalModelStub): - if not isinstance(run_config.model, str): - raise ValueError( - "Temporal workflows require a model name to be a string in the run config." - ) - run_config = dataclasses.replace( - run_config, - model=_TemporalModelStub( - run_config.model, model_params=self.model_params, agent=None - ), - ) - - # run_config.sandbox is global for the entire run — configure it if any agent needs it. - if _has_sandbox_agent(starting_agent) or run_config.sandbox: - if run_config.sandbox is None: - raise ValueError( - "A SandboxAgent was provided but run_config.sandbox is not configured. " - "You must set run_config.sandbox to a SandboxRunConfig. " - "For example:\n" - " from temporalio.contrib.openai_agents.workflow import temporal_sandbox_client\n" - " run_config = RunConfig(sandbox=SandboxRunConfig(client=temporal_sandbox_client('my-backend')))" - ) - elif run_config.sandbox.session is not None: - raise AgentsWorkflowError( - "run_config.sandbox.session is not supported by the Temporal OpenAI Agents " - "plugin. A live sandbox session is not a durable construct in a workflow. " - "Pass run_config.sandbox.client=temporal_sandbox_client(name) instead." - ) - elif run_config.sandbox.client is None: - raise ValueError( - "run_config.sandbox.client must be set to a temporal sandbox client. " - "Use temporalio.contrib.openai_agents.workflow.temporal_sandbox_client(name) " - "to create one, where name matches a SandboxClientProvider registered on the plugin." - ) - elif not isinstance(run_config.sandbox.client, TemporalSandboxClient): - raise ValueError( - "run_config.sandbox.client must be created via " - "temporalio.contrib.openai_agents.workflow.temporal_sandbox_client(name). " - "Do not pass a raw sandbox client directly." - ) - - kwargs["run_config"] = run_config - return _convert_agent(self.model_params, starting_agent, None) - - async def run( - self, - starting_agent: Agent[TContext], - input: str | list[TResponseInputItem] | RunState[TContext], - **kwargs: Unpack[RunOptions[TContext]], - ) -> RunResult: - """Run the agent in a Temporal workflow.""" - if not workflow.in_workflow(): - return await self._runner.run( - starting_agent, - input, - **kwargs, - ) - - converted_agent = self._prepare_workflow_run(starting_agent, kwargs) - - try: - return await self._runner.run( - starting_agent=converted_agent, - input=input, - **kwargs, - ) - except AgentsException as e: - # In order for workflow failures to properly fail the workflow, we need to rewrap them in - # a Temporal error - if e.__cause__ and workflow.is_failure_exception(e.__cause__): - reraise = AgentsWorkflowError( - f"Workflow failure exception in Agents Framework: {e}" - ) - reraise.__traceback__ = e.__traceback__ - raise reraise from e.__cause__ - else: - raise e - - def run_sync( - self, - starting_agent: Agent[TContext], - input: str | list[TResponseInputItem] | RunState[TContext], - **kwargs: Any, - ) -> RunResult: - """Run the agent synchronously (not supported in Temporal workflows).""" - if not workflow.in_workflow(): - return self._runner.run_sync( - starting_agent, - input, - **kwargs, - ) - raise RuntimeError("Temporal workflows do not support synchronous model calls.") - - def run_streamed( - self, - starting_agent: Agent[TContext], - input: str | list[TResponseInputItem] | RunState[TContext], - **kwargs: Unpack[RunOptions[TContext]], - ) -> RunResultStreaming: - """Run the agent with streaming responses. - - .. warning:: - Streaming inside Temporal workflows is experimental and may - change in future versions. - - Inside a workflow, model calls execute as the streaming model - activity. The workflow consumes events via - ``RunResultStreaming.stream_events()`` after each activity - completes; external clients can subscribe to the configured - stream topic to receive events as they arrive. - """ - if not workflow.in_workflow(): - return self._runner.run_streamed( - starting_agent, - input, - **kwargs, - ) - - # Fail-fast before the agents framework starts a background task: - # validation raised inside ``Model.stream_response`` is otherwise - # captured into ``RunResultStreaming._stored_exception`` and may - # be silently dropped if the queue completion sentinel is read - # before the run_loop_task is observed as done. - if self.model_params.streaming_topic is None: - raise AgentsWorkflowError( - "Runner.run_streamed requires " - "ModelActivityParameters.streaming_topic to be set." - ) - if self.model_params.use_local_activity: - raise AgentsWorkflowError( - "Runner.run_streamed is incompatible with " - "use_local_activity (local activities do not support " - "heartbeats or the workflow stream signal channel)." - ) - - converted_agent = self._prepare_workflow_run(starting_agent, kwargs) - - streamed_result = self._runner.run_streamed( - starting_agent=converted_agent, - input=input, - **kwargs, - ) - - # Mirror the AgentsException -> AgentsWorkflowError rewrap done - # in run() above. The streaming runner attaches the actual run - # to ``run_loop_task``; we wrap ``stream_events()`` (rather than - # the task itself) so the rewrap happens on the consumer's - # coroutine. Wrapping in a second asyncio task introduces a - # scheduling gap: ``RunResultStreaming.stream_events()`` reads - # the queue completion sentinel as soon as the run loop ends, - # but the wrapper task only resumes its ``await`` after another - # event-loop tick — between those two points, ``_check_errors`` - # sees no exception and ``_await_task_safely`` later swallows - # the rewrapped one. Iterating the underlying generator first, - # then inspecting the finished task on exit, keeps the rewrap - # race-free without touching ``run_loop_task``. - original_stream_events = streamed_result.stream_events - run_loop_task = streamed_result.run_loop_task - - async def _stream_events_with_rewrap() -> AsyncIterator[Any]: - try: - async for event in original_stream_events(): - yield event - except AgentsException as e: - _reraise_workflow_failure(e) - raise - # The agents framework may have stored the run-loop - # exception on ``_stored_exception`` (or surfaced it through - # the iterator) without re-raising it through stream_events. - # By the time the iterator is exhausted, ``run_loop_task`` - # is done — surface its exception here so a failed run - # cannot appear successful, applying the workflow-failure - # rewrap when applicable. - if run_loop_task is not None and run_loop_task.done(): - exc = run_loop_task.exception() - if exc is not None: - if isinstance(exc, AgentsException): - _reraise_workflow_failure(exc) - raise exc - - streamed_result.stream_events = _stream_events_with_rewrap # type: ignore[method-assign] - return streamed_result - - -def _reraise_workflow_failure(e: AgentsException) -> None: - """Rewrap an AgentsException whose cause is a Temporal workflow failure. - - Returns normally when ``e`` is not workflow-failure-bearing so the - caller can re-raise the original. - """ - if e.__cause__ and workflow.is_failure_exception(e.__cause__): - reraise = AgentsWorkflowError( - f"Workflow failure exception in Agents Framework: {e}" - ) - reraise.__traceback__ = e.__traceback__ - raise reraise from e.__cause__ - - -def _model_name(agent: Agent[Any]) -> str | None: - name = agent.model - if name is not None and not isinstance(name, str): - raise ValueError( - "Temporal workflows require a model name to be a string in the agent." - ) - return name diff --git a/temporalio/contrib/openai_agents/_otel_trace_interceptor.py b/temporalio/contrib/openai_agents/_otel_trace_interceptor.py deleted file mode 100644 index 63f8f9d83..000000000 --- a/temporalio/contrib/openai_agents/_otel_trace_interceptor.py +++ /dev/null @@ -1,88 +0,0 @@ -"""OTEL-aware variant of OpenAI Agents trace interceptor.""" - -from __future__ import annotations - -from typing import Any - -import opentelemetry.trace - -import temporalio.converter - -from ..opentelemetry._id_generator import TemporalIdGenerator -from ._trace_interceptor import ( - OpenAIAgentsContextPropagationInterceptor, - _InputWithHeaders, -) - - -class OTelOpenAIAgentsContextPropagationInterceptor( - OpenAIAgentsContextPropagationInterceptor -): - """OTEL-aware variant that enhances headers with OpenTelemetry span context.""" - - def __init__( - self, - otel_id_generator: TemporalIdGenerator, - payload_converter: temporalio.converter.PayloadConverter = temporalio.converter.default().payload_converter, - add_temporal_spans: bool = True, - ) -> None: - """Initialize OTEL-aware context propagation interceptor. - - Args: - otel_id_generator: Generator for OTEL-compatible IDs. - payload_converter: Converter for serializing trace context. - add_temporal_spans: Whether to add Temporal-specific spans. - """ - super().__init__(payload_converter, add_temporal_spans, start_traces=True) - self._otel_id_generator = otel_id_generator - - def header_contents(self) -> dict[str, Any]: - """Get header contents enhanced with OpenTelemetry span context. - - Returns: - Dictionary containing trace context with OTEL span information. - """ - otel_span = opentelemetry.trace.get_current_span() - - if otel_span and otel_span.get_span_context().is_valid: - span_context = otel_span.get_span_context() - return { - **super().header_contents(), - "otelSpanId": span_context.span_id, - "otelTraceId": span_context.trace_id, - } - else: - return super().header_contents() - - def context_from_header( - self, - input: _InputWithHeaders, - ): - """Extracts and initializes trace information the input header.""" - span_info = self.get_header_contents(input) - - if span_info is None: - return - otel_span_id = span_info.get("otelSpanId") - otel_trace_id = span_info.get("otelTraceId") - - # Seed the trace id before the trace is reconstructed so the workflow's root - # OTEL span shares the caller's trace id rather than generating a new one. - if otel_trace_id and self._otel_id_generator: - self._otel_id_generator.seed_trace_id(otel_trace_id) - - # If only a trace was propagated from the caller, we need to seed for trace context - if otel_span_id and self._otel_id_generator and span_info.get("spanId") is None: - self._otel_id_generator.seed_span_id(otel_span_id) - - super().trace_context_from_header_contents(span_info) - - # If a span was propagated from the caller, we need to seed for span context - if ( - otel_span_id - and self._otel_id_generator - and span_info.get("spanId") is not None - ): - self._otel_id_generator.seed_span_id(otel_span_id) - - super().span_context_from_header_contents(span_info) diff --git a/temporalio/contrib/openai_agents/_temporal_model_stub.py b/temporalio/contrib/openai_agents/_temporal_model_stub.py deleted file mode 100644 index d184daa4a..000000000 --- a/temporalio/contrib/openai_agents/_temporal_model_stub.py +++ /dev/null @@ -1,296 +0,0 @@ -from __future__ import annotations - -from collections.abc import AsyncIterator -from typing import Any - -from agents import ( - Agent, - AgentOutputSchema, - AgentOutputSchemaBase, - CodeInterpreterTool, - FileSearchTool, - FunctionTool, - Handoff, - HostedMCPTool, - ImageGenerationTool, - Model, - ModelResponse, - ModelSettings, - ModelTracing, - Tool, - TResponseInputItem, - WebSearchTool, -) -from agents.items import TResponseStreamEvent -from agents.tool import ( - ApplyPatchTool, - CustomTool, - LocalShellTool, - ShellTool, - ToolSearchTool, -) -from openai.types.responses.response_prompt_param import ResponsePromptParam - -from temporalio import workflow -from temporalio.contrib.openai_agents._invoke_model_activity import ( - ActivityModelInput, - AgentOutputSchemaInput, - ApplyPatchToolInput, - CustomToolInput, - FunctionToolInput, - HandoffInput, - HostedMCPToolInput, - ModelActivity, - ModelTracingInput, - ShellToolInput, - StreamingActivityModelInput, - ToolInput, -) -from temporalio.contrib.openai_agents._model_parameters import ModelActivityParameters - - -class _TemporalModelStub(Model): # type:ignore[reportUnusedClass] - """A stub that allows invoking models as Temporal activities.""" - - def __init__( - self, - model_name: str | None, - *, - model_params: ModelActivityParameters, - agent: Agent[Any] | None, - ) -> None: - self.model_name = model_name - self.model_params = model_params - self.agent = agent - - def _build_activity_input( - self, - *, - system_instructions: str | None, - input: str | list[TResponseInputItem], - model_settings: ModelSettings, - tools: list[Tool], - output_schema: AgentOutputSchemaBase | None, - handoffs: list[Handoff], - tracing: ModelTracing, - previous_response_id: str | None, - conversation_id: str | None, - prompt: ResponsePromptParam | None, - ) -> tuple[ActivityModelInput, str | None]: - def make_tool_info(tool: Tool) -> ToolInput: - if isinstance( - tool, - ( - FileSearchTool, - WebSearchTool, - ImageGenerationTool, - CodeInterpreterTool, - LocalShellTool, - ToolSearchTool, - ), - ): - return tool - elif isinstance(tool, ShellTool): - return ShellToolInput( - name=tool.name, - environment=tool.environment, - ) - elif isinstance(tool, ApplyPatchTool): - return ApplyPatchToolInput(name=tool.name) - elif isinstance(tool, HostedMCPTool): - return HostedMCPToolInput(tool_config=tool.tool_config) - elif isinstance(tool, CustomTool): - return CustomToolInput(tool_config=tool.tool_config) - elif isinstance(tool, FunctionTool): - return FunctionToolInput( - name=tool.name, - description=tool.description, - params_json_schema=tool.params_json_schema, - strict_json_schema=tool.strict_json_schema, - ) - else: - raise ValueError(f"Unsupported tool type: {tool.name}") - - tool_infos = [make_tool_info(x) for x in tools] - handoff_infos = [ - HandoffInput( - tool_name=x.tool_name, - tool_description=x.tool_description, - input_json_schema=x.input_json_schema, - agent_name=x.agent_name, - strict_json_schema=x.strict_json_schema, - ) - for x in handoffs - ] - if output_schema is not None and not isinstance( - output_schema, AgentOutputSchema - ): - raise TypeError( - f"Only AgentOutputSchema is supported by Temporal Model, got {type(output_schema).__name__}" - ) - agent_output_schema = output_schema - output_schema_input = ( - None - if agent_output_schema is None - else AgentOutputSchemaInput( - output_type_name=agent_output_schema.name(), - is_wrapped=agent_output_schema._is_wrapped, - output_schema=agent_output_schema.json_schema() - if not agent_output_schema.is_plain_text() - else None, - strict_json_schema=agent_output_schema.is_strict_json_schema(), - ) - ) - - activity_input = ActivityModelInput( - model_name=self.model_name, - system_instructions=system_instructions, - input=input, - model_settings=model_settings, - tools=tool_infos, - output_schema=output_schema_input, - handoffs=handoff_infos, - tracing=ModelTracingInput(tracing.value), - previous_response_id=previous_response_id, - conversation_id=conversation_id, - prompt=prompt, - ) - - if self.model_params.summary_override: - summary = ( - self.model_params.summary_override - if isinstance(self.model_params.summary_override, str) - else ( - self.model_params.summary_override.provide( - self.agent, system_instructions, input - ) - ) - ) - elif self.agent: - summary = self.agent.name - else: - summary = None - - return activity_input, summary - - async def get_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, - *, - previous_response_id: str | None, - conversation_id: str | None, - prompt: ResponsePromptParam | None, - ) -> ModelResponse: - activity_input, summary = self._build_activity_input( - system_instructions=system_instructions, - input=input, - model_settings=model_settings, - tools=tools, - output_schema=output_schema, - handoffs=handoffs, - tracing=tracing, - previous_response_id=previous_response_id, - conversation_id=conversation_id, - prompt=prompt, - ) - - if self.model_params.use_local_activity: - return await workflow.execute_local_activity_method( - ModelActivity.invoke_model_activity, - activity_input, - summary=summary, - schedule_to_close_timeout=self.model_params.schedule_to_close_timeout, - schedule_to_start_timeout=self.model_params.schedule_to_start_timeout, - start_to_close_timeout=self.model_params.start_to_close_timeout, - retry_policy=self.model_params.retry_policy, - cancellation_type=self.model_params.cancellation_type, - ) - return await workflow.execute_activity_method( - ModelActivity.invoke_model_activity, - activity_input, - summary=summary, - task_queue=self.model_params.task_queue, - schedule_to_close_timeout=self.model_params.schedule_to_close_timeout, - schedule_to_start_timeout=self.model_params.schedule_to_start_timeout, - start_to_close_timeout=self.model_params.start_to_close_timeout, - heartbeat_timeout=self.model_params.heartbeat_timeout, - retry_policy=self.model_params.retry_policy, - cancellation_type=self.model_params.cancellation_type, - versioning_intent=self.model_params.versioning_intent, - priority=self.model_params.priority, - ) - - 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, - *, - previous_response_id: str | None, - conversation_id: str | None, - prompt: ResponsePromptParam | None, - ) -> AsyncIterator[TResponseStreamEvent]: - # Streaming relies on activity heartbeats to detect a stuck LLM - # call and on WorkflowStreamClient.from_within_activity() to signal - # partial results back to the workflow. Local activities support - # neither: their result commits with the workflow task, so there - # is no independent task to heartbeat against or to send signals - # from. - if self.model_params.use_local_activity: - raise ValueError( - "Streaming is incompatible with use_local_activity " - "(local activities do not support heartbeats or the " - "workflow stream signal channel)." - ) - - topic = self.model_params.streaming_topic - if topic is None: - raise ValueError( - "Runner.run_streamed requires " - "ModelActivityParameters.streaming_topic to be set." - ) - - base_input, summary = self._build_activity_input( - system_instructions=system_instructions, - input=input, - model_settings=model_settings, - tools=tools, - output_schema=output_schema, - handoffs=handoffs, - tracing=tracing, - previous_response_id=previous_response_id, - conversation_id=conversation_id, - prompt=prompt, - ) - streaming_input: StreamingActivityModelInput = { - **base_input, - "streaming_topic": topic, - "streaming_batch_interval": self.model_params.streaming_batch_interval, - } - - events = await workflow.execute_activity_method( - ModelActivity.invoke_model_activity_streaming, - streaming_input, - summary=summary, - task_queue=self.model_params.task_queue, - schedule_to_close_timeout=self.model_params.schedule_to_close_timeout, - schedule_to_start_timeout=self.model_params.schedule_to_start_timeout, - start_to_close_timeout=self.model_params.start_to_close_timeout, - heartbeat_timeout=self.model_params.heartbeat_timeout, - retry_policy=self.model_params.retry_policy, - cancellation_type=self.model_params.cancellation_type, - versioning_intent=self.model_params.versioning_intent, - priority=self.model_params.priority, - ) - for event in events: - yield event diff --git a/temporalio/contrib/openai_agents/_temporal_openai_agents.py b/temporalio/contrib/openai_agents/_temporal_openai_agents.py deleted file mode 100644 index 6023ad090..000000000 --- a/temporalio/contrib/openai_agents/_temporal_openai_agents.py +++ /dev/null @@ -1,482 +0,0 @@ -"""Initialize Temporal OpenAI Agents overrides.""" - -import dataclasses -import json -import threading -import typing -from collections.abc import AsyncIterator, Callable, Collection, Iterator, Sequence -from contextlib import asynccontextmanager, contextmanager -from datetime import timedelta - -import pydantic -from agents import ModelProvider, Trace, set_trace_provider -from agents.run import get_default_agent_runner, set_default_agent_runner -from agents.tracing import get_trace_provider -from agents.tracing.provider import DefaultTraceProvider - -# construct_type is OpenAI's lenient (non-validating) model builder, the same -# one the SDK uses to parse live API responses. It is in a private module but -# has no public alias. -from openai._models import construct_type - -import temporalio.api.common.v1 -from temporalio.contrib.openai_agents._errors import AgentsWorkflowError -from temporalio.contrib.openai_agents._invoke_model_activity import ModelActivity -from temporalio.contrib.openai_agents._model_parameters import ModelActivityParameters -from temporalio.contrib.openai_agents._openai_runner import ( - TemporalOpenAIRunner, -) -from temporalio.contrib.openai_agents._temporal_trace_provider import ( - TemporalTraceProvider, -) -from temporalio.contrib.openai_agents._temporal_worker_env_ref import ( - AllowAllWorkerEnvVars, - _snapshot_resolvable_env_vars, -) -from temporalio.contrib.openai_agents._trace_interceptor import ( - OpenAIAgentsContextPropagationInterceptor, -) -from temporalio.contrib.opentelemetry._tracer_provider import ReplaySafeTracerProvider -from temporalio.contrib.pydantic import ( - PydanticJSONPlainPayloadConverter, - ToJsonOptions, -) -from temporalio.converter import ( - CompositePayloadConverter, - DataConverter, - DefaultPayloadConverter, - JSONPlainPayloadConverter, -) -from temporalio.plugin import SimplePlugin -from temporalio.worker import WorkflowRunner -from temporalio.worker.workflow_sandbox import SandboxedWorkflowRunner - -if typing.TYPE_CHECKING: - from temporalio.contrib.openai_agents import ( - SandboxClientProvider, - StatefulMCPServerProvider, - StatelessMCPServerProvider, - ) - - -_otel_trace_start_patch_lock = threading.RLock() -_otel_trace_start_patch_ref_count = 0 -_otel_trace_start_original: Callable[..., typing.Any] | None = None -_otel_trace_start_instrumentor: typing.Any | None = None - - -def _install_otel_instrumentation(tracer_provider: typing.Any) -> None: - """Configure OpenInference while at least one tracing context is active.""" - global _otel_trace_start_instrumentor - global _otel_trace_start_original - global _otel_trace_start_patch_ref_count - - from openinference.instrumentation.openai_agents import OpenAIAgentsInstrumentor - from openinference.instrumentation.openai_agents._processor import ( - OpenInferenceTracingProcessor, - ) - from opentelemetry.context import attach - from opentelemetry.trace import set_span_in_context - - with _otel_trace_start_patch_lock: - if _otel_trace_start_patch_ref_count == 0: - original_on_trace_start = OpenInferenceTracingProcessor.on_trace_start - _otel_trace_start_original = original_on_trace_start - - def on_trace_start(self: typing.Any, trace: Trace) -> None: # type: ignore[reportUnusedFunction] - original_on_trace_start(self, trace) - attach(set_span_in_context(self._root_spans[trace.trace_id])) - - setattr(OpenInferenceTracingProcessor, "on_trace_start", on_trace_start) - try: - _otel_trace_start_instrumentor = OpenAIAgentsInstrumentor() - _otel_trace_start_instrumentor.instrument( - tracer_provider=tracer_provider - ) - except BaseException: - setattr( - OpenInferenceTracingProcessor, - "on_trace_start", - _otel_trace_start_original, - ) - _otel_trace_start_original = None - _otel_trace_start_instrumentor = None - raise - _otel_trace_start_patch_ref_count += 1 - - -def _uninstall_otel_instrumentation() -> None: - """Tear down OpenInference after the final tracing context exits.""" - global _otel_trace_start_instrumentor - global _otel_trace_start_original - global _otel_trace_start_patch_ref_count - - from openinference.instrumentation.openai_agents._processor import ( - OpenInferenceTracingProcessor, - ) - - with _otel_trace_start_patch_lock: - if _otel_trace_start_patch_ref_count == 0: - raise RuntimeError("OpenInference instrumentation was not acquired") - _otel_trace_start_patch_ref_count -= 1 - if _otel_trace_start_patch_ref_count == 0: - try: - if _otel_trace_start_instrumentor is not None: - _otel_trace_start_instrumentor.uninstrument() - finally: - if _otel_trace_start_original is not None: - setattr( - OpenInferenceTracingProcessor, - "on_trace_start", - _otel_trace_start_original, - ) - _otel_trace_start_original = None - _otel_trace_start_instrumentor = None - - -@contextmanager -def _set_open_ai_agent_temporal_overrides( - model_params: ModelActivityParameters, - start_spans_in_replay: bool = False, -): - previous_runner = get_default_agent_runner() - previous_trace_provider = get_trace_provider() - provider = TemporalTraceProvider( - start_spans_in_replay=start_spans_in_replay, - ) - - try: - set_default_agent_runner(TemporalOpenAIRunner(model_params)) - set_trace_provider(provider) - yield provider - finally: - set_default_agent_runner(previous_runner) - set_trace_provider(previous_trace_provider or DefaultTraceProvider()) - - -def _lenient_construct(type_: typing.Any, value: typing.Any) -> typing.Any: - """Build ``value`` into ``type_`` without enforcing required fields. - - OpenAI's ``construct_type`` handles its own response models (and the - unions/lists thereof), but not the ``agents`` dataclasses that wrap them - (e.g. ``ModelResponse``), so the dataclass layer is reconstructed here and - each field delegated to ``construct_type``. ``include_extras`` preserves the - ``Annotated`` discriminators the unions rely on. - """ - if ( - isinstance(type_, type) - and dataclasses.is_dataclass(type_) - and isinstance(value, dict) - ): - hints = typing.get_type_hints(type_, include_extras=True) - return type_( - **{ - field.name: _lenient_construct( - hints.get(field.name, object), value[field.name] - ) - for field in dataclasses.fields(type_) - if field.name in value - } - ) - return construct_type(type_=type_, value=value) - - -class _OpenAIJSONPlainPayloadConverter(PydanticJSONPlainPayloadConverter): - """Strict pydantic deserialization with a lenient fallback. - - OpenAI's response models can drift from live API payloads (e.g. a - deprecated-but-required field the API has stopped sending). The SDK tolerates - this when parsing responses, but strict ``validate_json`` on the workflow - side does not, so fall back to lenient construction when validation fails. - """ - - def from_payload( - self, - payload: temporalio.api.common.v1.Payload, - type_hint: type | None = None, - ) -> typing.Any: - """See base class.""" - try: - return super().from_payload(payload, type_hint) - except pydantic.ValidationError: - if type_hint is None: - raise - return _lenient_construct(type_hint, json.loads(payload.data)) - - -class OpenAIPayloadConverter(CompositePayloadConverter): - """PayloadConverter for OpenAI agents.""" - - def __init__(self) -> None: - """Initialize a payload converter.""" - json_payload_converter = _OpenAIJSONPlainPayloadConverter( - ToJsonOptions(exclude_unset=True) - ) - super().__init__( - *( - c - if not isinstance(c, JSONPlainPayloadConverter) - else json_payload_converter - for c in DefaultPayloadConverter.default_encoding_payload_converters - ) - ) - - -def _data_converter(converter: DataConverter | None) -> DataConverter: - if converter is None: - return DataConverter(payload_converter_class=OpenAIPayloadConverter) - elif converter.payload_converter_class is DefaultPayloadConverter: - return dataclasses.replace( - converter, payload_converter_class=OpenAIPayloadConverter - ) - elif not isinstance(converter.payload_converter, OpenAIPayloadConverter): - raise ValueError( - "The payload converter must be of type OpenAIPayloadConverter." - ) - return converter - - -class OpenAIAgentsPlugin(SimplePlugin): - """Temporal plugin for integrating OpenAI agents with Temporal workflows. - - This plugin provides seamless integration between the OpenAI Agents SDK and - Temporal workflows. It automatically configures the necessary interceptors, - activities, and data converters to enable OpenAI agents to run within - Temporal workflows with proper tracing and model execution. - - The plugin: - 1. Configures the Pydantic data converter for type-safe serialization - 2. Sets up tracing interceptors for OpenAI agent interactions - 3. Registers model execution activities - 4. Automatically registers MCP server activities and manages their lifecycles - 5. Manages the OpenAI agent runtime overrides during worker execution - - Example: - >>> from temporalio.client import Client - >>> from temporalio.worker import Worker - >>> from temporalio.contrib.openai_agents import OpenAIAgentsPlugin, ModelActivityParameters, StatelessMCPServerProvider - >>> from agents.mcp import MCPServerStdio - >>> from datetime import timedelta - >>> - >>> # Configure model parameters - >>> model_params = ModelActivityParameters( - ... start_to_close_timeout=timedelta(seconds=30), - ... retry_policy=RetryPolicy(maximum_attempts=3) - ... ) - >>> - >>> # Create MCP servers - >>> filesystem_server = StatelessMCPServerProvider(MCPServerStdio( - ... name="Filesystem Server", - ... params={"command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "."]} - ... )) - >>> - >>> # Create plugin with MCP servers - >>> plugin = OpenAIAgentsPlugin( - ... model_params=model_params, - ... mcp_server_providers=[filesystem_server] - ... ) - >>> - >>> # Use with client and worker - >>> client = await Client.connect( - ... "localhost:7233", - ... plugins=[plugin] - ... ) - >>> worker = Worker( - ... client, - ... task_queue="my-task-queue", - ... workflows=[MyWorkflow], - ... ) - """ - - def __init__( - self, - model_params: ModelActivityParameters | None = None, - model_provider: ModelProvider | None = None, - mcp_server_providers: Sequence[ - "StatelessMCPServerProvider | StatefulMCPServerProvider" - ] = (), - sandbox_clients: Sequence["SandboxClientProvider"] = (), - register_activities: bool = True, - add_temporal_spans: bool = True, - use_otel_instrumentation: bool = False, - resolvable_worker_env_vars: Collection[str] | AllowAllWorkerEnvVars = (), - ) -> None: - """Initialize the OpenAI agents plugin. - - Args: - model_params: Configuration parameters for Temporal activity execution - of model calls. If None, default parameters will be used. - model_provider: Optional model provider for custom model implementations. - Useful for testing or custom model integrations. - mcp_server_providers: Sequence of MCP servers to automatically register with the worker. - Each server will be wrapped in a TemporalMCPServer if not already wrapped, - and their activities will be automatically registered with the worker. - The plugin manages the connection lifecycle of these servers. - sandbox_clients: Sequence of named sandbox client providers to register - on the worker. Each provider pairs a unique name with a real - ``BaseSandboxClient`` (e.g. ``DaytonaSandboxClient``, - ``UnixLocalSandboxClient``). On the workflow side, use - ``temporal_sandbox_client`` - with the matching name to target the correct backend. - Warning: sandbox_clients is experimental and behavior may change in future versions. - Use with caution in production environments. - register_activities: Whether to register activities during the worker execution. - This can be disabled on some workers to allow a separation of workflows and activities - but should not be disabled on all workers, or agents will not be able to progress. - add_temporal_spans: Whether to add temporal spans to traces - use_otel_instrumentation: If set to true, enable open telemetry instrumentation. - Warning: use_otel_instrumentation is experimental and behavior may change in future versions. - Use with caution in production environments. - resolvable_worker_env_vars: Names of the environment variables that - ``temporal_worker_env_ref()`` and ``TemporalWorkerEnvValue`` may - read on this worker. Names are matched exactly, with no globbing; - pass ``AllowAllWorkerEnvVars()`` in place of the names to allow - every variable. - Warning: resolvable_worker_env_vars is experimental and behavior may change in future versions. - Use with caution in production environments. - - """ - if model_params is None: - model_params = ModelActivityParameters() - - # For the default provider, we provide a default start_to_close_timeout of 60 seconds. - # Other providers will need to define their own. - if ( - model_params.start_to_close_timeout is None - and model_params.schedule_to_close_timeout is None - ): - if model_provider is None: - model_params.start_to_close_timeout = timedelta(seconds=60) - else: - raise ValueError( - "When configuring a custom provider, the model activity must have start_to_close_timeout or schedule_to_close_timeout" - ) - - self._use_otel_instrumentation = use_otel_instrumentation - - resolvable_env_vars = _snapshot_resolvable_env_vars(resolvable_worker_env_vars) - - # Delay activity construction until they are actually needed - def add_activities( - activities: Sequence[Callable] | None, - ) -> Sequence[Callable]: - if not register_activities: - return activities or [] - - model_activity = ModelActivity( - model_provider, resolvable_worker_env_vars=resolvable_env_vars - ) - new_activities = [ - model_activity.invoke_model_activity, - model_activity.invoke_model_activity_streaming, - ] - - server_names = [server.name for server in mcp_server_providers] - if len(server_names) != len(set(server_names)): - raise ValueError( - "More than one mcp server registered with the same name. Please provide unique names." - ) - - for mcp_server in mcp_server_providers: - new_activities.extend(mcp_server._get_activities()) - - sandbox_names = [sc.name for sc in sandbox_clients] - if len(sandbox_names) != len(set(sandbox_names)): - raise ValueError( - "More than one sandbox client registered with the same name. Please provide unique names." - ) - - for sandbox_provider in sandbox_clients: - new_activities.extend( - sandbox_provider._get_activities(resolvable_env_vars) - ) - - return list(activities or []) + new_activities - - def workflow_runner(runner: WorkflowRunner | None) -> WorkflowRunner: - if not runner: - raise ValueError("No WorkflowRunner provided to the OpenAI plugin.") - - # If in sandbox, add additional passthrough - if isinstance(runner, SandboxedWorkflowRunner): - return dataclasses.replace( - runner, - restrictions=runner.restrictions.with_passthrough_modules( - "openai", "agents", "mcp" - ), - ) - return runner - - if not use_otel_instrumentation: - interceptor = OpenAIAgentsContextPropagationInterceptor( - add_temporal_spans=add_temporal_spans, - ) - else: - from opentelemetry import trace as otel_trace - - from ._otel_trace_interceptor import ( - OTelOpenAIAgentsContextPropagationInterceptor, - ) - - provider = otel_trace.get_tracer_provider() - if not isinstance(provider, ReplaySafeTracerProvider): - raise ValueError( - "Global tracer provider must a ReplaySafeTracerProvider. Use temporalio.contrib.opentelemtry.create_trace_provider to create one." - ) - - interceptor = OTelOpenAIAgentsContextPropagationInterceptor( - add_temporal_spans=add_temporal_spans, - otel_id_generator=provider.id_generator(), - ) - - @asynccontextmanager - async def run_context() -> AsyncIterator[None]: - with self.tracing_context(): - with _set_open_ai_agent_temporal_overrides( - model_params, - start_spans_in_replay=use_otel_instrumentation, - ): - yield - - super().__init__( - name="OpenAIAgentsPlugin", - data_converter=_data_converter, - interceptors=[interceptor], - activities=add_activities, - workflow_runner=workflow_runner, - workflow_failure_exception_types=[AgentsWorkflowError], - run_context=lambda: run_context(), - ) - - @contextmanager - def tracing_context(self) -> Iterator[None]: - """Context manager for setting up OpenAI Agents tracing instrumentation. - - This should be called if AgentsSDK traces and/or spans are started outside of the context of a worker. - For example: - - .. code-block:: python - - with env.openai_agents_plugin.tracing_context(): - with trace("External trace"): - with custom_span("External span"): - workflow_handle = await new_client.start_workflow( - ... - ) - - Yields: - Context with tracing instrumentation enabled. - """ - # Set up OTEL instrumentation if enabled - otel_instrumentation_installed = False - if self._use_otel_instrumentation: - from opentelemetry import trace - - _install_otel_instrumentation(trace.get_tracer_provider()) - otel_instrumentation_installed = True - try: - yield - finally: - # Clean up OTEL instrumentation - if otel_instrumentation_installed: - _uninstall_otel_instrumentation() diff --git a/temporalio/contrib/openai_agents/_temporal_trace_provider.py b/temporalio/contrib/openai_agents/_temporal_trace_provider.py deleted file mode 100644 index 347473545..000000000 --- a/temporalio/contrib/openai_agents/_temporal_trace_provider.py +++ /dev/null @@ -1,211 +0,0 @@ -"""Provides support for integration with OpenAI Agents SDK tracing across workflows""" - -import uuid -from types import TracebackType -from typing import Any, cast - -from agents import SpanData, Trace, TracingProcessor -from agents.tracing import ( - get_trace_provider, -) -from agents.tracing.provider import ( - DefaultTraceProvider, - SynchronousMultiTracingProcessor, -) -from agents.tracing.spans import Span - -import temporalio.workflow -from temporalio import workflow -from temporalio.workflow import ReadOnlyContextError - - -class ActivitySpanData(SpanData): - """Captures fields from ActivityTaskScheduledEventAttributes for tracing.""" - - def __init__( - self, - activity_id: str, - activity_type: str, - task_queue: str, - schedule_to_close_timeout: float | None = None, - schedule_to_start_timeout: float | None = None, - start_to_close_timeout: float | None = None, - heartbeat_timeout: float | None = None, - ): - """Initialize an ActivitySpanData instance.""" - self.activity_id = activity_id - self.activity_type = activity_type - self.task_queue = task_queue - self.schedule_to_close_timeout = schedule_to_close_timeout - self.schedule_to_start_timeout = schedule_to_start_timeout - self.start_to_close_timeout = start_to_close_timeout - self.heartbeat_timeout = heartbeat_timeout - - @property - def type(self) -> str: - """Return the type of this span data.""" - return "temporal-activity" - - def export(self) -> dict[str, Any]: - """Export the span data as a dictionary.""" - return { - "type": self.type, - "activity_id": self.activity_id, - "activity_type": self.activity_type, - "task_queue": self.task_queue, - "schedule_to_close_timeout": self.schedule_to_close_timeout, - "schedule_to_start_timeout": self.schedule_to_start_timeout, - "start_to_close_timeout": self.start_to_close_timeout, - "heartbeat_timeout": self.heartbeat_timeout, - } - - -def activity_span( - activity_id: str, - activity_type: str, - task_queue: str, - start_to_close_timeout: float, -) -> Span[ActivitySpanData]: - """Create a trace span for a Temporal activity.""" - return get_trace_provider().create_span( - span_data=ActivitySpanData( - activity_id=activity_id, - activity_type=activity_type, - task_queue=task_queue, - start_to_close_timeout=start_to_close_timeout, - ), - ) - - -class _TemporalTracingProcessor(SynchronousMultiTracingProcessor): - def __init__( - self, - impl: SynchronousMultiTracingProcessor, - start_spans_in_replay: bool, - ): - super().__init__() - self._impl = impl - self._emit_spans_in_replay = start_spans_in_replay - - def add_tracing_processor(self, tracing_processor: TracingProcessor): - self._impl.add_tracing_processor(tracing_processor) - - def set_processors(self, processors: list[TracingProcessor]): - self._impl.set_processors(processors) - - def on_trace_start(self, trace: Trace) -> None: - if not self._emit_spans_in_replay: - if workflow.in_workflow() and workflow.unsafe.is_replaying_history_events(): - # In replay mode, don't report - return - - self._impl.on_trace_start(trace) - - def on_trace_end(self, trace: Trace) -> None: - if not self._emit_spans_in_replay: - if workflow.in_workflow() and workflow.unsafe.is_replaying_history_events(): - # In replay mode, don't report - return - - self._impl.on_trace_end(trace) - - def on_span_start(self, span: Span[Any]) -> None: - if not self._emit_spans_in_replay: - if workflow.in_workflow() and workflow.unsafe.is_replaying_history_events(): - # In replay mode, don't report - return - self._impl.on_span_start(span) - - def on_span_end(self, span: Span[Any]) -> None: - if not self._emit_spans_in_replay: - if workflow.in_workflow() and workflow.unsafe.is_replaying_history_events(): - # In replay mode, don't report - return - - self._impl.on_span_end(span) - - def shutdown(self, timeout: float | None = None) -> None: - self._impl.shutdown(timeout) - - def force_flush(self) -> None: - self._impl.force_flush() - - -def _workflow_uuid() -> str: - if ( - getattr( - temporalio.workflow.instance(), "__temporal_openai_tracing_random", None - ) - is None - ): - setattr( - temporalio.workflow.instance(), - "__temporal_openai_tracing_random", - temporalio.workflow.new_random(), - ) - random = getattr(temporalio.workflow.instance(), "__temporal_openai_tracing_random") - return uuid.UUID( - bytes=random.getrandbits(16 * 8).to_bytes(16, "big"), version=4 - ).hex[:24] - - -class TemporalTraceProvider(DefaultTraceProvider): - """A trace provider that integrates with Temporal workflows.""" - - def __init__(self, start_spans_in_replay: bool = False): - """Initialize the TemporalTraceProvider.""" - super().__init__() - self._original_provider = cast(DefaultTraceProvider, get_trace_provider()) - self._multi_processor = _TemporalTracingProcessor( - self._original_provider._multi_processor, - start_spans_in_replay, - ) - - def time_iso(self) -> str: - """Return the current deterministic time in ISO 8601 format.""" - if workflow.in_workflow(): - return workflow.now().isoformat() - return super().time_iso() - - def gen_trace_id(self) -> str: - """Generate a new trace ID.""" - if workflow.in_workflow(): - try: - """Generate a new trace ID.""" - return f"trace_{_workflow_uuid()}" - except ReadOnlyContextError: - return f"trace_{uuid.uuid4().hex}" - return super().gen_trace_id() - - def gen_span_id(self) -> str: - """Generate a span ID.""" - if workflow.in_workflow(): - try: - """Generate a deterministic span ID.""" - return f"span_{_workflow_uuid()}" - except ReadOnlyContextError: - return f"span_{uuid.uuid4().hex[:24]}" - return super().gen_span_id() - - def gen_group_id(self) -> str: - """Generate a group ID.""" - if workflow.in_workflow(): - try: - """Generate a deterministic group ID.""" - return f"group_{_workflow_uuid()}" - except ReadOnlyContextError: - return f"group_{uuid.uuid4().hex[:24]}" - return super().gen_group_id() - - def __enter__(self): - """Enter the context of the Temporal trace provider.""" - return self - - def __exit__( - self, - exc_type: type[BaseException], - exc_val: BaseException, - exc_tb: TracebackType, - ): - """Exit the context of the Temporal trace provider.""" - self._multi_processor.shutdown() diff --git a/temporalio/contrib/openai_agents/_temporal_worker_env_ref.py b/temporalio/contrib/openai_agents/_temporal_worker_env_ref.py deleted file mode 100644 index ab8e82625..000000000 --- a/temporalio/contrib/openai_agents/_temporal_worker_env_ref.py +++ /dev/null @@ -1,177 +0,0 @@ -"""Worker-environment secrets: the reference form, and the allowlist both forms share.""" - -from __future__ import annotations - -import dataclasses -import os -import re -from collections.abc import Collection, Mapping, MutableMapping -from typing import Any, cast - -from agents.tool import ShellToolContainerAutoEnvironment, ShellToolEnvironment -from openai.types.responses.tool_param import CodeInterpreter, Mcp - -_REF_PREFIX = "temporal.worker_env_ref:" - -_REF_PATTERN = re.compile(re.escape(_REF_PREFIX) + r"\{([^}{]*)\}") - - -@dataclasses.dataclass(frozen=True) -class AllowAllWorkerEnvVars: - """Make every environment variable on the worker resolvable. - - .. warning:: - This class is experimental and may change in future versions. - Use with caution in production environments. - - Pass an instance in place of a list of names:: - - OpenAIAgentsPlugin(resolvable_worker_env_vars=AllowAllWorkerEnvVars()) - - This grants far more on the sandbox form than on the hosted tool form. A - sandbox manifest is written in workflow code, so allowing every name lets a - workflow name any variable on the worker and have its value set inside the - sandbox container, where a shell command the model composes can read it. - """ - - -def _snapshot_resolvable_env_vars( - resolvable_worker_env_vars: Collection[str] | AllowAllWorkerEnvVars, -) -> frozenset[str] | AllowAllWorkerEnvVars: - if isinstance(resolvable_worker_env_vars, AllowAllWorkerEnvVars): - return resolvable_worker_env_vars - if isinstance(resolvable_worker_env_vars, str): - raise TypeError( - "resolvable_worker_env_vars takes a collection of environment variable " - 'names, such as ["MY_MCP_TOKEN"], or AllowAllWorkerEnvVars(). A single ' - "string is read as the collection of its characters, so pass a list even " - "for one name." - ) - if cast(object, resolvable_worker_env_vars) is AllowAllWorkerEnvVars: - raise TypeError( - "resolvable_worker_env_vars takes an AllowAllWorkerEnvVars instance, not " - "the class itself. Pass AllowAllWorkerEnvVars()." - ) - return frozenset(resolvable_worker_env_vars) - - -def _is_resolvable( - resolvable: frozenset[str] | AllowAllWorkerEnvVars, name: str -) -> bool: - return isinstance(resolvable, AllowAllWorkerEnvVars) or name in resolvable - - -def temporal_worker_env_ref(name: str) -> str: - """Refer to a secret held in the Temporal Worker's environment. - - .. warning:: - This function is experimental and may change in future versions. - Use with caution in production environments. - - Use it for a hosted tool credential that should come from the worker's - environment rather than being written into your workflow. Put the returned - reference in a ``HostedMCPTool``'s ``authorization`` or header value, or in - the ``value`` of a ``domain_secrets`` entry under a ``ShellTool`` or - ``CodeInterpreterTool``. The reference carries only the variable's name, - never its value. - - Every worker that runs model activities must set the variable and name it in - ``OpenAIAgentsPlugin(resolvable_worker_env_vars=[...])``. A name the worker - allows but has not set resolves to an empty value. - - Args: - name: Name of the environment variable to read on the worker. - - Returns: - A reference string to use in place of the secret. - """ - return f"{_REF_PREFIX}{{{name}}}" - - -class _WorkerEnvRefResolver: # type:ignore[reportUnusedClass] - def __init__( - self, - resolvable_worker_env_vars: Collection[str] | AllowAllWorkerEnvVars, - ) -> None: - self._allowed = _snapshot_resolvable_env_vars(resolvable_worker_env_vars) - - def _resolve_ref(self, value: str) -> str: - def substitute(match: re.Match[str]) -> str: - name = match.group(1) - if not _is_resolvable(self._allowed, name): - return match.group(0) - return os.environ.get(name, "") - - return _REF_PATTERN.sub(substitute, value) - - def _resolve_domain_secret(self, secret: Mapping[str, Any]) -> dict[str, Any]: - return {**secret, "value": self._resolve_ref(secret["value"])} - - def _resolve_network_policy(self, network_policy: Any) -> Any: - policy = cast(MutableMapping[str, Any], network_policy) - domain_secrets = policy.get("domain_secrets") - if domain_secrets is None: - return network_policy - unresolved = list(domain_secrets) - # On the code interpreter path pydantic deserializes domain_secrets into a - # single-pass iterator, so the entries read here go back onto the input. - policy["domain_secrets"] = unresolved - return { - **policy, - "domain_secrets": [ - self._resolve_domain_secret(secret) for secret in unresolved - ], - } - - def resolve_mcp_tool_config(self, tool_config: Mcp) -> Mcp: - resolved: Mcp = tool_config - if "authorization" in resolved: - resolved = { - **resolved, - "authorization": self._resolve_ref(resolved["authorization"]), - } - headers = resolved.get("headers") - if headers is not None: - resolved = { - **resolved, - "headers": { - name: self._resolve_ref(value) for name, value in headers.items() - }, - } - return resolved - - def resolve_shell_tool_environment( - self, - environment: ShellToolEnvironment | None, - ) -> ShellToolEnvironment: - """An absent environment comes back as the local one ``ShellTool`` normalizes it to.""" - if environment is None: - return {"type": "local"} - if environment.get("type") != "container_auto": - return environment - auto = cast(ShellToolContainerAutoEnvironment, environment) - network_policy = auto.get("network_policy") - if network_policy is None: - return environment - return { - **auto, - "network_policy": self._resolve_network_policy(network_policy), - } - - def resolve_code_interpreter_tool_config( - self, - tool_config: CodeInterpreter, - ) -> CodeInterpreter: - container = tool_config.get("container") - if not isinstance(container, Mapping): - return tool_config - network_policy = container.get("network_policy") - if network_policy is None: - return tool_config - return { - **tool_config, - "container": { - **container, - "network_policy": self._resolve_network_policy(network_policy), - }, - } diff --git a/temporalio/contrib/openai_agents/_trace_interceptor.py b/temporalio/contrib/openai_agents/_trace_interceptor.py deleted file mode 100644 index 66297e20b..000000000 --- a/temporalio/contrib/openai_agents/_trace_interceptor.py +++ /dev/null @@ -1,445 +0,0 @@ -"""Adds OpenAI Agents traces and spans to Temporal workflows and activities.""" - -from __future__ import annotations - -import abc -from collections.abc import Mapping -from contextlib import contextmanager -from typing import Any, Protocol - -from agents import CustomSpanData, custom_span, get_current_span, trace -from agents.tracing import ( - get_trace_provider, -) -from agents.tracing.scope import Scope -from agents.tracing.spans import Span - -import temporalio.api.common.v1 -import temporalio.client -import temporalio.converter -import temporalio.worker -import temporalio.workflow -from temporalio import activity - -HEADER_KEY = "__openai_span" - - -class _InputWithHeaders(Protocol): - headers: Mapping[str, temporalio.api.common.v1.Payload] - - -@contextmanager -def temporal_span( - add_temporal_spans: bool, - span_name: str, -): - """Create a temporal span context manager. - - Args: - add_temporal_spans: Whether to add temporal-specific span data. - span_name: The name of the span to create. - - Yields: - A span context with temporal metadata if enabled. - """ - if add_temporal_spans: - """Extracts and initializes trace information the input header.""" - data = ( - { - "activityId": activity.info().activity_id, - "activity": activity.info().activity_type, - } - if activity.in_activity() - else None - ) - current_span = get_trace_provider().get_current_span() - - with custom_span(name=span_name, parent=current_span, data=data): - yield - else: - yield - - -class OpenAIAgentsContextPropagationInterceptor( - temporalio.client.Interceptor, temporalio.worker.Interceptor -): - """Interceptor that propagates OpenAI agent tracing context through Temporal workflows and activities. - - This interceptor enables tracing of OpenAI agent operations across Temporal workflows - and activities. It propagates trace context through workflow and activity boundaries, - allowing for end-to-end tracing of agent operations. - - The interceptor handles: - 1. Propagating trace context from client to workflow - 2. Propagating trace context from workflow to activities - 3. Maintaining trace context across workflow and activity boundaries - - Example usage: - interceptor = OpenAIAgentsTracingInterceptor() - client = await Client.connect("localhost:7233", interceptors=[interceptor]) - worker = Worker(client, task_queue="my-task-queue", interceptors=[interceptor]) - """ - - def __init__( - self, - payload_converter: temporalio.converter.PayloadConverter = temporalio.converter.default().payload_converter, - add_temporal_spans: bool = True, - start_traces: bool = False, - ) -> None: - """Initialize the interceptor with a payload converter. - - Args: - payload_converter: The payload converter to use for serializing/deserializing - trace context. Defaults to the default Temporal payload converter. - add_temporal_spans: Whether to add temporal-specific spans to traces. - start_traces: Whether to start new traces if none exist. This will cause duplication if the underlying - trace provider actually process start events. Primarily designed for use with Open Telemetry integration. - """ - super().__init__() - self._payload_converter = payload_converter - self._start_traces = start_traces - self._add_temporal_spans = add_temporal_spans - - def intercept_client( - self, next: temporalio.client.OutboundInterceptor - ) -> temporalio.client.OutboundInterceptor: - """Intercepts client calls to propagate trace context. - - Args: - next: The next interceptor in the chain. - - Returns: - An interceptor that propagates trace context for client operations. - """ - return _ContextPropagationClientOutboundInterceptor(next, self) - - def intercept_activity( - self, next: temporalio.worker.ActivityInboundInterceptor - ) -> temporalio.worker.ActivityInboundInterceptor: - """Intercepts activity calls to propagate trace context. - - Args: - next: The next interceptor in the chain. - - Returns: - An interceptor that propagates trace context for activity operations. - """ - return _ContextPropagationActivityInboundInterceptor(next, self) - - def workflow_interceptor_class( - self, input: temporalio.worker.WorkflowInterceptorClassInput - ) -> type[_ContextPropagationWorkflowInboundInterceptor]: - """Returns the workflow interceptor class to propagate trace context. - - Args: - input: The input for creating the workflow interceptor. - - Returns: - The class of the workflow interceptor that propagates trace context. - """ - _root = self - - class ModifiedInterceptor(_ContextPropagationWorkflowInboundInterceptor): - def root(self): - return _root - - return ModifiedInterceptor - - def set_header_from_context(self, input: _InputWithHeaders) -> None: - """Inserts the OpenAI Agents trace/span data in the input header.""" - input.headers = { - **input.headers, - HEADER_KEY: temporalio.converter.PayloadConverter.default.to_payload( - self.header_contents() - ), - } - - def header_contents(self) -> dict[str, Any]: - """Gets the OpenAI Agents trace/span data for the input header.""" - current = get_current_span() - trace = get_trace_provider().get_current_trace() - return { - "traceName": trace.name if trace else "Unknown Workflow", - "spanId": current.span_id if current else None, - "traceId": trace.trace_id if trace else None, - } - - def get_header_contents(self, input: _InputWithHeaders) -> dict[str, Any] | None: - """Extract trace context information from input headers. - - Args: - input: Input with headers containing trace information. - - Returns: - Dictionary containing trace context or None if no headers present. - """ - payload = input.headers.get(HEADER_KEY) - return self._payload_converter.from_payload(payload) if payload else None - - def trace_context_from_header_contents(self, span_info: dict[str, Any]): - """Initialize trace context from header contents. - - Args: - span_info: Dictionary containing trace information from headers. - """ - current_trace = get_trace_provider().get_current_trace() - if current_trace is None and span_info["traceId"] is not None: - current_trace = trace( - span_info["traceName"], - trace_id=span_info["traceId"], - ) - - if self._start_traces: - current_trace.start(mark_as_current=True) - else: - Scope.set_current_trace(current_trace) - - def span_context_from_header_contents(self, span_info: dict[str, Any]): - """Initialize span context from header contents. - - Args: - span_info: Dictionary containing span information from headers. - """ - current_span = get_trace_provider().get_current_span() - if current_span is None and span_info["spanId"] is not None: - current_span = get_trace_provider().create_span( - span_data=CustomSpanData(name="", data={}), span_id=span_info["spanId"] - ) - if self._start_traces: - current_span.start(mark_as_current=True) - else: - Scope.set_current_span(current_span) - - def context_from_header( - self, - input: _InputWithHeaders, - ): - """Extracts and initializes trace information the input header.""" - span_info = self.get_header_contents(input) - if span_info is None: - return - - self.trace_context_from_header_contents(span_info) - self.span_context_from_header_contents(span_info) - - @contextmanager - def maybe_span(self, span_name: str, data: dict[str, Any] | None): - """Context manager that conditionally creates a span. - - Args: - span_name: Name for the span. - data: Optional data to attach to the span. - - Yields: - Context with optional span tracking. - """ - if ( - self._add_temporal_spans - and get_trace_provider().get_current_trace() is not None - ): - with custom_span(name=span_name, data=data): - yield - else: - yield - - -class _ContextPropagationClientOutboundInterceptor( - temporalio.client.OutboundInterceptor -): - def __init__( - self, - next: temporalio.client.OutboundInterceptor, - root: OpenAIAgentsContextPropagationInterceptor, - ) -> None: - super().__init__(next) - self._root = root - - async def start_workflow( - self, input: temporalio.client.StartWorkflowInput - ) -> temporalio.client.WorkflowHandle[Any, Any]: - data = {"workflowId": input.id} if input.id else None - span_name = "temporal:startWorkflow" - with self._root.maybe_span( - span_name + ":" + input.workflow, - data=data, - ): - self._root.set_header_from_context(input) - return await super().start_workflow(input) - - async def query_workflow(self, input: temporalio.client.QueryWorkflowInput) -> Any: - data = {"workflowId": input.id, "query": input.query} - span_name = "temporal:queryWorkflow" - with self._root.maybe_span( - span_name, - data=data, - ): - self._root.set_header_from_context(input) - return await super().query_workflow(input) - - async def signal_workflow( - self, input: temporalio.client.SignalWorkflowInput - ) -> None: - data = {"workflowId": input.id, "signal": input.signal} - span_name = "temporal:signalWorkflow" - with self._root.maybe_span( - span_name, - data=data, - ): - self._root.set_header_from_context(input) - await super().signal_workflow(input) - - async def start_workflow_update( - self, input: temporalio.client.StartWorkflowUpdateInput - ) -> temporalio.client.WorkflowUpdateHandle[Any]: - data = { - **({"workflowId": input.id} if input.id else {}), - "update": input.update, - } - span_name = "temporal:updateWorkflow" - with self._root.maybe_span( - span_name, - data=data, - ): - self._root.set_header_from_context(input) - return await self.next.start_workflow_update(input) - - -class _ContextPropagationActivityInboundInterceptor( - temporalio.worker.ActivityInboundInterceptor -): - def __init__( - self, - next: temporalio.worker.ActivityInboundInterceptor, - root: OpenAIAgentsContextPropagationInterceptor, - ) -> None: - super().__init__(next) - self._root = root - - async def execute_activity( - self, input: temporalio.worker.ExecuteActivityInput - ) -> Any: - self._root.context_from_header(input) - with temporal_span(self._root._add_temporal_spans, "temporal:executeActivity"): - return await self.next.execute_activity(input) - - -class _ContextPropagationWorkflowInboundInterceptor( - temporalio.worker.WorkflowInboundInterceptor, abc.ABC -): - @abc.abstractmethod - def root(self): - raise NotImplementedError - - def init(self, outbound: temporalio.worker.WorkflowOutboundInterceptor) -> None: - _root = self.root() - - class ModifiedInterceptor(_ContextPropagationWorkflowOutboundInterceptor): - def root(self): - return _root - - self.next.init(ModifiedInterceptor(outbound)) - - async def execute_workflow( - self, input: temporalio.worker.ExecuteWorkflowInput - ) -> Any: - self.root().context_from_header(input) - with temporal_span(self.root()._add_temporal_spans, "temporal:executeWorkflow"): - return await self.next.execute_workflow(input) - - async def handle_signal(self, input: temporalio.worker.HandleSignalInput) -> None: - self.root().context_from_header(input) - with temporal_span(self.root()._add_temporal_spans, "temporal:handleSignal"): - return await self.next.handle_signal(input) - - async def handle_query(self, input: temporalio.worker.HandleQueryInput) -> Any: - with temporal_span(self.root()._add_temporal_spans, "temporal:handleQuery"): - return await self.next.handle_query(input) - - def handle_update_validator( - self, input: temporalio.worker.HandleUpdateInput - ) -> None: - self.root().context_from_header(input) - self.next.handle_update_validator(input) - - async def handle_update_handler( - self, input: temporalio.worker.HandleUpdateInput - ) -> Any: - self.root().context_from_header(input) - return await self.next.handle_update_handler(input) - - -class _ContextPropagationWorkflowOutboundInterceptor( - temporalio.worker.WorkflowOutboundInterceptor, abc.ABC -): - @abc.abstractmethod - def root(self): - raise NotImplementedError - - async def signal_child_workflow( - self, input: temporalio.worker.SignalChildWorkflowInput - ) -> None: - with self.root().maybe_span( - "temporal:signalChildWorkflow", - data={"workflowId": input.child_workflow_id}, - ): - self.root().set_header_from_context(input) - await self.next.signal_child_workflow(input) - - async def signal_external_workflow( - self, input: temporalio.worker.SignalExternalWorkflowInput - ) -> None: - with self.root().maybe_span( - "temporal:signalExternalWorkflow", - data={"workflowId": input.workflow_id}, - ): - self.root().set_header_from_context(input) - await self.next.signal_external_workflow(input) - - def start_activity( - self, input: temporalio.worker.StartActivityInput - ) -> temporalio.workflow.ActivityHandle: - trace = get_trace_provider().get_current_trace() - span: Span | None = None - if trace and self.root()._add_temporal_spans: - span = custom_span( - name="temporal:startActivity", data={"activity": input.activity} - ) - span.start(mark_as_current=True) - - self.root().set_header_from_context(input) - handle = self.next.start_activity(input) - if span: - handle.add_done_callback(lambda _: span.finish()) # type: ignore - return handle - - async def start_child_workflow( - self, input: temporalio.worker.StartChildWorkflowInput - ) -> temporalio.workflow.ChildWorkflowHandle: - trace = get_trace_provider().get_current_trace() - span: Span | None = None - if trace and self.root()._add_temporal_spans: - span = custom_span( - name="temporal:startChildWorkflow", data={"workflow": input.workflow} - ) - span.start(mark_as_current=True) - self.root().set_header_from_context(input) - handle = await self.next.start_child_workflow(input) - if span: - handle.add_done_callback(lambda _: span.finish()) # type: ignore - return handle - - def start_local_activity( - self, input: temporalio.worker.StartLocalActivityInput - ) -> temporalio.workflow.ActivityHandle: - trace = get_trace_provider().get_current_trace() - span: Span | None = None - if trace and self.root()._add_temporal_spans: - span = custom_span( - name="temporal:startLocalActivity", data={"activity": input.activity} - ) - span.start(mark_as_current=True) - self.root().set_header_from_context(input) - handle = self.next.start_local_activity(input) - if span: - handle.add_done_callback(lambda _: span.finish()) # type: ignore - return handle diff --git a/temporalio/contrib/openai_agents/sandbox/__init__.py b/temporalio/contrib/openai_agents/sandbox/__init__.py deleted file mode 100644 index 632b264cd..000000000 --- a/temporalio/contrib/openai_agents/sandbox/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Sandbox support for Temporal OpenAI Agents plugin.""" diff --git a/temporalio/contrib/openai_agents/sandbox/_sandbox_client_provider.py b/temporalio/contrib/openai_agents/sandbox/_sandbox_client_provider.py deleted file mode 100644 index f16895255..000000000 --- a/temporalio/contrib/openai_agents/sandbox/_sandbox_client_provider.py +++ /dev/null @@ -1,304 +0,0 @@ -"""Public-facing provider that pairs a name with a real sandbox client.""" - -from __future__ import annotations - -import io -from collections.abc import Callable, Collection, Iterator, Sequence -from contextlib import contextmanager -from pathlib import Path -from typing import Any - -from agents.sandbox.errors import SandboxError -from agents.sandbox.session.sandbox_client import BaseSandboxClient -from agents.sandbox.session.sandbox_session import SandboxSession - -from temporalio import activity -from temporalio.contrib.openai_agents._temporal_worker_env_ref import ( - AllowAllWorkerEnvVars, - _snapshot_resolvable_env_vars, -) -from temporalio.contrib.openai_agents.sandbox._temporal_activity_models import ( - CreateSessionArgs, - ExecArgs, - HydrateWorkspaceArgs, - PersistWorkspaceArgs, - PersistWorkspaceResult, - PtyExecStartArgs, - PtyExecUpdateResult, - PtyWriteStdinArgs, - ReadArgs, - ReadResult, - ResumeSessionArgs, - RunningArgs, - RunningResult, - SessionResult, - StartArgs, - StopArgs, - WriteArgs, - _HasState, -) -from temporalio.contrib.openai_agents.sandbox._temporal_activity_models import ( - ExecResult as ExecResultModel, -) -from temporalio.contrib.openai_agents.sandbox._temporal_worker_env_value import ( - _resolvable_worker_env_vars_scope, -) -from temporalio.exceptions import ApplicationError - - -@contextmanager -def _translate_sandbox_errors() -> Iterator[None]: - # Temporal retries every activity exception by default, so only a SandboxError - # the library has classified as terminal (retryable is False) is turned into a - # non-retryable ApplicationError. - try: - yield - except SandboxError as e: - if e.retryable is False: - raise ApplicationError( - str(e), type=str(e.error_code), non_retryable=True - ) from e - raise - - -@contextmanager -def _sandbox_activity_scope( - resolvable_worker_env_vars: frozenset[str] | AllowAllWorkerEnvVars, -) -> Iterator[None]: - with _resolvable_worker_env_vars_scope(resolvable_worker_env_vars): - with _translate_sandbox_errors(): - yield - - -class SandboxClientProvider: - """A named sandbox client provider for Temporal workflows. - - .. warning:: - This class is experimental and may change in future versions. - Use with caution in production environments. - - Wraps a ``BaseSandboxClient`` with a unique name so that multiple - sandbox backends can be registered on a single Temporal worker. Each - provider gets its own set of Temporal activities whose names are prefixed - with the provider name, allowing them to coexist on the same task queue. - - On the **worker side**, pass one or more providers to the plugin:: - - plugin = OpenAIAgentsPlugin( - sandbox_clients=[ - SandboxClientProvider("daytona", DaytonaSandboxClient()), - SandboxClientProvider("local", UnixLocalSandboxClient()), - ], - ) - - On the **workflow side**, reference a provider by name via - :func:`temporalio.contrib.openai_agents.workflow.temporal_sandbox_client`:: - - run_config = RunConfig( - sandbox=SandboxRunConfig( - client=temporal_sandbox_client("daytona"), - ... - ), - ) - - Args: - name: A unique name for this sandbox backend (e.g. ``"daytona"``, - ``"local"``). Must match the name used on the workflow side. - client: The real ``BaseSandboxClient`` that performs sandbox - lifecycle and I/O operations on the worker. - """ - - def __init__(self, name: str, client: BaseSandboxClient[Any]) -> None: - """Initialize the provider.""" - self._name = name - self._client = client - self._sessions: dict[str, SandboxSession] = {} - - @property - def name(self) -> str: - """The provider name used as an activity-name prefix.""" - return self._name - - async def _session(self, args: _HasState) -> SandboxSession: - key = str(args.state.session_id) - if key not in self._sessions: - self._sessions[key] = await self._client.resume(args.state) - return self._sessions[key] - - def _get_activities( - self, resolvable_worker_env_vars: Collection[str] | AllowAllWorkerEnvVars - ) -> Sequence[Callable[..., Any]]: - """Return all activity callables for registration with a Temporal Worker.""" - prefix = self._name - resolvable = _snapshot_resolvable_env_vars(resolvable_worker_env_vars) - - # -- Client-level operations (lifecycle) -- - - @activity.defn(name=f"{prefix}-sandbox_client_create") - async def create_session(args: CreateSessionArgs) -> SessionResult: - with _sandbox_activity_scope(resolvable): - session = await self._client.create( - snapshot=args.snapshot_spec, - manifest=args.manifest, - options=args.client_options, - ) - self._sessions[str(session.state.session_id)] = session - return SessionResult( - state=session.state, supports_pty=session.supports_pty() - ) - - @activity.defn(name=f"{prefix}-sandbox_client_resume") - async def resume_session(args: ResumeSessionArgs) -> SessionResult: - with _sandbox_activity_scope(resolvable): - session = await self._client.resume(args.state) - self._sessions[str(session.state.session_id)] = session - return SessionResult( - state=session.state, supports_pty=session.supports_pty() - ) - - @activity.defn(name=f"{prefix}-sandbox_client_delete") - async def delete_session(args: StopArgs) -> None: - with _sandbox_activity_scope(resolvable): - session = await self._session(args) - await self._client.delete(session) - return None - - # -- Session-level operations (I/O and lifecycle) -- - - @activity.defn(name=f"{prefix}-sandbox_session_exec") - async def exec_(args: ExecArgs) -> ExecResultModel: - with _sandbox_activity_scope(resolvable): - session = await self._session(args) - result = await session.exec( - *args.command, - timeout=args.timeout, - shell=args.shell, - user=args.user, - ) - return ExecResultModel( - stdout=result.stdout, - stderr=result.stderr, - exit_code=result.exit_code, - ) - - @activity.defn(name=f"{prefix}-sandbox_session_read") - async def read(args: ReadArgs) -> ReadResult: - with _sandbox_activity_scope(resolvable): - session = await self._session(args) - handle = await session.read(Path(args.path)) - return ReadResult(data=handle.read()) - - @activity.defn(name=f"{prefix}-sandbox_session_write") - async def write(args: WriteArgs) -> None: - with _sandbox_activity_scope(resolvable): - session = await self._session(args) - await session.write(Path(args.path), io.BytesIO(args.data)) - return None - - @activity.defn(name=f"{prefix}-sandbox_session_running") - async def running(args: RunningArgs) -> RunningResult: - with _sandbox_activity_scope(resolvable): - session = await self._session(args) - return RunningResult(is_running=await session.running()) - - @activity.defn(name=f"{prefix}-sandbox_session_persist_workspace") - async def persist_workspace( - args: PersistWorkspaceArgs, - ) -> PersistWorkspaceResult: - with _sandbox_activity_scope(resolvable): - session = await self._session(args) - stream = await session.persist_workspace() - return PersistWorkspaceResult(data=stream.read()) - - @activity.defn(name=f"{prefix}-sandbox_session_hydrate_workspace") - async def hydrate_workspace(args: HydrateWorkspaceArgs) -> None: - with _sandbox_activity_scope(resolvable): - session = await self._session(args) - await session.hydrate_workspace(io.BytesIO(args.data)) - return None - - @activity.defn(name=f"{prefix}-sandbox_session_pty_exec_start") - async def pty_exec_start(args: PtyExecStartArgs) -> PtyExecUpdateResult: - with _sandbox_activity_scope(resolvable): - session = await self._session(args) - update = await session.pty_exec_start( - *args.command, - timeout=args.timeout, - shell=args.shell, - user=args.user, - tty=args.tty, - yield_time_s=args.yield_time_s, - max_output_tokens=args.max_output_tokens, - ) - return PtyExecUpdateResult( - process_id=update.process_id, - output=update.output, - exit_code=update.exit_code, - original_token_count=update.original_token_count, - ) - - @activity.defn(name=f"{prefix}-sandbox_session_pty_write_stdin") - async def pty_write_stdin(args: PtyWriteStdinArgs) -> PtyExecUpdateResult: - with _sandbox_activity_scope(resolvable): - session = await self._session(args) - update = await session.pty_write_stdin( - session_id=args.session_id, - chars=args.chars, - yield_time_s=args.yield_time_s, - max_output_tokens=args.max_output_tokens, - ) - return PtyExecUpdateResult( - process_id=update.process_id, - output=update.output, - exit_code=update.exit_code, - original_token_count=update.original_token_count, - ) - - @activity.defn(name=f"{prefix}-sandbox_session_start") - async def start(args: StartArgs) -> None: - with _sandbox_activity_scope(resolvable): - session = await self._session(args) - await session.start() - return None - - @activity.defn(name=f"{prefix}-sandbox_session_stop") - async def session_stop(args: StopArgs) -> None: - with _sandbox_activity_scope(resolvable): - session = await self._session(args) - await session.stop() - return None - - @activity.defn(name=f"{prefix}-sandbox_session_shutdown") - async def session_shutdown(args: StopArgs) -> None: - key = str(args.state.session_id) - session = self._sessions.get(key) - if session is None: - return None - try: - with _sandbox_activity_scope(resolvable): - await session.shutdown() - except ApplicationError: - # Terminal failure: the session is dead, so evict it before - # re-raising. A retryable error instead propagates with the - # entry kept so the activity's retry can still shut it down. - del self._sessions[key] - raise - del self._sessions[key] - return None - - return [ - create_session, - resume_session, - delete_session, - exec_, - read, - write, - running, - persist_workspace, - hydrate_workspace, - pty_exec_start, - pty_write_stdin, - start, - session_stop, - session_shutdown, - ] diff --git a/temporalio/contrib/openai_agents/sandbox/_temporal_activity_models.py b/temporalio/contrib/openai_agents/sandbox/_temporal_activity_models.py deleted file mode 100644 index 5cdc0ccba..000000000 --- a/temporalio/contrib/openai_agents/sandbox/_temporal_activity_models.py +++ /dev/null @@ -1,218 +0,0 @@ -"""Pydantic models for Temporal sandbox activity arguments and results. - -Using ``pydantic_data_converter`` on the Temporal client means these models are -serialized/deserialized automatically. Each activity receives a single typed -model instance rather than a positional arg list. -""" - -from __future__ import annotations - -from base64 import b64decode, b64encode -from typing import Annotated, Any, cast - -from agents.sandbox import Manifest -from agents.sandbox.session.sandbox_client import BaseSandboxClientOptions -from agents.sandbox.session.sandbox_session_state import SandboxSessionState -from agents.sandbox.snapshot import SnapshotBase, SnapshotSpecUnion -from agents.sandbox.types import User -from pydantic import ( - BaseModel, - BeforeValidator, - PlainSerializer, - SerializeAsAny, - field_validator, -) - - -def _coerce_bytes(v: Any) -> bytes: - if isinstance(v, bytes): - return v - if isinstance(v, str): - return b64decode(v) - raise ValueError(f"Expected bytes or base64 string, got {type(v)}") - - -# Bytes type that is stored as raw bytes in Python but base64-encoded in JSON, -# ensuring lossless serialization of arbitrary binary data through pydantic. -JsonSafeBytes = Annotated[ - bytes, - BeforeValidator(_coerce_bytes), - PlainSerializer(lambda v: b64encode(v).decode("ascii"), return_type=str), -] - -# --------------------------------------------------------------------------- -# Shared base for all argument models that carry a session state field. -# --------------------------------------------------------------------------- - - -class _HasState(BaseModel): - state: SerializeAsAny[SandboxSessionState] - - @field_validator("state", mode="before") - @classmethod - def _coerce_state(cls, value: object) -> SandboxSessionState: - return SandboxSessionState.parse(value) - - -# --------------------------------------------------------------------------- -# Argument models (workflow -> activity) -# --------------------------------------------------------------------------- - - -class ExecArgs(_HasState): - """Arguments for exec activity.""" - - command: list[str] - timeout: float | None = None - shell: bool | list[str] = True - user: str | User | None = None - - -class ReadArgs(_HasState): - """Arguments for read activity.""" - - path: str - - -class WriteArgs(_HasState): - """Arguments for write activity.""" - - path: str - data: JsonSafeBytes - - -class RunningArgs(_HasState): - """Arguments for running check activity.""" - - pass - - -class PersistWorkspaceArgs(_HasState): - """Arguments for persist workspace activity.""" - - pass - - -class HydrateWorkspaceArgs(_HasState): - """Arguments for hydrate workspace activity.""" - - data: JsonSafeBytes - - -class PtyExecStartArgs(_HasState): - """Arguments for PTY exec start activity.""" - - command: list[str] - timeout: float | None = None - shell: bool | list[str] = True - user: str | User | None = None - tty: bool = False - yield_time_s: float | None = None - max_output_tokens: int | None = None - - -class PtyWriteStdinArgs(_HasState): - """Arguments for PTY write stdin activity.""" - - session_id: int - chars: str - yield_time_s: float | None = None - max_output_tokens: int | None = None - - -class StartArgs(_HasState): - """Arguments for start activity.""" - - pass - - -class StopArgs(_HasState): - """Arguments for stop activity.""" - - pass - - -# --------------------------------------------------------------------------- -# Result models (activity -> workflow) -# --------------------------------------------------------------------------- - - -class ExecResult(BaseModel): - """Result of an exec activity.""" - - stdout: JsonSafeBytes - stderr: JsonSafeBytes - exit_code: int - - -class PtyExecUpdateResult(BaseModel): - """Result of a PTY exec activity.""" - - process_id: int | None - output: JsonSafeBytes - exit_code: int | None - original_token_count: int | None - - -class ReadResult(BaseModel): - """Result of a read activity.""" - - data: JsonSafeBytes - - -class RunningResult(BaseModel): - """Result of a running check activity.""" - - is_running: bool - - -class PersistWorkspaceResult(BaseModel): - """Result of a persist workspace activity.""" - - data: JsonSafeBytes - - -# --------------------------------------------------------------------------- -# Session lifecycle models (create / resume) -# --------------------------------------------------------------------------- - - -class CreateSessionArgs(BaseModel): - """Arguments for create session activity.""" - - snapshot_spec: SnapshotSpecUnion | SerializeAsAny[SnapshotBase] | None = None - manifest: Manifest | None = None - client_options: SerializeAsAny[BaseSandboxClientOptions] | None = None - - @field_validator("snapshot_spec", mode="before") - @classmethod - def _coerce_snapshot_spec( - cls, value: object - ) -> SnapshotSpecUnion | SnapshotBase | None: - if value is None or isinstance(value, SnapshotBase): - return value - # SnapshotBase subclasses always carry an `id` field; - # SnapshotSpec subclasses do not. Use that to distinguish - # serialized SnapshotBase dicts from SnapshotSpecUnion dicts. - if isinstance(value, dict) and "id" in value: - return SnapshotBase.parse(value) - return cast(SnapshotSpecUnion | None, value) - - @field_validator("client_options", mode="before") - @classmethod - def _coerce_client_options(cls, value: object) -> BaseSandboxClientOptions | None: - if value is None: - return None - return BaseSandboxClientOptions.parse(value) - - -class ResumeSessionArgs(_HasState): - """Arguments for resume session activity.""" - - pass - - -class SessionResult(_HasState): - """Result of create/resume -- session state + capabilities.""" - - supports_pty: bool diff --git a/temporalio/contrib/openai_agents/sandbox/_temporal_sandbox_client.py b/temporalio/contrib/openai_agents/sandbox/_temporal_sandbox_client.py deleted file mode 100644 index 4df0d9405..000000000 --- a/temporalio/contrib/openai_agents/sandbox/_temporal_sandbox_client.py +++ /dev/null @@ -1,140 +0,0 @@ -"""Temporal-aware sandbox client that dispatches lifecycle operations as activities.""" - -from __future__ import annotations - -from datetime import timedelta -from typing import Any - -from agents.sandbox import Manifest -from agents.sandbox.session.sandbox_client import ( - BaseSandboxClient, - BaseSandboxClientOptions, -) -from agents.sandbox.session.sandbox_session import SandboxSession -from agents.sandbox.session.sandbox_session_state import SandboxSessionState -from agents.sandbox.snapshot import SnapshotBase, SnapshotSpec, SnapshotSpecUnion -from pydantic.type_adapter import TypeAdapter - -from temporalio import workflow -from temporalio.contrib.openai_agents._errors import AgentsWorkflowError -from temporalio.contrib.openai_agents.sandbox._temporal_activity_models import ( - CreateSessionArgs, - ResumeSessionArgs, - SessionResult, - StopArgs, -) -from temporalio.contrib.openai_agents.sandbox._temporal_sandbox_session import ( - TemporalSandboxSession, -) -from temporalio.workflow import ActivityConfig - - -class TemporalSandboxClient(BaseSandboxClient[BaseSandboxClientOptions]): - """Stateless client that dispatches all lifecycle operations as Temporal activities. - - No inner client is needed -- session creation, resumption, and deletion are - all handled by activities whose names are prefixed with the provider - ``name`` (e.g. ``"daytona-sandbox_create_session"``). The real - ``BaseSandboxClient`` lives inside :class:`SandboxClientProvider` on the worker. - - Users should never need to instantiate this directly -- use - :func:`temporalio.contrib.openai_agents.workflow.temporal_sandbox_client` - instead. - - Args: - name: The name of the :class:`SandboxClientProvider` registered on the - worker. Used as an activity-name prefix so that the correct - sandbox backend is targeted. - config: Optional activity configuration for controlling timeouts, - retries, etc. Defaults to a 5-minute ``start_to_close_timeout``. - """ - - def __init__( - self, - name: str, - config: ActivityConfig | None = None, - ) -> None: - """Initialize the client.""" - self._name = name - self._config: ActivityConfig = config or ActivityConfig( - start_to_close_timeout=timedelta(minutes=5), - ) - self.backend_id = name - - async def create( - self, - *, - snapshot: SnapshotSpec | SnapshotBase | None = None, - manifest: Manifest | None = None, - options: BaseSandboxClientOptions, - ) -> SandboxSession: - """Create a new sandbox session via activity.""" - _reject_host_path_grants(manifest) - result: SessionResult = await workflow.execute_activity( - f"{self._name}-sandbox_client_create", - arg=CreateSessionArgs( - snapshot_spec=TypeAdapter(SnapshotSpecUnion).validate_python(snapshot) - if isinstance(snapshot, SnapshotSpec) - else snapshot, - manifest=manifest, - client_options=options, - ), - result_type=SessionResult, - **self._config, - ) - return self._wrap_session( - TemporalSandboxSession( - name=self._name, - config=self._config, - state=result.state, - supports_pty_flag=result.supports_pty, - ), - # Real instrumentation runs in the activity in the real client session. - instrumentation=None, - ) - - async def resume(self, state: SandboxSessionState) -> SandboxSession: - """Resume an existing sandbox session via activity.""" - _reject_host_path_grants(state.manifest) - result: SessionResult = await workflow.execute_activity( - f"{self._name}-sandbox_client_resume", - arg=ResumeSessionArgs(state=state), - result_type=SessionResult, - **self._config, - ) - return self._wrap_session( - TemporalSandboxSession( - name=self._name, - config=self._config, - state=result.state, - supports_pty_flag=result.supports_pty, - ), - # Real instrumentation runs in the activity in the real client session. - instrumentation=None, - ) - - async def delete(self, session: TemporalSandboxSession) -> TemporalSandboxSession: # type: ignore[override] - """Delete a sandbox session via activity.""" - await workflow.execute_activity( - f"{self._name}-sandbox_client_delete", - arg=StopArgs(state=session.state), - **self._config, - ) - return session - - def deserialize_session_state(self, payload: dict[str, Any]) -> SandboxSessionState: - """Deserialize a session state from a dict.""" - return SandboxSessionState.parse(payload) - - -def _reject_host_path_grants(manifest: Manifest | None) -> None: - if manifest is None: - return - # Sandbox-side paths only: this message reaches the workflow failure event. - bound = [g.path for g in manifest.extra_path_grants if g.host_path is not None] - if bound: - raise AgentsWorkflowError( - "Sandbox path grants with a host_path are not supported by the Temporal OpenAI " - f"Agents plugin (found: {', '.join(bound)}). A grant's host_path is written " - "into the activity argument in plaintext. Remove host_path from these grants." - ) diff --git a/temporalio/contrib/openai_agents/sandbox/_temporal_sandbox_session.py b/temporalio/contrib/openai_agents/sandbox/_temporal_sandbox_session.py deleted file mode 100644 index cccb72936..000000000 --- a/temporalio/contrib/openai_agents/sandbox/_temporal_sandbox_session.py +++ /dev/null @@ -1,239 +0,0 @@ -"""Temporal-aware sandbox session that routes all I/O through Temporal activities.""" - -from __future__ import annotations - -import io -from pathlib import Path - -from agents.sandbox.session.base_sandbox_session import BaseSandboxSession -from agents.sandbox.session.pty_types import PtyExecUpdate -from agents.sandbox.session.sandbox_session_state import SandboxSessionState -from agents.sandbox.types import ExecResult, User - -from temporalio import workflow -from temporalio.contrib.openai_agents.sandbox._temporal_activity_models import ( - ExecArgs, - HydrateWorkspaceArgs, - PersistWorkspaceArgs, - PersistWorkspaceResult, - PtyExecStartArgs, - PtyExecUpdateResult, - PtyWriteStdinArgs, - ReadArgs, - ReadResult, - RunningArgs, - RunningResult, - StartArgs, - StopArgs, - WriteArgs, -) -from temporalio.contrib.openai_agents.sandbox._temporal_activity_models import ( - ExecResult as ExecResultModel, -) -from temporalio.workflow import ActivityConfig - - -class TemporalSandboxSession(BaseSandboxSession): - """A BaseSandboxSession that routes all I/O through Temporal activities. - - This class is fully stateless with respect to the physical sandbox -- it - holds only the serializable ``SandboxSessionState`` and a ``supports_pty`` - flag (both provided by the worker-side ``SessionResult``). - - Activity names are prefixed with the provider ``name`` so that dispatches - reach the correct sandbox backend's activities on the worker. - - Each activity receives a single Pydantic model instance. Because the Temporal - client is configured with ``pydantic_data_converter``, all fields are - serialized and deserialized automatically. - """ - - def __init__( - self, - name: str, - config: ActivityConfig, - state: SandboxSessionState, - supports_pty_flag: bool = True, - ) -> None: - """Initialize the session.""" - self._name = name - self._config = config - self._state = state - self._supports_pty = supports_pty_flag - - @property - def state(self) -> SandboxSessionState: - """The current session state.""" - return self._state - - @state.setter - def state(self, value: SandboxSessionState) -> None: # type: ignore[reportIncompatibleVariableOverride] - self._state = value - - async def exec( - self, - *command: str | Path, - timeout: float | None = None, - shell: bool | list[str] = True, - user: str | User | None = None, - ) -> ExecResult: - """Execute a command in the sandbox via activity.""" - result: ExecResultModel = await workflow.execute_activity( - f"{self._name}-sandbox_session_exec", - arg=ExecArgs( - state=self.state, - command=[str(c) for c in command], - timeout=timeout, - shell=shell, - user=user, - ), - result_type=ExecResultModel, - **self._config, - ) - return ExecResult( - stdout=result.stdout, stderr=result.stderr, exit_code=result.exit_code - ) - - async def _exec_internal( - self, - *command: str | Path, - timeout: float | None = None, - ) -> ExecResult: - raise NotImplementedError("TemporalSandboxSession overrides exec() directly") - - async def read(self, path: Path, *, user: str | User | None = None) -> io.IOBase: - """Read a file from the sandbox via activity.""" - result: ReadResult = await workflow.execute_activity( - f"{self._name}-sandbox_session_read", - arg=ReadArgs(state=self.state, path=str(path)), - result_type=ReadResult, - **self._config, - ) - return io.BytesIO(result.data) - - async def write( - self, path: Path, data: io.IOBase, *, user: str | User | None = None - ) -> None: - """Write a file to the sandbox via activity.""" - await workflow.execute_activity( - f"{self._name}-sandbox_session_write", - arg=WriteArgs(state=self.state, path=str(path), data=data.read()), - **self._config, - ) - - async def running(self) -> bool: - """Check if the sandbox is running via activity.""" - result: RunningResult = await workflow.execute_activity( - f"{self._name}-sandbox_session_running", - arg=RunningArgs(state=self.state), - result_type=RunningResult, - **self._config, - ) - return result.is_running - - async def shutdown(self) -> None: - """Shut down the sandbox via activity.""" - await workflow.execute_activity( - f"{self._name}-sandbox_session_shutdown", - arg=StopArgs(state=self.state), - **self._config, - ) - - async def persist_workspace(self) -> io.IOBase: - """Persist the workspace via activity.""" - result: PersistWorkspaceResult = await workflow.execute_activity( - f"{self._name}-sandbox_session_persist_workspace", - arg=PersistWorkspaceArgs(state=self.state), - result_type=PersistWorkspaceResult, - **self._config, - ) - return io.BytesIO(result.data) - - async def hydrate_workspace(self, data: io.IOBase) -> None: - """Hydrate the workspace via activity.""" - await workflow.execute_activity( - f"{self._name}-sandbox_session_hydrate_workspace", - arg=HydrateWorkspaceArgs(state=self.state, data=data.read()), - **self._config, - ) - - def supports_pty(self) -> bool: - """Whether this session supports PTY operations.""" - return self._supports_pty - - async def pty_exec_start( - self, - *command: str | Path, - timeout: float | None = None, - shell: bool | list[str] = True, - user: str | User | None = None, - tty: bool = False, - yield_time_s: float | None = None, - max_output_tokens: int | None = None, - ) -> PtyExecUpdate: - """Start a PTY exec via activity.""" - result: PtyExecUpdateResult = await workflow.execute_activity( - f"{self._name}-sandbox_session_pty_exec_start", - arg=PtyExecStartArgs( - state=self.state, - command=[str(c) for c in command], - timeout=timeout, - shell=shell, - user=user, - tty=tty, - yield_time_s=yield_time_s, - max_output_tokens=max_output_tokens, - ), - result_type=PtyExecUpdateResult, - **self._config, - ) - return PtyExecUpdate( - process_id=result.process_id, - output=result.output, - exit_code=result.exit_code, - original_token_count=result.original_token_count, - ) - - async def pty_write_stdin( - self, - *, - session_id: int, - chars: str, - yield_time_s: float | None = None, - max_output_tokens: int | None = None, - ) -> PtyExecUpdate: - """Write to PTY stdin via activity.""" - result: PtyExecUpdateResult = await workflow.execute_activity( - f"{self._name}-sandbox_session_pty_write_stdin", - arg=PtyWriteStdinArgs( - state=self.state, - session_id=session_id, - chars=chars, - yield_time_s=yield_time_s, - max_output_tokens=max_output_tokens, - ), - result_type=PtyExecUpdateResult, - **self._config, - ) - return PtyExecUpdate( - process_id=result.process_id, - output=result.output, - exit_code=result.exit_code, - original_token_count=result.original_token_count, - ) - - async def start(self) -> None: - """Start the sandbox session via activity.""" - await workflow.execute_activity( - f"{self._name}-sandbox_session_start", - arg=StartArgs(state=self.state), - **self._config, - ) - - async def stop(self) -> None: - """Stop the sandbox session via activity.""" - await workflow.execute_activity( - f"{self._name}-sandbox_session_stop", - arg=StopArgs(state=self.state), - **self._config, - ) diff --git a/temporalio/contrib/openai_agents/sandbox/_temporal_worker_env_value.py b/temporalio/contrib/openai_agents/sandbox/_temporal_worker_env_value.py deleted file mode 100644 index a9efe240e..000000000 --- a/temporalio/contrib/openai_agents/sandbox/_temporal_worker_env_value.py +++ /dev/null @@ -1,88 +0,0 @@ -"""Sandbox environment value resolved from the Temporal Worker's environment.""" - -from __future__ import annotations - -import os -from collections.abc import Collection, Iterator -from contextlib import contextmanager -from contextvars import ContextVar -from typing import Literal - -from agents.sandbox.manifest import EnvValue - -from temporalio import workflow -from temporalio.contrib.openai_agents._temporal_worker_env_ref import ( - AllowAllWorkerEnvVars, - _is_resolvable, - _snapshot_resolvable_env_vars, -) -from temporalio.exceptions import ApplicationError - -_resolvable_worker_env_vars: ContextVar[frozenset[str] | AllowAllWorkerEnvVars] = ( - ContextVar("temporal_resolvable_worker_env_vars") -) - - -@contextmanager -def _resolvable_worker_env_vars_scope( # type:ignore[reportUnusedFunction] - names: Collection[str] | AllowAllWorkerEnvVars, -) -> Iterator[None]: - token = _resolvable_worker_env_vars.set(_snapshot_resolvable_env_vars(names)) - try: - yield - finally: - _resolvable_worker_env_vars.reset(token) - - -class TemporalWorkerEnvValue(EnvValue): - """A sandbox environment variable whose value is read on the Temporal Worker. - - .. warning:: - This class is experimental and may change in future versions. - Use with caution in production environments. - - Put one in a sandbox manifest's ``Environment`` in place of the value - itself. Only the name travels in the manifest, and the worker reads the - value when the sandbox environment is needed. Every worker that runs sandbox - activities must set the variable and name it in - ``OpenAIAgentsPlugin(resolvable_worker_env_vars=[...])``. - """ - - type: Literal["temporal.worker_env_value"] = "temporal.worker_env_value" # type: ignore[assignment] - - name: str - """Name of the environment variable to read on the worker.""" - - async def resolve(self) -> str: - """Return the value read from the worker's environment. - - Raises: - ApplicationError: If the variable is not resolvable on this worker, - is unset or empty, or if called from workflow code. - """ - if workflow.in_workflow(): - raise ApplicationError( - "TemporalWorkerEnvValue.resolve() must run in an activity: it reads the " - "process environment, which is non-deterministic on replay and would " - "pull the value into workflow state.", - type="TemporalWorkerEnvValueUnresolved", - non_retryable=True, - ) - resolvable = _resolvable_worker_env_vars.get(frozenset()) - if not _is_resolvable(resolvable, self.name): - raise ApplicationError( - f"TemporalWorkerEnvValue environment variable {self.name!r} is not " - "resolvable on this worker. Name it in " - "OpenAIAgentsPlugin(resolvable_worker_env_vars=[...]).", - type="TemporalWorkerEnvValueUnresolved", - non_retryable=True, - ) - value = os.environ.get(self.name) - if not value: - raise ApplicationError( - f"TemporalWorkerEnvValue environment variable {self.name!r} is not set, " - "or is empty, in the worker process environment.", - type="TemporalWorkerEnvValueUnresolved", - non_retryable=True, - ) - return value diff --git a/temporalio/contrib/openai_agents/testing.py b/temporalio/contrib/openai_agents/testing.py deleted file mode 100644 index 0c6b149c3..000000000 --- a/temporalio/contrib/openai_agents/testing.py +++ /dev/null @@ -1,277 +0,0 @@ -"""Testing utilities for OpenAI agents.""" - -from collections.abc import AsyncIterator, Callable, Collection, Sequence -from typing import Any - -from agents import ( - AgentOutputSchemaBase, - Handoff, - Model, - ModelProvider, - ModelResponse, - ModelSettings, - ModelTracing, - Tool, - TResponseInputItem, - Usage, -) -from agents.items import TResponseOutputItem, TResponseStreamEvent -from openai.types.responses import ( - ResponseFunctionToolCall, - ResponseOutputMessage, - ResponseOutputText, -) - -from temporalio.client import Client -from temporalio.contrib.openai_agents._mcp import ( - StatefulMCPServerProvider, - StatelessMCPServerProvider, -) -from temporalio.contrib.openai_agents._model_parameters import ModelActivityParameters -from temporalio.contrib.openai_agents._temporal_openai_agents import OpenAIAgentsPlugin -from temporalio.contrib.openai_agents._temporal_worker_env_ref import ( - AllowAllWorkerEnvVars, - _snapshot_resolvable_env_vars, -) - -__all__ = [ - "AgentEnvironment", - "ResponseBuilders", - "TestModel", - "TestModelProvider", -] - - -class ResponseBuilders: - """Builders for creating model responses for testing.""" - - @staticmethod - def model_response(output: TResponseOutputItem) -> ModelResponse: - """Create a ModelResponse with the given output.""" - return ModelResponse( - output=[output], - usage=Usage(), - response_id=None, - ) - - @staticmethod - def response_output_message(text: str) -> ResponseOutputMessage: - """Create a ResponseOutputMessage with text content.""" - return ResponseOutputMessage( - id="", - content=[ - ResponseOutputText( - text=text, - annotations=[], - type="output_text", - ) - ], - role="assistant", - status="completed", - type="message", - ) - - @staticmethod - def tool_call(arguments: str, name: str) -> ModelResponse: - """Create a ModelResponse with a function tool call.""" - return ResponseBuilders.model_response( - ResponseFunctionToolCall( - arguments=arguments, - call_id="call", - name=name, - type="function_call", - id="id", - status="completed", - ) - ) - - @staticmethod - def output_message(text: str) -> ModelResponse: - """Create a ModelResponse with an output message.""" - return ResponseBuilders.model_response( - ResponseBuilders.response_output_message(text) - ) - - -class TestModelProvider(ModelProvider): - """Test model provider which simply returns the given model.""" - - __test__ = False - - def __init__(self, model: Model): - """Initialize a test model provider with a model.""" - self._model = model - - def get_model(self, model_name: str | None) -> Model: - """Get a model from the model provider.""" - return self._model - - -class TestModel(Model): - """Test model for use mocking model responses.""" - - __test__ = False - - def __init__(self, fn: Callable[[], ModelResponse]) -> None: - """Initialize a test model with a callable.""" - self.fn = fn - - async def get_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, - ) -> ModelResponse: - """Get a response from the mocked model, by calling the callable passed to the constructor.""" - return self.fn() - - 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]: - """Get a streamed response from the model. Unimplemented.""" - raise NotImplementedError() - - @staticmethod - def returning_responses(responses: list[ModelResponse]) -> "TestModel": - """Create a mock model which sequentially returns responses from a list.""" - i = iter(responses) - return TestModel(lambda: next(i)) - - -class AgentEnvironment: - """Testing environment for OpenAI agents with Temporal integration. - - This async context manager provides a convenient way to set up testing environments - for OpenAI agents with mocked model calls and Temporal integration. - - Example: - >>> from temporalio.contrib.openai_agents.testing import AgentEnvironment, TestModelProvider, ResponseBuilders - >>> from temporalio.client import Client - >>> - >>> # Create a mock model that returns predefined responses - >>> mock_model = TestModel.returning_responses([ - ... ResponseBuilders.output_message("Hello, world!"), - ... ResponseBuilders.output_message("How can I help you?") - ... ]) - >>> - >>> async with AgentEnvironment(model=mock_model) as env: - ... client = env.applied_on_client(client) - ... # Use client for testing workflows with mocked model calls - """ - - __test__ = False - - def __init__( - self, - model_params: ModelActivityParameters | None = None, - model_provider: ModelProvider | None = None, - model: Model | None = None, - mcp_server_providers: Sequence[ - StatelessMCPServerProvider | StatefulMCPServerProvider - ] = (), - register_activities: bool = True, - add_temporal_spans: bool = True, - use_otel_instrumentation: bool = False, - resolvable_worker_env_vars: Collection[str] | AllowAllWorkerEnvVars = (), - ) -> None: - """Initialize the AgentEnvironment. - - Args: - model_params: Configuration parameters for Temporal activity execution - of model calls. If None, default parameters will be used. - model_provider: Optional model provider for custom model implementations. - Only one of model_provider or model should be provided. - If both are provided, model_provider will be used. - model: Optional model for custom model implementations. - Use TestModel for mocking model responses. - Equivalent to model_provider=TestModelProvider(model). - Only one of model_provider or model should be provided. - If both are provided, model_provider will be used. - mcp_server_providers: Sequence of MCP servers to automatically register with the worker. - register_activities: Whether to register activities during worker execution. - add_temporal_spans: Whether to add temporal spans to traces - use_otel_instrumentation: If set to true, enable open telemetry instrumentation. - Warning: use_otel_instrumentation is experimental and behavior may change in future versions. - Use with caution in production environments. - resolvable_worker_env_vars: Names of the environment variables that - ``temporal_worker_env_ref()`` may read on this environment's workers; - pass ``AllowAllWorkerEnvVars()`` in place of the names to allow every - variable. - Warning: resolvable_worker_env_vars is experimental and behavior may change in future versions. - Use with caution in production environments. - """ - self._model_params = model_params - self._model_provider = None - if model_provider is not None: - self._model_provider = model_provider - elif model is not None: - self._model_provider = TestModelProvider(model) - self._mcp_server_providers = mcp_server_providers - self._register_activities = register_activities - self._plugin: OpenAIAgentsPlugin | None = None - self._add_temporal_spans = add_temporal_spans - self._use_otel_instrumentation = use_otel_instrumentation - self._resolvable_worker_env_vars = _snapshot_resolvable_env_vars( - resolvable_worker_env_vars - ) - - async def __aenter__(self) -> "AgentEnvironment": - """Enter the async context manager.""" - # Create the plugin with the provided configuration - self._plugin = OpenAIAgentsPlugin( - model_params=self._model_params, - model_provider=self._model_provider, - mcp_server_providers=self._mcp_server_providers, - register_activities=self._register_activities, - add_temporal_spans=self._add_temporal_spans, - use_otel_instrumentation=self._use_otel_instrumentation, - resolvable_worker_env_vars=self._resolvable_worker_env_vars, - ) - - return self - - async def __aexit__(self, *args: Any) -> None: - """Exit the async context manager.""" - # No cleanup needed currently - pass - - def applied_on_client(self, client: Client) -> Client: - """Apply the agent environment's plugin to a client and return a new client instance. - - Args: - client: The base Temporal client to apply the plugin to. - - Returns: - A new Client instance with the OpenAI agents plugin applied. - """ - if self._plugin is None: - raise RuntimeError( - "AgentEnvironment must be entered before applying to client" - ) - - new_config = client.config() - existing_plugins = new_config.get("plugins", []) - new_config["plugins"] = list(existing_plugins) + [self._plugin] - return Client(**new_config) - - @property - def openai_agents_plugin(self) -> OpenAIAgentsPlugin: - """Get the underlying OpenAI agents plugin.""" - if self._plugin is None: - raise RuntimeError( - "AgentEnvironment must be entered before accessing plugin" - ) - return self._plugin diff --git a/temporalio/contrib/openai_agents/workflow.py b/temporalio/contrib/openai_agents/workflow.py deleted file mode 100644 index 70f54629f..000000000 --- a/temporalio/contrib/openai_agents/workflow.py +++ /dev/null @@ -1,382 +0,0 @@ -"""Workflow-specific primitives for working with the OpenAI Agents SDK in a workflow context""" - -import functools -import inspect -import json -import typing -from collections.abc import Callable -from contextlib import AbstractAsyncContextManager -from datetime import timedelta -from typing import Any - -import nexusrpc -from agents import ( - RunContextWrapper, - Tool, -) -from agents.function_schema import function_schema -from agents.tool import ( - FunctionTool, -) - -from temporalio import activity -from temporalio import workflow as temporal_workflow -from temporalio.common import Priority, RetryPolicy -from temporalio.contrib.openai_agents._errors import ( - AgentsWorkflowError as AgentsWorkflowError, -) -from temporalio.contrib.openai_agents.sandbox._temporal_sandbox_client import ( - TemporalSandboxClient, -) -from temporalio.exceptions import ApplicationError, TemporalError -from temporalio.workflow import ( - ActivityCancellationType, - ActivityConfig, - VersioningIntent, -) - -if typing.TYPE_CHECKING: - from agents.mcp import MCPServer - - -def activity_as_tool( - fn: Callable, - *, - task_queue: str | None = None, - schedule_to_close_timeout: timedelta | None = None, - schedule_to_start_timeout: timedelta | None = None, - start_to_close_timeout: timedelta | None = None, - heartbeat_timeout: timedelta | None = None, - retry_policy: RetryPolicy | None = None, - cancellation_type: ActivityCancellationType = ActivityCancellationType.TRY_CANCEL, - activity_id: str | None = None, - versioning_intent: VersioningIntent | None = None, - summary: str | None = None, - priority: Priority = Priority.default, - strict_json_schema: bool = True, -) -> Tool: - """Convert a single Temporal activity function to an OpenAI agent tool. - - This function takes a Temporal activity function and converts it into an - OpenAI agent tool that can be used by the agent to execute the activity - during workflow execution. The tool will automatically handle the conversion - of inputs and outputs between the agent and the activity. Note that if you take a context, - mutation will not be persisted, as the activity may not be running in the same location. - - For undocumented arguments, refer to :py:mod:`workflow` and :py:meth:`start_activity` - - Args: - fn: A Temporal activity function to convert to a tool. - strict_json_schema: Whether the tool should follow a strict schema. - See https://openai.github.io/openai-agents-python/ref/tool/#agents.tool.FunctionTool.strict_json_schema - - - Returns: - An OpenAI agent tool that wraps the provided activity. - - Raises: - ApplicationError: If the function is not properly decorated as a Temporal activity. - - Example: - >>> @activity.defn - >>> def process_data(input: str) -> str: - ... return f"Processed: {input}" - >>> - >>> # Create tool with custom activity options - >>> tool = activity_as_tool( - ... process_data, - ... start_to_close_timeout=timedelta(seconds=30), - ... retry_policy=RetryPolicy(maximum_attempts=3), - ... heartbeat_timeout=timedelta(seconds=10) - ... ) - >>> # Use tool with an OpenAI agent - """ - ret = activity._Definition.from_callable(fn) - if not ret: - raise ApplicationError( - "Bare function without tool and activity decorators is not supported", - "invalid_tool", - ) - if ret.name is None: - raise ApplicationError( - "Input activity must have a name to be made into a tool", - "invalid_tool", - ) - # If the provided callable has a first argument of `self`, partially apply it with the same metadata - # The actual instance will be picked up by the activity execution, the partially applied function will never actually be executed - params = list(inspect.signature(fn).parameters.keys()) - if len(params) > 0 and params[0] == "self": - partial = functools.partial(fn, None) - setattr(partial, "__name__", fn.__name__) - partial.__annotations__ = getattr(fn, "__annotations__") - setattr( - partial, - "__temporal_activity_definition", - getattr(fn, "__temporal_activity_definition"), - ) - partial.__doc__ = fn.__doc__ - fn = partial - schema = function_schema(fn) - - async def run_activity(ctx: RunContextWrapper[Any], input: str) -> Any: - try: - json_data = json.loads(input) - except Exception as e: - raise ApplicationError( - f"Invalid JSON input for tool {schema.name}: {input}" - ) from e - - # Activities don't support keyword only arguments, so we can ignore the kwargs_dict return - args, _ = schema.to_call_args(schema.params_pydantic_model(**json_data)) - - # Add the context to the arguments if it takes that - if schema.takes_context: - args = [ctx] + args - result = await temporal_workflow.execute_activity( - ret.name, # type: ignore - args=args, - task_queue=task_queue, - schedule_to_close_timeout=schedule_to_close_timeout, - schedule_to_start_timeout=schedule_to_start_timeout, - start_to_close_timeout=start_to_close_timeout, - heartbeat_timeout=heartbeat_timeout, - retry_policy=retry_policy, - cancellation_type=cancellation_type, - activity_id=activity_id, - versioning_intent=versioning_intent, - summary=summary or schema.description, - priority=priority, - ) - try: - return str(result) - except Exception as e: - raise ToolSerializationError( - "You must return a string representation of the tool output, or something we can call str() on" - ) from e - - return FunctionTool( - name=schema.name, - description=schema.description or "", - params_json_schema=schema.params_json_schema, - on_invoke_tool=run_activity, - strict_json_schema=strict_json_schema, - ) - - -def nexus_operation_as_tool( - operation: nexusrpc.Operation[Any, Any], - *, - service: type[Any], - endpoint: str, - schedule_to_close_timeout: timedelta | None = None, - strict_json_schema: bool = True, -) -> Tool: - """Convert a Nexus operation into an OpenAI agent tool. - - This function takes a Nexus operation and converts it into an - OpenAI agent tool that can be used by the agent to execute the operation - during workflow execution. The tool will automatically handle the conversion - of inputs and outputs between the agent and the operation. - - Args: - operation: A Nexus operation to convert into a tool. - service: The Nexus service class that contains the operation. - endpoint: The Nexus endpoint to use for the operation. - strict_json_schema: Whether the tool should follow a strict schema - - Returns: - An OpenAI agent tool that wraps the provided operation. - - Example: - >>> @nexusrpc.service - ... class WeatherService: - ... get_weather_object_nexus_operation: nexusrpc.Operation[WeatherInput, Weather] - >>> - >>> # Create tool with custom activity options - >>> tool = nexus_operation_as_tool( - ... WeatherService.get_weather_object_nexus_operation, - ... service=WeatherService, - ... endpoint="weather-service", - ... ) - >>> # Use tool with an OpenAI agent - """ - - def operation_callable(input: Any): # type: ignore[reportUnusedParameter] - raise NotImplementedError("This function definition is used as a type only") - - operation_callable.__annotations__ = { - "input": operation.input_type, - "return": operation.output_type, - } - operation_callable.__name__ = operation.name - - schema = function_schema(operation_callable) - - async def run_operation(_ctx: RunContextWrapper[Any], input: str) -> Any: - try: - json_data = json.loads(input) - except Exception as e: - raise ApplicationError( - f"Invalid JSON input for tool {schema.name}: {input}" - ) from e - - nexus_client = temporal_workflow.create_nexus_client( - service=service, endpoint=endpoint - ) - args, _ = schema.to_call_args(schema.params_pydantic_model(**json_data)) - assert len(args) == 1, "Nexus operations must have exactly one argument" - [arg] = args - result = await nexus_client.execute_operation( - operation, - arg, - schedule_to_close_timeout=schedule_to_close_timeout, - ) - try: - return str(result) - except Exception as e: - raise ToolSerializationError( - "You must return a string representation of the tool output, or something we can call str() on" - ) from e - - return FunctionTool( - name=schema.name, - description=schema.description or "", - params_json_schema=schema.params_json_schema, - on_invoke_tool=run_operation, - strict_json_schema=strict_json_schema, - ) - - -def temporal_sandbox_client( - name: str, - config: ActivityConfig | None = None, -) -> Any: - """Create a sandbox client reference for use in a Temporal workflow ``RunConfig``. - - .. warning:: - This is experimental and may change in future versions. - Use with caution in production environments. - - This returns a ``BaseSandboxClient`` that dispatches all sandbox operations - as Temporal activities, targeting the ``SandboxClientProvider`` registered - on the worker with the matching ``name``. - - Example:: - - run_config = RunConfig( - sandbox=SandboxRunConfig( - client=temporal_sandbox_client("daytona"), - options=DaytonaSandboxClientOptions(...), - ), - ) - - Args: - name: The name of the ``SandboxClientProvider`` registered on the - worker. Must match exactly. - config: Optional activity configuration for controlling timeouts, - retries, etc. Defaults to a 5-minute ``start_to_close_timeout``. - """ - return TemporalSandboxClient(name=name, config=config) - - -def stateless_mcp_server( - name: str, - config: ActivityConfig | None = None, - cache_tools_list: bool = False, - factory_argument: Any | None = None, -) -> "MCPServer": - """A stateless MCP server implementation for Temporal workflows. - - This uses a TemporalMCPServer of the same name registered with the OpenAIAgents plugin to implement - durable MCP operations statelessly. - - This approach is suitable for simple use cases where connection overhead is acceptable - and you don't need to maintain state between operations. It should be preferred to stateful when possible due to its - superior durability guarantees. - - .. warning:: - Do not pass secrets, credentials, or API keys through ``factory_argument``. It is an - activity argument, so it is recorded in workflow history and, without a payload codec, - visible in the web UI. Resolve credentials worker-side inside the server factory - instead. - - Args: - name: A string name for the server. Should match that provided in the plugin. - config: Optional activity configuration for MCP operation activities. - Defaults to 1-minute start-to-close timeout. - cache_tools_list: If true, the list of tools will be cached for the duration of the server - factory_argument: Optional argument to be provided to the factory when producing an MCPServer. - Must not contain secrets. - """ - from temporalio.contrib.openai_agents._mcp import ( - _StatelessMCPServerReference, - ) - - return _StatelessMCPServerReference( - name, config, cache_tools_list, factory_argument - ) - - -def stateful_mcp_server( - name: str, - config: ActivityConfig | None = None, - server_session_config: ActivityConfig | None = None, - factory_argument: Any | None = None, -) -> AbstractAsyncContextManager["MCPServer"]: - """A stateful MCP server implementation for Temporal workflows. - - This wraps an MCP server to maintain a persistent connection throughout - the workflow execution. It creates a dedicated worker that stays connected to - the MCP server and processes operations on a dedicated task queue. - - This approach is more efficient for workflows that make multiple MCP calls, - as it avoids connection overhead, but requires more resources to maintain - the persistent connection and worker. - - The caller will have to handle cases where the dedicated worker fails, as Temporal is - unable to seamlessly recreate any lost state in that case. - - .. warning:: - Do not pass secrets, credentials, or API keys through ``factory_argument``. It is an - activity argument, so it is recorded in workflow history and, without a payload codec, - visible in the web UI. Resolve credentials worker-side inside the server factory - instead. - - Args: - name: A string name for the server. Should match that provided in the plugin. - config: Optional activity configuration for MCP operation activities. - Defaults to 1-minute start-to-close and 30-second schedule-to-start timeouts. - server_session_config: Optional activity configuration for the connection activity. - Defaults to 1-hour start-to-close timeout. - factory_argument: Optional argument to be provided to the factory when producing an MCPServer. - Must not contain secrets. - """ - from temporalio.contrib.openai_agents._mcp import ( - _StatefulMCPServerReference, - ) - - return _StatefulMCPServerReference( - name, config, server_session_config, factory_argument - ) - - -class ToolSerializationError(TemporalError): - """Error that occurs when a tool output could not be serialized. - - This exception is raised when a tool (created from an activity or Nexus operation) - returns a value that cannot be properly serialized for use by the OpenAI agent. - All tool outputs must be convertible to strings for the agent to process them. - - The error typically occurs when: - - A tool returns a complex object that doesn't have a meaningful string representation - - The returned object cannot be converted using str() - - Custom serialization is needed but not implemented - - Example: - >>> @activity.defn - >>> def problematic_tool() -> ComplexObject: - ... return ComplexObject() # This might cause ToolSerializationError - - To fix this error, ensure your tool returns string-convertible values or - modify the tool to return a string representation of the result. - """ diff --git a/tests/contrib/openai_agents/histories/agents-as-tools-workflow-history.json b/tests/contrib/openai_agents/histories/agents-as-tools-workflow-history.json deleted file mode 100644 index 21090cbdd..000000000 --- a/tests/contrib/openai_agents/histories/agents-as-tools-workflow-history.json +++ /dev/null @@ -1,651 +0,0 @@ -{ - "events": [ - { - "eventId": "1", - "eventTime": "2025-06-30T16:04:02.623610Z", - "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_STARTED", - "taskId": "1056429", - "workflowExecutionStartedEventAttributes": { - "workflowType": { - "name": "AgentsAsToolsWorkflow" - }, - "taskQueue": { - "name": "1e8fa3c8-41b4-4ea7-907b-63088b63e36f", - "kind": "TASK_QUEUE_KIND_NORMAL" - }, - "input": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "IlRyYW5zbGF0ZSB0byBTcGFuaXNoOiAnSSBhbSBmdWxsJyI=" - } - ] - }, - "workflowExecutionTimeout": "30s", - "workflowRunTimeout": "30s", - "workflowTaskTimeout": "10s", - "originalExecutionRunId": "0197c194-c7bf-794e-a2e3-1486d80ca4bb", - "identity": "16872@Tims-MacBook-Pro.local", - "firstExecutionRunId": "0197c194-c7bf-794e-a2e3-1486d80ca4bb", - "attempt": 1, - "workflowExecutionExpirationTime": "2025-06-30T16:04:32.623Z", - "firstWorkflowTaskBackoff": "0s", - "workflowId": "agents-as-tools-workflow-5595c716-c9ec-4aa9-8cec-c1b995bdca50" - } - }, - { - "eventId": "2", - "eventTime": "2025-06-30T16:04:02.623655Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", - "taskId": "1056430", - "workflowTaskScheduledEventAttributes": { - "taskQueue": { - "name": "1e8fa3c8-41b4-4ea7-907b-63088b63e36f", - "kind": "TASK_QUEUE_KIND_NORMAL" - }, - "startToCloseTimeout": "10s", - "attempt": 1 - } - }, - { - "eventId": "3", - "eventTime": "2025-06-30T16:04:02.625165Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", - "taskId": "1056436", - "workflowTaskStartedEventAttributes": { - "scheduledEventId": "2", - "identity": "16872@Tims-MacBook-Pro.local", - "requestId": "57afdb83-3d04-4219-93d5-1390ccbc78e9", - "historySizeBytes": "438", - "workerVersion": { - "buildId": "01c9746b80ed5a14fc25fb053c5e410a" - } - } - }, - { - "eventId": "4", - "eventTime": "2025-06-30T16:04:02.666927Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", - "taskId": "1056440", - "workflowTaskCompletedEventAttributes": { - "scheduledEventId": "2", - "startedEventId": "3", - "identity": "16872@Tims-MacBook-Pro.local", - "workerVersion": { - "buildId": "01c9746b80ed5a14fc25fb053c5e410a" - }, - "sdkMetadata": { - "coreUsedFlags": [ - 3, - 2, - 1 - ], - "sdkName": "temporal-python", - "sdkVersion": "1.13.0" - }, - "meteringMetadata": {} - } - }, - { - "eventId": "5", - "eventTime": "2025-06-30T16:04:02.666956Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_SCHEDULED", - "taskId": "1056441", - "activityTaskScheduledEventAttributes": { - "activityId": "1", - "activityType": { - "name": "invoke_model_activity" - }, - "taskQueue": { - "name": "1e8fa3c8-41b4-4ea7-907b-63088b63e36f", - "kind": "TASK_QUEUE_KIND_NORMAL" - }, - "header": { - "fields": { - "__openai_span": { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJ0cmFjZU5hbWUiOiJPcmNoZXN0cmF0b3IgZXZhbHVhdG9yIiwic3BhbklkIjoic3Bhbl85N2RkOTkxZDczZDQ0MTY3ODliODMxODAiLCJ0cmFjZUlkIjoidHJhY2VfN2ZjMzk4YmZiZGYzNDhmMDg0YTYzZDRkNjI0OGUwNDUifQ==" - } - } - }, - "input": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJtb2RlbF9uYW1lIjpudWxsLCJzeXN0ZW1faW5zdHJ1Y3Rpb25zIjoiWW91IGFyZSBhIHRyYW5zbGF0aW9uIGFnZW50LiBZb3UgdXNlIHRoZSB0b29scyBnaXZlbiB0byB5b3UgdG8gdHJhbnNsYXRlLklmIGFza2VkIGZvciBtdWx0aXBsZSB0cmFuc2xhdGlvbnMsIHlvdSBjYWxsIHRoZSByZWxldmFudCB0b29scyBpbiBvcmRlci5Zb3UgbmV2ZXIgdHJhbnNsYXRlIG9uIHlvdXIgb3duLCB5b3UgYWx3YXlzIHVzZSB0aGUgcHJvdmlkZWQgdG9vbHMuIiwiaW5wdXQiOlt7ImNvbnRlbnQiOiJUcmFuc2xhdGUgdG8gU3BhbmlzaDogJ0kgYW0gZnVsbCciLCJyb2xlIjoidXNlciJ9XSwibW9kZWxfc2V0dGluZ3MiOnsidGVtcGVyYXR1cmUiOm51bGwsInRvcF9wIjpudWxsLCJmcmVxdWVuY3lfcGVuYWx0eSI6bnVsbCwicHJlc2VuY2VfcGVuYWx0eSI6bnVsbCwidG9vbF9jaG9pY2UiOm51bGwsInBhcmFsbGVsX3Rvb2xfY2FsbHMiOm51bGwsInRydW5jYXRpb24iOm51bGwsIm1heF90b2tlbnMiOm51bGwsInJlYXNvbmluZyI6bnVsbCwibWV0YWRhdGEiOm51bGwsInN0b3JlIjpudWxsLCJpbmNsdWRlX3VzYWdlIjpudWxsLCJleHRyYV9xdWVyeSI6bnVsbCwiZXh0cmFfYm9keSI6bnVsbCwiZXh0cmFfaGVhZGVycyI6bnVsbCwiZXh0cmFfYXJncyI6bnVsbH0sInRvb2xzIjpbeyJuYW1lIjoidHJhbnNsYXRlX3RvX3NwYW5pc2giLCJkZXNjcmlwdGlvbiI6IlRyYW5zbGF0ZSB0aGUgdXNlcidzIG1lc3NhZ2UgdG8gU3BhbmlzaCIsInBhcmFtc19qc29uX3NjaGVtYSI6eyJwcm9wZXJ0aWVzIjp7ImlucHV0Ijp7InRpdGxlIjoiSW5wdXQiLCJ0eXBlIjoic3RyaW5nIn19LCJyZXF1aXJlZCI6WyJpbnB1dCJdLCJ0aXRsZSI6InRyYW5zbGF0ZV90b19zcGFuaXNoX2FyZ3MiLCJ0eXBlIjoib2JqZWN0IiwiYWRkaXRpb25hbFByb3BlcnRpZXMiOmZhbHNlfSwic3RyaWN0X2pzb25fc2NoZW1hIjp0cnVlfSx7Im5hbWUiOiJ0cmFuc2xhdGVfdG9fZnJlbmNoIiwiZGVzY3JpcHRpb24iOiJUcmFuc2xhdGUgdGhlIHVzZXIncyBtZXNzYWdlIHRvIEZyZW5jaCIsInBhcmFtc19qc29uX3NjaGVtYSI6eyJwcm9wZXJ0aWVzIjp7ImlucHV0Ijp7InRpdGxlIjoiSW5wdXQiLCJ0eXBlIjoic3RyaW5nIn19LCJyZXF1aXJlZCI6WyJpbnB1dCJdLCJ0aXRsZSI6InRyYW5zbGF0ZV90b19mcmVuY2hfYXJncyIsInR5cGUiOiJvYmplY3QiLCJhZGRpdGlvbmFsUHJvcGVydGllcyI6ZmFsc2V9LCJzdHJpY3RfanNvbl9zY2hlbWEiOnRydWV9LHsibmFtZSI6InRyYW5zbGF0ZV90b19pdGFsaWFuIiwiZGVzY3JpcHRpb24iOiJUcmFuc2xhdGUgdGhlIHVzZXIncyBtZXNzYWdlIHRvIEl0YWxpYW4iLCJwYXJhbXNfanNvbl9zY2hlbWEiOnsicHJvcGVydGllcyI6eyJpbnB1dCI6eyJ0aXRsZSI6IklucHV0IiwidHlwZSI6InN0cmluZyJ9fSwicmVxdWlyZWQiOlsiaW5wdXQiXSwidGl0bGUiOiJ0cmFuc2xhdGVfdG9faXRhbGlhbl9hcmdzIiwidHlwZSI6Im9iamVjdCIsImFkZGl0aW9uYWxQcm9wZXJ0aWVzIjpmYWxzZX0sInN0cmljdF9qc29uX3NjaGVtYSI6dHJ1ZX1dLCJvdXRwdXRfc2NoZW1hIjpudWxsLCJoYW5kb2ZmcyI6W10sInRyYWNpbmciOjEsInByZXZpb3VzX3Jlc3BvbnNlX2lkIjpudWxsLCJwcm9tcHQiOm51bGx9" - } - ] - }, - "scheduleToCloseTimeout": "30s", - "scheduleToStartTimeout": "30s", - "startToCloseTimeout": "10s", - "heartbeatTimeout": "0s", - "workflowTaskCompletedEventId": "4", - "retryPolicy": { - "initialInterval": "1s", - "backoffCoefficient": 2.0, - "maximumInterval": "100s" - }, - "useWorkflowBuildId": true - }, - "userMetadata": { - "summary": { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "IlRyYW5zbGF0ZSB0byBTcGFuaXNoOiAnSSBhbSBmdWxsJyI=" - } - } - }, - { - "eventId": "6", - "eventTime": "2025-06-30T16:04:02.667697Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_STARTED", - "taskId": "1056447", - "activityTaskStartedEventAttributes": { - "scheduledEventId": "5", - "identity": "16872@Tims-MacBook-Pro.local", - "requestId": "a4e24baa-fc8b-4b3d-90bf-55a08e7d3680", - "attempt": 1, - "workerVersion": { - "buildId": "01c9746b80ed5a14fc25fb053c5e410a" - } - } - }, - { - "eventId": "7", - "eventTime": "2025-06-30T16:04:02.686423Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_COMPLETED", - "taskId": "1056448", - "activityTaskCompletedEventAttributes": { - "result": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJvdXRwdXQiOlt7ImFyZ3VtZW50cyI6IntcImlucHV0XCI6XCJJIGFtIGZ1bGxcIn0iLCJjYWxsX2lkIjoiY2FsbCIsIm5hbWUiOiJ0cmFuc2xhdGVfdG9fc3BhbmlzaCIsInR5cGUiOiJmdW5jdGlvbl9jYWxsIiwiaWQiOiJpZCIsInN0YXR1cyI6ImNvbXBsZXRlZCJ9XSwidXNhZ2UiOnsicmVxdWVzdHMiOjAsImlucHV0X3Rva2VucyI6MCwiaW5wdXRfdG9rZW5zX2RldGFpbHMiOnsiY2FjaGVkX3Rva2VucyI6MH0sIm91dHB1dF90b2tlbnMiOjAsIm91dHB1dF90b2tlbnNfZGV0YWlscyI6eyJyZWFzb25pbmdfdG9rZW5zIjowfSwidG90YWxfdG9rZW5zIjowfSwicmVzcG9uc2VfaWQiOm51bGx9" - } - ] - }, - "scheduledEventId": "5", - "startedEventId": "6", - "identity": "16872@Tims-MacBook-Pro.local" - } - }, - { - "eventId": "8", - "eventTime": "2025-06-30T16:04:02.686427Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", - "taskId": "1056449", - "workflowTaskScheduledEventAttributes": { - "taskQueue": { - "name": "16872@Tims-MacBook-Pro.local-547b7584bee9475f87bc46161a6291a5", - "kind": "TASK_QUEUE_KIND_STICKY", - "normalName": "1e8fa3c8-41b4-4ea7-907b-63088b63e36f" - }, - "startToCloseTimeout": "10s", - "attempt": 1 - } - }, - { - "eventId": "9", - "eventTime": "2025-06-30T16:04:02.687098Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", - "taskId": "1056453", - "workflowTaskStartedEventAttributes": { - "scheduledEventId": "8", - "identity": "16872@Tims-MacBook-Pro.local", - "requestId": "3709df1d-7d2a-4929-af1c-1ab8d2074b00", - "historySizeBytes": "3467", - "workerVersion": { - "buildId": "01c9746b80ed5a14fc25fb053c5e410a" - } - } - }, - { - "eventId": "10", - "eventTime": "2025-06-30T16:04:02.718095Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", - "taskId": "1056457", - "workflowTaskCompletedEventAttributes": { - "scheduledEventId": "8", - "startedEventId": "9", - "identity": "16872@Tims-MacBook-Pro.local", - "workerVersion": { - "buildId": "01c9746b80ed5a14fc25fb053c5e410a" - }, - "sdkMetadata": {}, - "meteringMetadata": {} - } - }, - { - "eventId": "11", - "eventTime": "2025-06-30T16:04:02.718115Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_SCHEDULED", - "taskId": "1056458", - "activityTaskScheduledEventAttributes": { - "activityId": "2", - "activityType": { - "name": "invoke_model_activity" - }, - "taskQueue": { - "name": "1e8fa3c8-41b4-4ea7-907b-63088b63e36f", - "kind": "TASK_QUEUE_KIND_NORMAL" - }, - "header": { - "fields": { - "__openai_span": { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJ0cmFjZU5hbWUiOiJPcmNoZXN0cmF0b3IgZXZhbHVhdG9yIiwic3BhbklkIjoic3Bhbl81ZTUwYjU3ODU2ZWI0NWY4Yjk4MDI4N2QiLCJ0cmFjZUlkIjoidHJhY2VfN2ZjMzk4YmZiZGYzNDhmMDg0YTYzZDRkNjI0OGUwNDUifQ==" - } - } - }, - "input": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJtb2RlbF9uYW1lIjpudWxsLCJzeXN0ZW1faW5zdHJ1Y3Rpb25zIjoiWW91IHRyYW5zbGF0ZSB0aGUgdXNlcidzIG1lc3NhZ2UgdG8gU3BhbmlzaCIsImlucHV0IjpbeyJjb250ZW50IjoiSSBhbSBmdWxsIiwicm9sZSI6InVzZXIifV0sIm1vZGVsX3NldHRpbmdzIjp7InRlbXBlcmF0dXJlIjpudWxsLCJ0b3BfcCI6bnVsbCwiZnJlcXVlbmN5X3BlbmFsdHkiOm51bGwsInByZXNlbmNlX3BlbmFsdHkiOm51bGwsInRvb2xfY2hvaWNlIjpudWxsLCJwYXJhbGxlbF90b29sX2NhbGxzIjpudWxsLCJ0cnVuY2F0aW9uIjpudWxsLCJtYXhfdG9rZW5zIjpudWxsLCJyZWFzb25pbmciOm51bGwsIm1ldGFkYXRhIjpudWxsLCJzdG9yZSI6bnVsbCwiaW5jbHVkZV91c2FnZSI6bnVsbCwiZXh0cmFfcXVlcnkiOm51bGwsImV4dHJhX2JvZHkiOm51bGwsImV4dHJhX2hlYWRlcnMiOm51bGwsImV4dHJhX2FyZ3MiOm51bGx9LCJ0b29scyI6W10sIm91dHB1dF9zY2hlbWEiOm51bGwsImhhbmRvZmZzIjpbXSwidHJhY2luZyI6MSwicHJldmlvdXNfcmVzcG9uc2VfaWQiOm51bGwsInByb21wdCI6bnVsbH0=" - } - ] - }, - "scheduleToCloseTimeout": "30s", - "scheduleToStartTimeout": "30s", - "startToCloseTimeout": "10s", - "heartbeatTimeout": "0s", - "workflowTaskCompletedEventId": "10", - "retryPolicy": { - "initialInterval": "1s", - "backoffCoefficient": 2.0, - "maximumInterval": "100s" - }, - "useWorkflowBuildId": true - }, - "userMetadata": { - "summary": { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "IkkgYW0gZnVsbCI=" - } - } - }, - { - "eventId": "12", - "eventTime": "2025-06-30T16:04:02.718843Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_STARTED", - "taskId": "1056463", - "activityTaskStartedEventAttributes": { - "scheduledEventId": "11", - "identity": "16872@Tims-MacBook-Pro.local", - "requestId": "988daf7d-b4fa-44cb-868f-e2f5e7663f37", - "attempt": 1, - "workerVersion": { - "buildId": "01c9746b80ed5a14fc25fb053c5e410a" - } - } - }, - { - "eventId": "13", - "eventTime": "2025-06-30T16:04:02.732675Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_COMPLETED", - "taskId": "1056464", - "activityTaskCompletedEventAttributes": { - "result": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJvdXRwdXQiOlt7ImlkIjoiIiwiY29udGVudCI6W3siYW5ub3RhdGlvbnMiOltdLCJ0ZXh0IjoiRXN0b3kgbGxlbm8uIiwidHlwZSI6Im91dHB1dF90ZXh0In1dLCJyb2xlIjoiYXNzaXN0YW50Iiwic3RhdHVzIjoiY29tcGxldGVkIiwidHlwZSI6Im1lc3NhZ2UifV0sInVzYWdlIjp7InJlcXVlc3RzIjowLCJpbnB1dF90b2tlbnMiOjAsImlucHV0X3Rva2Vuc19kZXRhaWxzIjp7ImNhY2hlZF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5zIjowLCJvdXRwdXRfdG9rZW5zX2RldGFpbHMiOnsicmVhc29uaW5nX3Rva2VucyI6MH0sInRvdGFsX3Rva2VucyI6MH0sInJlc3BvbnNlX2lkIjpudWxsfQ==" - } - ] - }, - "scheduledEventId": "11", - "startedEventId": "12", - "identity": "16872@Tims-MacBook-Pro.local" - } - }, - { - "eventId": "14", - "eventTime": "2025-06-30T16:04:02.732678Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", - "taskId": "1056465", - "workflowTaskScheduledEventAttributes": { - "taskQueue": { - "name": "16872@Tims-MacBook-Pro.local-547b7584bee9475f87bc46161a6291a5", - "kind": "TASK_QUEUE_KIND_STICKY", - "normalName": "1e8fa3c8-41b4-4ea7-907b-63088b63e36f" - }, - "startToCloseTimeout": "10s", - "attempt": 1 - } - }, - { - "eventId": "15", - "eventTime": "2025-06-30T16:04:02.733464Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", - "taskId": "1056469", - "workflowTaskStartedEventAttributes": { - "scheduledEventId": "14", - "identity": "16872@Tims-MacBook-Pro.local", - "requestId": "26e50494-c82f-4c88-a9a1-aa77bddcb994", - "historySizeBytes": "5361", - "workerVersion": { - "buildId": "01c9746b80ed5a14fc25fb053c5e410a" - } - } - }, - { - "eventId": "16", - "eventTime": "2025-06-30T16:04:02.738255Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", - "taskId": "1056473", - "workflowTaskCompletedEventAttributes": { - "scheduledEventId": "14", - "startedEventId": "15", - "identity": "16872@Tims-MacBook-Pro.local", - "workerVersion": { - "buildId": "01c9746b80ed5a14fc25fb053c5e410a" - }, - "sdkMetadata": {}, - "meteringMetadata": {} - } - }, - { - "eventId": "17", - "eventTime": "2025-06-30T16:04:02.738276Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_SCHEDULED", - "taskId": "1056474", - "activityTaskScheduledEventAttributes": { - "activityId": "3", - "activityType": { - "name": "invoke_model_activity" - }, - "taskQueue": { - "name": "1e8fa3c8-41b4-4ea7-907b-63088b63e36f", - "kind": "TASK_QUEUE_KIND_NORMAL" - }, - "header": { - "fields": { - "__openai_span": { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJ0cmFjZU5hbWUiOiJPcmNoZXN0cmF0b3IgZXZhbHVhdG9yIiwic3BhbklkIjoic3Bhbl9mYWFjMGZkM2Y2ZTE0OTEwOWNkNTBiMjMiLCJ0cmFjZUlkIjoidHJhY2VfN2ZjMzk4YmZiZGYzNDhmMDg0YTYzZDRkNjI0OGUwNDUifQ==" - } - } - }, - "input": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJtb2RlbF9uYW1lIjpudWxsLCJzeXN0ZW1faW5zdHJ1Y3Rpb25zIjoiWW91IGFyZSBhIHRyYW5zbGF0aW9uIGFnZW50LiBZb3UgdXNlIHRoZSB0b29scyBnaXZlbiB0byB5b3UgdG8gdHJhbnNsYXRlLklmIGFza2VkIGZvciBtdWx0aXBsZSB0cmFuc2xhdGlvbnMsIHlvdSBjYWxsIHRoZSByZWxldmFudCB0b29scyBpbiBvcmRlci5Zb3UgbmV2ZXIgdHJhbnNsYXRlIG9uIHlvdXIgb3duLCB5b3UgYWx3YXlzIHVzZSB0aGUgcHJvdmlkZWQgdG9vbHMuIiwiaW5wdXQiOlt7ImNvbnRlbnQiOiJUcmFuc2xhdGUgdG8gU3BhbmlzaDogJ0kgYW0gZnVsbCciLCJyb2xlIjoidXNlciJ9LHsiYXJndW1lbnRzIjoie1wiaW5wdXRcIjpcIkkgYW0gZnVsbFwifSIsImNhbGxfaWQiOiJjYWxsIiwibmFtZSI6InRyYW5zbGF0ZV90b19zcGFuaXNoIiwidHlwZSI6ImZ1bmN0aW9uX2NhbGwiLCJpZCI6ImlkIiwic3RhdHVzIjoiY29tcGxldGVkIn0seyJjYWxsX2lkIjoiY2FsbCIsIm91dHB1dCI6IkVzdG95IGxsZW5vLiIsInR5cGUiOiJmdW5jdGlvbl9jYWxsX291dHB1dCJ9XSwibW9kZWxfc2V0dGluZ3MiOnsidGVtcGVyYXR1cmUiOm51bGwsInRvcF9wIjpudWxsLCJmcmVxdWVuY3lfcGVuYWx0eSI6bnVsbCwicHJlc2VuY2VfcGVuYWx0eSI6bnVsbCwidG9vbF9jaG9pY2UiOm51bGwsInBhcmFsbGVsX3Rvb2xfY2FsbHMiOm51bGwsInRydW5jYXRpb24iOm51bGwsIm1heF90b2tlbnMiOm51bGwsInJlYXNvbmluZyI6bnVsbCwibWV0YWRhdGEiOm51bGwsInN0b3JlIjpudWxsLCJpbmNsdWRlX3VzYWdlIjpudWxsLCJleHRyYV9xdWVyeSI6bnVsbCwiZXh0cmFfYm9keSI6bnVsbCwiZXh0cmFfaGVhZGVycyI6bnVsbCwiZXh0cmFfYXJncyI6bnVsbH0sInRvb2xzIjpbeyJuYW1lIjoidHJhbnNsYXRlX3RvX3NwYW5pc2giLCJkZXNjcmlwdGlvbiI6IlRyYW5zbGF0ZSB0aGUgdXNlcidzIG1lc3NhZ2UgdG8gU3BhbmlzaCIsInBhcmFtc19qc29uX3NjaGVtYSI6eyJwcm9wZXJ0aWVzIjp7ImlucHV0Ijp7InRpdGxlIjoiSW5wdXQiLCJ0eXBlIjoic3RyaW5nIn19LCJyZXF1aXJlZCI6WyJpbnB1dCJdLCJ0aXRsZSI6InRyYW5zbGF0ZV90b19zcGFuaXNoX2FyZ3MiLCJ0eXBlIjoib2JqZWN0IiwiYWRkaXRpb25hbFByb3BlcnRpZXMiOmZhbHNlfSwic3RyaWN0X2pzb25fc2NoZW1hIjp0cnVlfSx7Im5hbWUiOiJ0cmFuc2xhdGVfdG9fZnJlbmNoIiwiZGVzY3JpcHRpb24iOiJUcmFuc2xhdGUgdGhlIHVzZXIncyBtZXNzYWdlIHRvIEZyZW5jaCIsInBhcmFtc19qc29uX3NjaGVtYSI6eyJwcm9wZXJ0aWVzIjp7ImlucHV0Ijp7InRpdGxlIjoiSW5wdXQiLCJ0eXBlIjoic3RyaW5nIn19LCJyZXF1aXJlZCI6WyJpbnB1dCJdLCJ0aXRsZSI6InRyYW5zbGF0ZV90b19mcmVuY2hfYXJncyIsInR5cGUiOiJvYmplY3QiLCJhZGRpdGlvbmFsUHJvcGVydGllcyI6ZmFsc2V9LCJzdHJpY3RfanNvbl9zY2hlbWEiOnRydWV9LHsibmFtZSI6InRyYW5zbGF0ZV90b19pdGFsaWFuIiwiZGVzY3JpcHRpb24iOiJUcmFuc2xhdGUgdGhlIHVzZXIncyBtZXNzYWdlIHRvIEl0YWxpYW4iLCJwYXJhbXNfanNvbl9zY2hlbWEiOnsicHJvcGVydGllcyI6eyJpbnB1dCI6eyJ0aXRsZSI6IklucHV0IiwidHlwZSI6InN0cmluZyJ9fSwicmVxdWlyZWQiOlsiaW5wdXQiXSwidGl0bGUiOiJ0cmFuc2xhdGVfdG9faXRhbGlhbl9hcmdzIiwidHlwZSI6Im9iamVjdCIsImFkZGl0aW9uYWxQcm9wZXJ0aWVzIjpmYWxzZX0sInN0cmljdF9qc29uX3NjaGVtYSI6dHJ1ZX1dLCJvdXRwdXRfc2NoZW1hIjpudWxsLCJoYW5kb2ZmcyI6W10sInRyYWNpbmciOjEsInByZXZpb3VzX3Jlc3BvbnNlX2lkIjpudWxsLCJwcm9tcHQiOm51bGx9" - } - ] - }, - "scheduleToCloseTimeout": "30s", - "scheduleToStartTimeout": "30s", - "startToCloseTimeout": "10s", - "heartbeatTimeout": "0s", - "workflowTaskCompletedEventId": "16", - "retryPolicy": { - "initialInterval": "1s", - "backoffCoefficient": 2.0, - "maximumInterval": "100s" - }, - "useWorkflowBuildId": true - } - }, - { - "eventId": "18", - "eventTime": "2025-06-30T16:04:02.739011Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_STARTED", - "taskId": "1056479", - "activityTaskStartedEventAttributes": { - "scheduledEventId": "17", - "identity": "16872@Tims-MacBook-Pro.local", - "requestId": "dd454850-7b41-4ea2-aa03-88eb1995aa5a", - "attempt": 1, - "workerVersion": { - "buildId": "01c9746b80ed5a14fc25fb053c5e410a" - } - } - }, - { - "eventId": "19", - "eventTime": "2025-06-30T16:04:02.743068Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_COMPLETED", - "taskId": "1056480", - "activityTaskCompletedEventAttributes": { - "result": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJvdXRwdXQiOlt7ImlkIjoiIiwiY29udGVudCI6W3siYW5ub3RhdGlvbnMiOltdLCJ0ZXh0IjoiVGhlIHRyYW5zbGF0aW9uIHRvIFNwYW5pc2ggaXM6IFwiRXN0b3kgbGxlbm8uXCIiLCJ0eXBlIjoib3V0cHV0X3RleHQifV0sInJvbGUiOiJhc3Npc3RhbnQiLCJzdGF0dXMiOiJjb21wbGV0ZWQiLCJ0eXBlIjoibWVzc2FnZSJ9XSwidXNhZ2UiOnsicmVxdWVzdHMiOjAsImlucHV0X3Rva2VucyI6MCwiaW5wdXRfdG9rZW5zX2RldGFpbHMiOnsiY2FjaGVkX3Rva2VucyI6MH0sIm91dHB1dF90b2tlbnMiOjAsIm91dHB1dF90b2tlbnNfZGV0YWlscyI6eyJyZWFzb25pbmdfdG9rZW5zIjowfSwidG90YWxfdG9rZW5zIjowfSwicmVzcG9uc2VfaWQiOm51bGx9" - } - ] - }, - "scheduledEventId": "17", - "startedEventId": "18", - "identity": "16872@Tims-MacBook-Pro.local" - } - }, - { - "eventId": "20", - "eventTime": "2025-06-30T16:04:02.743071Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", - "taskId": "1056481", - "workflowTaskScheduledEventAttributes": { - "taskQueue": { - "name": "16872@Tims-MacBook-Pro.local-547b7584bee9475f87bc46161a6291a5", - "kind": "TASK_QUEUE_KIND_STICKY", - "normalName": "1e8fa3c8-41b4-4ea7-907b-63088b63e36f" - }, - "startToCloseTimeout": "10s", - "attempt": 1 - } - }, - { - "eventId": "21", - "eventTime": "2025-06-30T16:04:02.743631Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", - "taskId": "1056485", - "workflowTaskStartedEventAttributes": { - "scheduledEventId": "20", - "identity": "16872@Tims-MacBook-Pro.local", - "requestId": "3d42b98e-c61a-463b-ad9c-4ae65de03d86", - "historySizeBytes": "8542", - "workerVersion": { - "buildId": "01c9746b80ed5a14fc25fb053c5e410a" - } - } - }, - { - "eventId": "22", - "eventTime": "2025-06-30T16:04:02.748200Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", - "taskId": "1056489", - "workflowTaskCompletedEventAttributes": { - "scheduledEventId": "20", - "startedEventId": "21", - "identity": "16872@Tims-MacBook-Pro.local", - "workerVersion": { - "buildId": "01c9746b80ed5a14fc25fb053c5e410a" - }, - "sdkMetadata": {}, - "meteringMetadata": {} - } - }, - { - "eventId": "23", - "eventTime": "2025-06-30T16:04:02.748225Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_SCHEDULED", - "taskId": "1056490", - "activityTaskScheduledEventAttributes": { - "activityId": "4", - "activityType": { - "name": "invoke_model_activity" - }, - "taskQueue": { - "name": "1e8fa3c8-41b4-4ea7-907b-63088b63e36f", - "kind": "TASK_QUEUE_KIND_NORMAL" - }, - "header": { - "fields": { - "__openai_span": { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJ0cmFjZU5hbWUiOiJPcmNoZXN0cmF0b3IgZXZhbHVhdG9yIiwic3BhbklkIjoic3Bhbl85MDVlNTdkZGNjYTU0NDhmYTFkYTE1NzkiLCJ0cmFjZUlkIjoidHJhY2VfN2ZjMzk4YmZiZGYzNDhmMDg0YTYzZDRkNjI0OGUwNDUifQ==" - } - } - }, - "input": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJtb2RlbF9uYW1lIjpudWxsLCJzeXN0ZW1faW5zdHJ1Y3Rpb25zIjoiWW91IGluc3BlY3QgdHJhbnNsYXRpb25zLCBjb3JyZWN0IHRoZW0gaWYgbmVlZGVkLCBhbmQgcHJvZHVjZSBhIGZpbmFsIGNvbmNhdGVuYXRlZCByZXNwb25zZS4iLCJpbnB1dCI6W3siY29udGVudCI6IlRyYW5zbGF0ZSB0byBTcGFuaXNoOiAnSSBhbSBmdWxsJyIsInJvbGUiOiJ1c2VyIn0seyJhcmd1bWVudHMiOiJ7XCJpbnB1dFwiOlwiSSBhbSBmdWxsXCJ9IiwiY2FsbF9pZCI6ImNhbGwiLCJuYW1lIjoidHJhbnNsYXRlX3RvX3NwYW5pc2giLCJ0eXBlIjoiZnVuY3Rpb25fY2FsbCIsImlkIjoiaWQiLCJzdGF0dXMiOiJjb21wbGV0ZWQifSx7ImNhbGxfaWQiOiJjYWxsIiwib3V0cHV0IjoiRXN0b3kgbGxlbm8uIiwidHlwZSI6ImZ1bmN0aW9uX2NhbGxfb3V0cHV0In0seyJpZCI6IiIsImNvbnRlbnQiOlt7ImFubm90YXRpb25zIjpbXSwidGV4dCI6IlRoZSB0cmFuc2xhdGlvbiB0byBTcGFuaXNoIGlzOiBcIkVzdG95IGxsZW5vLlwiIiwidHlwZSI6Im91dHB1dF90ZXh0In1dLCJyb2xlIjoiYXNzaXN0YW50Iiwic3RhdHVzIjoiY29tcGxldGVkIiwidHlwZSI6Im1lc3NhZ2UifV0sIm1vZGVsX3NldHRpbmdzIjp7InRlbXBlcmF0dXJlIjpudWxsLCJ0b3BfcCI6bnVsbCwiZnJlcXVlbmN5X3BlbmFsdHkiOm51bGwsInByZXNlbmNlX3BlbmFsdHkiOm51bGwsInRvb2xfY2hvaWNlIjpudWxsLCJwYXJhbGxlbF90b29sX2NhbGxzIjpudWxsLCJ0cnVuY2F0aW9uIjpudWxsLCJtYXhfdG9rZW5zIjpudWxsLCJyZWFzb25pbmciOm51bGwsIm1ldGFkYXRhIjpudWxsLCJzdG9yZSI6bnVsbCwiaW5jbHVkZV91c2FnZSI6bnVsbCwiZXh0cmFfcXVlcnkiOm51bGwsImV4dHJhX2JvZHkiOm51bGwsImV4dHJhX2hlYWRlcnMiOm51bGwsImV4dHJhX2FyZ3MiOm51bGx9LCJ0b29scyI6W10sIm91dHB1dF9zY2hlbWEiOm51bGwsImhhbmRvZmZzIjpbXSwidHJhY2luZyI6MSwicHJldmlvdXNfcmVzcG9uc2VfaWQiOm51bGwsInByb21wdCI6bnVsbH0=" - } - ] - }, - "scheduleToCloseTimeout": "30s", - "scheduleToStartTimeout": "30s", - "startToCloseTimeout": "10s", - "heartbeatTimeout": "0s", - "workflowTaskCompletedEventId": "22", - "retryPolicy": { - "initialInterval": "1s", - "backoffCoefficient": 2.0, - "maximumInterval": "100s" - }, - "useWorkflowBuildId": true - }, - "userMetadata": { - "summary": { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "W3siYW5ub3RhdGlvbnMiOltdLCJ0ZXh0IjoiVGhlIHRyYW5zbGF0aW9uIHRvIFNwYW5pc2ggaXM6IFwiRXN0b3kgbGxlbm8uXCIiLCJ0eXBlIjoib3V0cHV0X3RleHQifV0=" - } - } - }, - { - "eventId": "24", - "eventTime": "2025-06-30T16:04:02.748848Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_STARTED", - "taskId": "1056495", - "activityTaskStartedEventAttributes": { - "scheduledEventId": "23", - "identity": "16872@Tims-MacBook-Pro.local", - "requestId": "6bc19064-47e9-404a-a461-180920e82f88", - "attempt": 1, - "workerVersion": { - "buildId": "01c9746b80ed5a14fc25fb053c5e410a" - } - } - }, - { - "eventId": "25", - "eventTime": "2025-06-30T16:04:02.753447Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_COMPLETED", - "taskId": "1056496", - "activityTaskCompletedEventAttributes": { - "result": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJvdXRwdXQiOlt7ImlkIjoiIiwiY29udGVudCI6W3siYW5ub3RhdGlvbnMiOltdLCJ0ZXh0IjoiVGhlIHRyYW5zbGF0aW9uIHRvIFNwYW5pc2ggaXM6IFwiRXN0b3kgbGxlbm8uXCIiLCJ0eXBlIjoib3V0cHV0X3RleHQifV0sInJvbGUiOiJhc3Npc3RhbnQiLCJzdGF0dXMiOiJjb21wbGV0ZWQiLCJ0eXBlIjoibWVzc2FnZSJ9XSwidXNhZ2UiOnsicmVxdWVzdHMiOjAsImlucHV0X3Rva2VucyI6MCwiaW5wdXRfdG9rZW5zX2RldGFpbHMiOnsiY2FjaGVkX3Rva2VucyI6MH0sIm91dHB1dF90b2tlbnMiOjAsIm91dHB1dF90b2tlbnNfZGV0YWlscyI6eyJyZWFzb25pbmdfdG9rZW5zIjowfSwidG90YWxfdG9rZW5zIjowfSwicmVzcG9uc2VfaWQiOm51bGx9" - } - ] - }, - "scheduledEventId": "23", - "startedEventId": "24", - "identity": "16872@Tims-MacBook-Pro.local" - } - }, - { - "eventId": "26", - "eventTime": "2025-06-30T16:04:02.753450Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", - "taskId": "1056497", - "workflowTaskScheduledEventAttributes": { - "taskQueue": { - "name": "16872@Tims-MacBook-Pro.local-547b7584bee9475f87bc46161a6291a5", - "kind": "TASK_QUEUE_KIND_STICKY", - "normalName": "1e8fa3c8-41b4-4ea7-907b-63088b63e36f" - }, - "startToCloseTimeout": "10s", - "attempt": 1 - } - }, - { - "eventId": "27", - "eventTime": "2025-06-30T16:04:02.754059Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", - "taskId": "1056501", - "workflowTaskStartedEventAttributes": { - "scheduledEventId": "26", - "identity": "16872@Tims-MacBook-Pro.local", - "requestId": "cbfd4087-6e86-498e-b451-5c26e2548334", - "historySizeBytes": "11023", - "workerVersion": { - "buildId": "01c9746b80ed5a14fc25fb053c5e410a" - } - } - }, - { - "eventId": "28", - "eventTime": "2025-06-30T16:04:02.758051Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", - "taskId": "1056505", - "workflowTaskCompletedEventAttributes": { - "scheduledEventId": "26", - "startedEventId": "27", - "identity": "16872@Tims-MacBook-Pro.local", - "workerVersion": { - "buildId": "01c9746b80ed5a14fc25fb053c5e410a" - }, - "sdkMetadata": {}, - "meteringMetadata": {} - } - }, - { - "eventId": "29", - "eventTime": "2025-06-30T16:04:02.758068Z", - "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_COMPLETED", - "taskId": "1056506", - "workflowExecutionCompletedEventAttributes": { - "result": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "IlRoZSB0cmFuc2xhdGlvbiB0byBTcGFuaXNoIGlzOiBcIkVzdG95IGxsZW5vLlwiIg==" - } - ] - }, - "workflowTaskCompletedEventId": "28" - } - } - ] -} \ No newline at end of file diff --git a/tests/contrib/openai_agents/histories/customer-service-workflow-history.json b/tests/contrib/openai_agents/histories/customer-service-workflow-history.json deleted file mode 100644 index d5c878c4b..000000000 --- a/tests/contrib/openai_agents/histories/customer-service-workflow-history.json +++ /dev/null @@ -1,1356 +0,0 @@ -{ - "events": [ - { - "eventId": "1", - "eventTime": "2025-06-30T16:05:23.897631Z", - "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_STARTED", - "taskId": "1056511", - "workflowExecutionStartedEventAttributes": { - "workflowType": { - "name": "CustomerServiceWorkflow" - }, - "taskQueue": { - "name": "be82986c-9ddc-441b-9833-c3fce7c79df1", - "kind": "TASK_QUEUE_KIND_NORMAL" - }, - "workflowExecutionTimeout": "30s", - "workflowRunTimeout": "30s", - "workflowTaskTimeout": "10s", - "originalExecutionRunId": "0197c196-0539-799c-8a5d-af7f3d1b59e2", - "identity": "16915@Tims-MacBook-Pro.local", - "firstExecutionRunId": "0197c196-0539-799c-8a5d-af7f3d1b59e2", - "attempt": 1, - "workflowExecutionExpirationTime": "2025-06-30T16:05:53.897Z", - "firstWorkflowTaskBackoff": "0s", - "workflowId": "customer-service-d7028390-930d-4478-b69f-185af000bb17" - } - }, - { - "eventId": "2", - "eventTime": "2025-06-30T16:05:23.897679Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", - "taskId": "1056512", - "workflowTaskScheduledEventAttributes": { - "taskQueue": { - "name": "be82986c-9ddc-441b-9833-c3fce7c79df1", - "kind": "TASK_QUEUE_KIND_NORMAL" - }, - "startToCloseTimeout": "10s", - "attempt": 1 - } - }, - { - "eventId": "3", - "eventTime": "2025-06-30T16:05:23.898564Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", - "taskId": "1056518", - "workflowTaskStartedEventAttributes": { - "scheduledEventId": "2", - "identity": "16915@Tims-MacBook-Pro.local", - "requestId": "d4bd65f3-93f8-46cb-bf44-e7f2a750e7e4", - "historySizeBytes": "367", - "workerVersion": { - "buildId": "01c9746b80ed5a14fc25fb053c5e410a" - } - } - }, - { - "eventId": "4", - "eventTime": "2025-06-30T16:05:23.935560Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", - "taskId": "1056522", - "workflowTaskCompletedEventAttributes": { - "scheduledEventId": "2", - "startedEventId": "3", - "identity": "16915@Tims-MacBook-Pro.local", - "workerVersion": { - "buildId": "01c9746b80ed5a14fc25fb053c5e410a" - }, - "sdkMetadata": { - "coreUsedFlags": [ - 1, - 2, - 3 - ], - "sdkName": "temporal-python", - "sdkVersion": "1.13.0" - }, - "meteringMetadata": {} - } - }, - { - "eventId": "5", - "eventTime": "2025-06-30T16:05:23.935885Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", - "taskId": "1056526", - "workflowTaskScheduledEventAttributes": { - "taskQueue": { - "name": "16915@Tims-MacBook-Pro.local-b3f84e19537b4e0a99176a97b19a0c7d", - "kind": "TASK_QUEUE_KIND_STICKY", - "normalName": "be82986c-9ddc-441b-9833-c3fce7c79df1" - }, - "startToCloseTimeout": "10s", - "attempt": 1 - } - }, - { - "eventId": "6", - "eventTime": "2025-06-30T16:05:23.935887Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", - "taskId": "1056527", - "workflowTaskStartedEventAttributes": { - "scheduledEventId": "5", - "identity": "16915@Tims-MacBook-Pro.local", - "requestId": "request-from-RespondWorkflowTaskCompleted", - "historySizeBytes": "634", - "workerVersion": { - "buildId": "01c9746b80ed5a14fc25fb053c5e410a" - } - } - }, - { - "eventId": "7", - "eventTime": "2025-06-30T16:05:23.945868Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", - "taskId": "1056528", - "workflowTaskCompletedEventAttributes": { - "scheduledEventId": "5", - "startedEventId": "6", - "identity": "16915@Tims-MacBook-Pro.local", - "workerVersion": { - "buildId": "01c9746b80ed5a14fc25fb053c5e410a" - }, - "sdkMetadata": {}, - "meteringMetadata": {} - } - }, - { - "eventId": "8", - "eventTime": "2025-06-30T16:05:23.945901Z", - "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_UPDATE_ACCEPTED", - "taskId": "1056529", - "workflowExecutionUpdateAcceptedEventAttributes": { - "protocolInstanceId": "84ebe654-6558-4df3-a536-f391bd61ddef", - "acceptedRequestMessageId": "84ebe654-6558-4df3-a536-f391bd61ddef/request", - "acceptedRequestSequencingEventId": "5", - "acceptedRequest": { - "meta": { - "updateId": "84ebe654-6558-4df3-a536-f391bd61ddef", - "identity": "16915@Tims-MacBook-Pro.local" - }, - "input": { - "name": "process_user_message", - "args": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJ1c2VyX2lucHV0IjoiSGVsbG8iLCJjaGF0X2xlbmd0aCI6MH0=" - } - ] - } - } - } - } - }, - { - "eventId": "9", - "eventTime": "2025-06-30T16:05:23.945923Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_SCHEDULED", - "taskId": "1056530", - "activityTaskScheduledEventAttributes": { - "activityId": "1", - "activityType": { - "name": "invoke_model_activity" - }, - "taskQueue": { - "name": "be82986c-9ddc-441b-9833-c3fce7c79df1", - "kind": "TASK_QUEUE_KIND_NORMAL" - }, - "header": { - "fields": { - "__openai_span": { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJ0cmFjZU5hbWUiOiJDdXN0b21lciBzZXJ2aWNlIiwic3BhbklkIjoic3Bhbl9lZmRlY2VhZjBmMzI0MDgyOGQxNTM4OWMiLCJ0cmFjZUlkIjoidHJhY2VfNzgwYzg0NjY5OTgzNGQ1ODljMjZkNzYyZDliNzMwOWIifQ==" - } - } - }, - "input": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJtb2RlbF9uYW1lIjpudWxsLCJzeXN0ZW1faW5zdHJ1Y3Rpb25zIjoiIyBTeXN0ZW0gY29udGV4dFxuWW91IGFyZSBwYXJ0IG9mIGEgbXVsdGktYWdlbnQgc3lzdGVtIGNhbGxlZCB0aGUgQWdlbnRzIFNESywgZGVzaWduZWQgdG8gbWFrZSBhZ2VudCBjb29yZGluYXRpb24gYW5kIGV4ZWN1dGlvbiBlYXN5LiBBZ2VudHMgdXNlcyB0d28gcHJpbWFyeSBhYnN0cmFjdGlvbjogKipBZ2VudHMqKiBhbmQgKipIYW5kb2ZmcyoqLiBBbiBhZ2VudCBlbmNvbXBhc3NlcyBpbnN0cnVjdGlvbnMgYW5kIHRvb2xzIGFuZCBjYW4gaGFuZCBvZmYgYSBjb252ZXJzYXRpb24gdG8gYW5vdGhlciBhZ2VudCB3aGVuIGFwcHJvcHJpYXRlLiBIYW5kb2ZmcyBhcmUgYWNoaWV2ZWQgYnkgY2FsbGluZyBhIGhhbmRvZmYgZnVuY3Rpb24sIGdlbmVyYWxseSBuYW1lZCBgdHJhbnNmZXJfdG9fPGFnZW50X25hbWU+YC4gVHJhbnNmZXJzIGJldHdlZW4gYWdlbnRzIGFyZSBoYW5kbGVkIHNlYW1sZXNzbHkgaW4gdGhlIGJhY2tncm91bmQ7IGRvIG5vdCBtZW50aW9uIG9yIGRyYXcgYXR0ZW50aW9uIHRvIHRoZXNlIHRyYW5zZmVycyBpbiB5b3VyIGNvbnZlcnNhdGlvbiB3aXRoIHRoZSB1c2VyLlxuIFlvdSBhcmUgYSBoZWxwZnVsIHRyaWFnaW5nIGFnZW50LiBZb3UgY2FuIHVzZSB5b3VyIHRvb2xzIHRvIGRlbGVnYXRlIHF1ZXN0aW9ucyB0byBvdGhlciBhcHByb3ByaWF0ZSBhZ2VudHMuIiwiaW5wdXQiOlt7ImNvbnRlbnQiOiJIZWxsbyIsInJvbGUiOiJ1c2VyIn1dLCJtb2RlbF9zZXR0aW5ncyI6eyJ0ZW1wZXJhdHVyZSI6bnVsbCwidG9wX3AiOm51bGwsImZyZXF1ZW5jeV9wZW5hbHR5IjpudWxsLCJwcmVzZW5jZV9wZW5hbHR5IjpudWxsLCJ0b29sX2Nob2ljZSI6bnVsbCwicGFyYWxsZWxfdG9vbF9jYWxscyI6bnVsbCwidHJ1bmNhdGlvbiI6bnVsbCwibWF4X3Rva2VucyI6bnVsbCwicmVhc29uaW5nIjpudWxsLCJtZXRhZGF0YSI6bnVsbCwic3RvcmUiOm51bGwsImluY2x1ZGVfdXNhZ2UiOm51bGwsImV4dHJhX3F1ZXJ5IjpudWxsLCJleHRyYV9ib2R5IjpudWxsLCJleHRyYV9oZWFkZXJzIjpudWxsLCJleHRyYV9hcmdzIjpudWxsfSwidG9vbHMiOltdLCJvdXRwdXRfc2NoZW1hIjpudWxsLCJoYW5kb2ZmcyI6W3sidG9vbF9uYW1lIjoidHJhbnNmZXJfdG9fZmFxX2FnZW50IiwidG9vbF9kZXNjcmlwdGlvbiI6IkhhbmRvZmYgdG8gdGhlIEZBUSBBZ2VudCBhZ2VudCB0byBoYW5kbGUgdGhlIHJlcXVlc3QuIEEgaGVscGZ1bCBhZ2VudCB0aGF0IGNhbiBhbnN3ZXIgcXVlc3Rpb25zIGFib3V0IHRoZSBhaXJsaW5lLiIsImlucHV0X2pzb25fc2NoZW1hIjp7ImFkZGl0aW9uYWxQcm9wZXJ0aWVzIjpmYWxzZSwidHlwZSI6Im9iamVjdCIsInByb3BlcnRpZXMiOnt9LCJyZXF1aXJlZCI6W119LCJhZ2VudF9uYW1lIjoiRkFRIEFnZW50Iiwic3RyaWN0X2pzb25fc2NoZW1hIjp0cnVlfSx7InRvb2xfbmFtZSI6InRyYW5zZmVyX3RvX3NlYXRfYm9va2luZ19hZ2VudCIsInRvb2xfZGVzY3JpcHRpb24iOiJIYW5kb2ZmIHRvIHRoZSBTZWF0IEJvb2tpbmcgQWdlbnQgYWdlbnQgdG8gaGFuZGxlIHRoZSByZXF1ZXN0LiBBIGhlbHBmdWwgYWdlbnQgdGhhdCBjYW4gdXBkYXRlIGEgc2VhdCBvbiBhIGZsaWdodC4iLCJpbnB1dF9qc29uX3NjaGVtYSI6eyJhZGRpdGlvbmFsUHJvcGVydGllcyI6ZmFsc2UsInR5cGUiOiJvYmplY3QiLCJwcm9wZXJ0aWVzIjp7fSwicmVxdWlyZWQiOltdfSwiYWdlbnRfbmFtZSI6IlNlYXQgQm9va2luZyBBZ2VudCIsInN0cmljdF9qc29uX3NjaGVtYSI6dHJ1ZX1dLCJ0cmFjaW5nIjoxLCJwcmV2aW91c19yZXNwb25zZV9pZCI6bnVsbCwicHJvbXB0IjpudWxsfQ==" - } - ] - }, - "scheduleToCloseTimeout": "30s", - "scheduleToStartTimeout": "30s", - "startToCloseTimeout": "10s", - "heartbeatTimeout": "0s", - "workflowTaskCompletedEventId": "7", - "retryPolicy": { - "initialInterval": "1s", - "backoffCoefficient": 2.0, - "maximumInterval": "100s" - }, - "useWorkflowBuildId": true - }, - "userMetadata": { - "summary": { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "IkhlbGxvIg==" - } - } - }, - { - "eventId": "10", - "eventTime": "2025-06-30T16:05:23.946659Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_STARTED", - "taskId": "1056536", - "activityTaskStartedEventAttributes": { - "scheduledEventId": "9", - "identity": "16915@Tims-MacBook-Pro.local", - "requestId": "3a5187f5-d97d-4dc3-bad1-9fbcd45973c0", - "attempt": 1, - "workerVersion": { - "buildId": "01c9746b80ed5a14fc25fb053c5e410a" - } - } - }, - { - "eventId": "11", - "eventTime": "2025-06-30T16:05:23.966721Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_COMPLETED", - "taskId": "1056537", - "activityTaskCompletedEventAttributes": { - "result": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJvdXRwdXQiOlt7ImlkIjoiIiwiY29udGVudCI6W3siYW5ub3RhdGlvbnMiOltdLCJ0ZXh0IjoiSGkgdGhlcmUhIEhvdyBjYW4gSSBhc3Npc3QgeW91IHRvZGF5PyIsInR5cGUiOiJvdXRwdXRfdGV4dCJ9XSwicm9sZSI6ImFzc2lzdGFudCIsInN0YXR1cyI6ImNvbXBsZXRlZCIsInR5cGUiOiJtZXNzYWdlIn1dLCJ1c2FnZSI6eyJyZXF1ZXN0cyI6MCwiaW5wdXRfdG9rZW5zIjowLCJpbnB1dF90b2tlbnNfZGV0YWlscyI6eyJjYWNoZWRfdG9rZW5zIjowfSwib3V0cHV0X3Rva2VucyI6MCwib3V0cHV0X3Rva2Vuc19kZXRhaWxzIjp7InJlYXNvbmluZ190b2tlbnMiOjB9LCJ0b3RhbF90b2tlbnMiOjB9LCJyZXNwb25zZV9pZCI6bnVsbH0=" - } - ] - }, - "scheduledEventId": "9", - "startedEventId": "10", - "identity": "16915@Tims-MacBook-Pro.local" - } - }, - { - "eventId": "12", - "eventTime": "2025-06-30T16:05:23.966725Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", - "taskId": "1056538", - "workflowTaskScheduledEventAttributes": { - "taskQueue": { - "name": "16915@Tims-MacBook-Pro.local-b3f84e19537b4e0a99176a97b19a0c7d", - "kind": "TASK_QUEUE_KIND_STICKY", - "normalName": "be82986c-9ddc-441b-9833-c3fce7c79df1" - }, - "startToCloseTimeout": "10s", - "attempt": 1 - } - }, - { - "eventId": "13", - "eventTime": "2025-06-30T16:05:23.967600Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", - "taskId": "1056542", - "workflowTaskStartedEventAttributes": { - "scheduledEventId": "12", - "identity": "16915@Tims-MacBook-Pro.local", - "requestId": "b906d85d-cd71-4656-b6a7-082a1bc52243", - "historySizeBytes": "4251", - "workerVersion": { - "buildId": "01c9746b80ed5a14fc25fb053c5e410a" - } - } - }, - { - "eventId": "14", - "eventTime": "2025-06-30T16:05:23.979403Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", - "taskId": "1056546", - "workflowTaskCompletedEventAttributes": { - "scheduledEventId": "12", - "startedEventId": "13", - "identity": "16915@Tims-MacBook-Pro.local", - "workerVersion": { - "buildId": "01c9746b80ed5a14fc25fb053c5e410a" - }, - "sdkMetadata": {}, - "meteringMetadata": {} - } - }, - { - "eventId": "15", - "eventTime": "2025-06-30T16:05:23.979438Z", - "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_UPDATE_COMPLETED", - "taskId": "1056547", - "workflowExecutionUpdateCompletedEventAttributes": { - "meta": { - "updateId": "84ebe654-6558-4df3-a536-f391bd61ddef", - "identity": "16915@Tims-MacBook-Pro.local" - }, - "outcome": { - "success": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "WyJVc2VyOiBIZWxsbyIsIlRyaWFnZSBBZ2VudDogSGkgdGhlcmUhIEhvdyBjYW4gSSBhc3Npc3QgeW91IHRvZGF5PyJd" - } - ] - } - }, - "acceptedEventId": "8" - } - }, - { - "eventId": "16", - "eventTime": "2025-06-30T16:05:23.984766Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", - "taskId": "1056553", - "workflowTaskScheduledEventAttributes": { - "taskQueue": { - "name": "16915@Tims-MacBook-Pro.local-b3f84e19537b4e0a99176a97b19a0c7d", - "kind": "TASK_QUEUE_KIND_STICKY", - "normalName": "be82986c-9ddc-441b-9833-c3fce7c79df1" - }, - "startToCloseTimeout": "10s", - "attempt": 1 - } - }, - { - "eventId": "17", - "eventTime": "2025-06-30T16:05:23.985105Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", - "taskId": "1056554", - "workflowTaskStartedEventAttributes": { - "scheduledEventId": "16", - "identity": "16915@Tims-MacBook-Pro.local", - "requestId": "209063cf-510e-46c2-9896-edc64b67472e", - "historySizeBytes": "4689", - "workerVersion": { - "buildId": "01c9746b80ed5a14fc25fb053c5e410a" - } - } - }, - { - "eventId": "18", - "eventTime": "2025-06-30T16:05:23.994030Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", - "taskId": "1056555", - "workflowTaskCompletedEventAttributes": { - "scheduledEventId": "16", - "startedEventId": "17", - "identity": "16915@Tims-MacBook-Pro.local", - "workerVersion": { - "buildId": "01c9746b80ed5a14fc25fb053c5e410a" - }, - "sdkMetadata": {}, - "meteringMetadata": {} - } - }, - { - "eventId": "19", - "eventTime": "2025-06-30T16:05:23.994061Z", - "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_UPDATE_ACCEPTED", - "taskId": "1056556", - "workflowExecutionUpdateAcceptedEventAttributes": { - "protocolInstanceId": "582a5778-a28c-4a2d-bee1-9629da75c8a5", - "acceptedRequestMessageId": "582a5778-a28c-4a2d-bee1-9629da75c8a5/request", - "acceptedRequestSequencingEventId": "16", - "acceptedRequest": { - "meta": { - "updateId": "582a5778-a28c-4a2d-bee1-9629da75c8a5", - "identity": "16915@Tims-MacBook-Pro.local" - }, - "input": { - "name": "process_user_message", - "args": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJ1c2VyX2lucHV0IjoiQm9vayBtZSBhIGZsaWdodCB0byBQRFgiLCJjaGF0X2xlbmd0aCI6Mn0=" - } - ] - } - } - } - } - }, - { - "eventId": "20", - "eventTime": "2025-06-30T16:05:23.994084Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_SCHEDULED", - "taskId": "1056557", - "activityTaskScheduledEventAttributes": { - "activityId": "2", - "activityType": { - "name": "invoke_model_activity" - }, - "taskQueue": { - "name": "be82986c-9ddc-441b-9833-c3fce7c79df1", - "kind": "TASK_QUEUE_KIND_NORMAL" - }, - "header": { - "fields": { - "__openai_span": { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJ0cmFjZU5hbWUiOiJDdXN0b21lciBzZXJ2aWNlIiwic3BhbklkIjoic3Bhbl8wM2Y5YzFkYWQ4MmM0N2JmYTE4MWExMTAiLCJ0cmFjZUlkIjoidHJhY2VfZGQxMWQyNTYxOWQ1NDZmMGE4MDgxMzc3OTdiYzE2MjMifQ==" - } - } - }, - "input": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJtb2RlbF9uYW1lIjpudWxsLCJzeXN0ZW1faW5zdHJ1Y3Rpb25zIjoiIyBTeXN0ZW0gY29udGV4dFxuWW91IGFyZSBwYXJ0IG9mIGEgbXVsdGktYWdlbnQgc3lzdGVtIGNhbGxlZCB0aGUgQWdlbnRzIFNESywgZGVzaWduZWQgdG8gbWFrZSBhZ2VudCBjb29yZGluYXRpb24gYW5kIGV4ZWN1dGlvbiBlYXN5LiBBZ2VudHMgdXNlcyB0d28gcHJpbWFyeSBhYnN0cmFjdGlvbjogKipBZ2VudHMqKiBhbmQgKipIYW5kb2ZmcyoqLiBBbiBhZ2VudCBlbmNvbXBhc3NlcyBpbnN0cnVjdGlvbnMgYW5kIHRvb2xzIGFuZCBjYW4gaGFuZCBvZmYgYSBjb252ZXJzYXRpb24gdG8gYW5vdGhlciBhZ2VudCB3aGVuIGFwcHJvcHJpYXRlLiBIYW5kb2ZmcyBhcmUgYWNoaWV2ZWQgYnkgY2FsbGluZyBhIGhhbmRvZmYgZnVuY3Rpb24sIGdlbmVyYWxseSBuYW1lZCBgdHJhbnNmZXJfdG9fPGFnZW50X25hbWU+YC4gVHJhbnNmZXJzIGJldHdlZW4gYWdlbnRzIGFyZSBoYW5kbGVkIHNlYW1sZXNzbHkgaW4gdGhlIGJhY2tncm91bmQ7IGRvIG5vdCBtZW50aW9uIG9yIGRyYXcgYXR0ZW50aW9uIHRvIHRoZXNlIHRyYW5zZmVycyBpbiB5b3VyIGNvbnZlcnNhdGlvbiB3aXRoIHRoZSB1c2VyLlxuIFlvdSBhcmUgYSBoZWxwZnVsIHRyaWFnaW5nIGFnZW50LiBZb3UgY2FuIHVzZSB5b3VyIHRvb2xzIHRvIGRlbGVnYXRlIHF1ZXN0aW9ucyB0byBvdGhlciBhcHByb3ByaWF0ZSBhZ2VudHMuIiwiaW5wdXQiOlt7ImNvbnRlbnQiOiJIZWxsbyIsInJvbGUiOiJ1c2VyIn0seyJpZCI6IiIsImNvbnRlbnQiOlt7ImFubm90YXRpb25zIjpbXSwidGV4dCI6IkhpIHRoZXJlISBIb3cgY2FuIEkgYXNzaXN0IHlvdSB0b2RheT8iLCJ0eXBlIjoib3V0cHV0X3RleHQifV0sInJvbGUiOiJhc3Npc3RhbnQiLCJzdGF0dXMiOiJjb21wbGV0ZWQiLCJ0eXBlIjoibWVzc2FnZSJ9LHsiY29udGVudCI6IkJvb2sgbWUgYSBmbGlnaHQgdG8gUERYIiwicm9sZSI6InVzZXIifV0sIm1vZGVsX3NldHRpbmdzIjp7InRlbXBlcmF0dXJlIjpudWxsLCJ0b3BfcCI6bnVsbCwiZnJlcXVlbmN5X3BlbmFsdHkiOm51bGwsInByZXNlbmNlX3BlbmFsdHkiOm51bGwsInRvb2xfY2hvaWNlIjpudWxsLCJwYXJhbGxlbF90b29sX2NhbGxzIjpudWxsLCJ0cnVuY2F0aW9uIjpudWxsLCJtYXhfdG9rZW5zIjpudWxsLCJyZWFzb25pbmciOm51bGwsIm1ldGFkYXRhIjpudWxsLCJzdG9yZSI6bnVsbCwiaW5jbHVkZV91c2FnZSI6bnVsbCwiZXh0cmFfcXVlcnkiOm51bGwsImV4dHJhX2JvZHkiOm51bGwsImV4dHJhX2hlYWRlcnMiOm51bGwsImV4dHJhX2FyZ3MiOm51bGx9LCJ0b29scyI6W10sIm91dHB1dF9zY2hlbWEiOm51bGwsImhhbmRvZmZzIjpbeyJ0b29sX25hbWUiOiJ0cmFuc2Zlcl90b19mYXFfYWdlbnQiLCJ0b29sX2Rlc2NyaXB0aW9uIjoiSGFuZG9mZiB0byB0aGUgRkFRIEFnZW50IGFnZW50IHRvIGhhbmRsZSB0aGUgcmVxdWVzdC4gQSBoZWxwZnVsIGFnZW50IHRoYXQgY2FuIGFuc3dlciBxdWVzdGlvbnMgYWJvdXQgdGhlIGFpcmxpbmUuIiwiaW5wdXRfanNvbl9zY2hlbWEiOnsiYWRkaXRpb25hbFByb3BlcnRpZXMiOmZhbHNlLCJ0eXBlIjoib2JqZWN0IiwicHJvcGVydGllcyI6e30sInJlcXVpcmVkIjpbXX0sImFnZW50X25hbWUiOiJGQVEgQWdlbnQiLCJzdHJpY3RfanNvbl9zY2hlbWEiOnRydWV9LHsidG9vbF9uYW1lIjoidHJhbnNmZXJfdG9fc2VhdF9ib29raW5nX2FnZW50IiwidG9vbF9kZXNjcmlwdGlvbiI6IkhhbmRvZmYgdG8gdGhlIFNlYXQgQm9va2luZyBBZ2VudCBhZ2VudCB0byBoYW5kbGUgdGhlIHJlcXVlc3QuIEEgaGVscGZ1bCBhZ2VudCB0aGF0IGNhbiB1cGRhdGUgYSBzZWF0IG9uIGEgZmxpZ2h0LiIsImlucHV0X2pzb25fc2NoZW1hIjp7ImFkZGl0aW9uYWxQcm9wZXJ0aWVzIjpmYWxzZSwidHlwZSI6Im9iamVjdCIsInByb3BlcnRpZXMiOnt9LCJyZXF1aXJlZCI6W119LCJhZ2VudF9uYW1lIjoiU2VhdCBCb29raW5nIEFnZW50Iiwic3RyaWN0X2pzb25fc2NoZW1hIjp0cnVlfV0sInRyYWNpbmciOjEsInByZXZpb3VzX3Jlc3BvbnNlX2lkIjpudWxsLCJwcm9tcHQiOm51bGx9" - } - ] - }, - "scheduleToCloseTimeout": "30s", - "scheduleToStartTimeout": "30s", - "startToCloseTimeout": "10s", - "heartbeatTimeout": "0s", - "workflowTaskCompletedEventId": "18", - "retryPolicy": { - "initialInterval": "1s", - "backoffCoefficient": 2.0, - "maximumInterval": "100s" - }, - "useWorkflowBuildId": true - }, - "userMetadata": { - "summary": { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "IkJvb2sgbWUgYSBmbGlnaHQgdG8gUERYIg==" - } - } - }, - { - "eventId": "21", - "eventTime": "2025-06-30T16:05:23.994909Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_STARTED", - "taskId": "1056563", - "activityTaskStartedEventAttributes": { - "scheduledEventId": "20", - "identity": "16915@Tims-MacBook-Pro.local", - "requestId": "10361ec0-e6e9-4d84-a302-41e06e1c96a4", - "attempt": 1, - "workerVersion": { - "buildId": "01c9746b80ed5a14fc25fb053c5e410a" - } - } - }, - { - "eventId": "22", - "eventTime": "2025-06-30T16:05:24.001754Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_COMPLETED", - "taskId": "1056564", - "activityTaskCompletedEventAttributes": { - "result": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJvdXRwdXQiOlt7ImFyZ3VtZW50cyI6Int9IiwiY2FsbF9pZCI6ImNhbGwiLCJuYW1lIjoidHJhbnNmZXJfdG9fc2VhdF9ib29raW5nX2FnZW50IiwidHlwZSI6ImZ1bmN0aW9uX2NhbGwiLCJpZCI6ImlkIiwic3RhdHVzIjoiY29tcGxldGVkIn1dLCJ1c2FnZSI6eyJyZXF1ZXN0cyI6MCwiaW5wdXRfdG9rZW5zIjowLCJpbnB1dF90b2tlbnNfZGV0YWlscyI6eyJjYWNoZWRfdG9rZW5zIjowfSwib3V0cHV0X3Rva2VucyI6MCwib3V0cHV0X3Rva2Vuc19kZXRhaWxzIjp7InJlYXNvbmluZ190b2tlbnMiOjB9LCJ0b3RhbF90b2tlbnMiOjB9LCJyZXNwb25zZV9pZCI6bnVsbH0=" - } - ] - }, - "scheduledEventId": "20", - "startedEventId": "21", - "identity": "16915@Tims-MacBook-Pro.local" - } - }, - { - "eventId": "23", - "eventTime": "2025-06-30T16:05:24.001757Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", - "taskId": "1056565", - "workflowTaskScheduledEventAttributes": { - "taskQueue": { - "name": "16915@Tims-MacBook-Pro.local-b3f84e19537b4e0a99176a97b19a0c7d", - "kind": "TASK_QUEUE_KIND_STICKY", - "normalName": "be82986c-9ddc-441b-9833-c3fce7c79df1" - }, - "startToCloseTimeout": "10s", - "attempt": 1 - } - }, - { - "eventId": "24", - "eventTime": "2025-06-30T16:05:24.002535Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", - "taskId": "1056569", - "workflowTaskStartedEventAttributes": { - "scheduledEventId": "23", - "identity": "16915@Tims-MacBook-Pro.local", - "requestId": "469bede9-5aa7-4e8b-bd27-cb987cc6808f", - "historySizeBytes": "8515", - "workerVersion": { - "buildId": "01c9746b80ed5a14fc25fb053c5e410a" - } - } - }, - { - "eventId": "25", - "eventTime": "2025-06-30T16:05:24.007974Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", - "taskId": "1056573", - "workflowTaskCompletedEventAttributes": { - "scheduledEventId": "23", - "startedEventId": "24", - "identity": "16915@Tims-MacBook-Pro.local", - "workerVersion": { - "buildId": "01c9746b80ed5a14fc25fb053c5e410a" - }, - "sdkMetadata": {}, - "meteringMetadata": {} - } - }, - { - "eventId": "26", - "eventTime": "2025-06-30T16:05:24.008Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_SCHEDULED", - "taskId": "1056574", - "activityTaskScheduledEventAttributes": { - "activityId": "3", - "activityType": { - "name": "invoke_model_activity" - }, - "taskQueue": { - "name": "be82986c-9ddc-441b-9833-c3fce7c79df1", - "kind": "TASK_QUEUE_KIND_NORMAL" - }, - "header": { - "fields": { - "__openai_span": { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJ0cmFjZU5hbWUiOiJDdXN0b21lciBzZXJ2aWNlIiwic3BhbklkIjoic3Bhbl9iZDhiMDA3NzhmOWE0NjBkYTYxNjc2OTciLCJ0cmFjZUlkIjoidHJhY2VfZGQxMWQyNTYxOWQ1NDZmMGE4MDgxMzc3OTdiYzE2MjMifQ==" - } - } - }, - "input": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJtb2RlbF9uYW1lIjpudWxsLCJzeXN0ZW1faW5zdHJ1Y3Rpb25zIjoiIyBTeXN0ZW0gY29udGV4dFxuWW91IGFyZSBwYXJ0IG9mIGEgbXVsdGktYWdlbnQgc3lzdGVtIGNhbGxlZCB0aGUgQWdlbnRzIFNESywgZGVzaWduZWQgdG8gbWFrZSBhZ2VudCBjb29yZGluYXRpb24gYW5kIGV4ZWN1dGlvbiBlYXN5LiBBZ2VudHMgdXNlcyB0d28gcHJpbWFyeSBhYnN0cmFjdGlvbjogKipBZ2VudHMqKiBhbmQgKipIYW5kb2ZmcyoqLiBBbiBhZ2VudCBlbmNvbXBhc3NlcyBpbnN0cnVjdGlvbnMgYW5kIHRvb2xzIGFuZCBjYW4gaGFuZCBvZmYgYSBjb252ZXJzYXRpb24gdG8gYW5vdGhlciBhZ2VudCB3aGVuIGFwcHJvcHJpYXRlLiBIYW5kb2ZmcyBhcmUgYWNoaWV2ZWQgYnkgY2FsbGluZyBhIGhhbmRvZmYgZnVuY3Rpb24sIGdlbmVyYWxseSBuYW1lZCBgdHJhbnNmZXJfdG9fPGFnZW50X25hbWU+YC4gVHJhbnNmZXJzIGJldHdlZW4gYWdlbnRzIGFyZSBoYW5kbGVkIHNlYW1sZXNzbHkgaW4gdGhlIGJhY2tncm91bmQ7IGRvIG5vdCBtZW50aW9uIG9yIGRyYXcgYXR0ZW50aW9uIHRvIHRoZXNlIHRyYW5zZmVycyBpbiB5b3VyIGNvbnZlcnNhdGlvbiB3aXRoIHRoZSB1c2VyLlxuXG4gICAgICAgIFlvdSBhcmUgYSBzZWF0IGJvb2tpbmcgYWdlbnQuIElmIHlvdSBhcmUgc3BlYWtpbmcgdG8gYSBjdXN0b21lciwgeW91IHByb2JhYmx5IHdlcmUgdHJhbnNmZXJyZWQgdG8gZnJvbSB0aGUgdHJpYWdlIGFnZW50LlxuICAgICAgICBVc2UgdGhlIGZvbGxvd2luZyByb3V0aW5lIHRvIHN1cHBvcnQgdGhlIGN1c3RvbWVyLlxuICAgICAgICAjIFJvdXRpbmVcbiAgICAgICAgMS4gQXNrIGZvciB0aGVpciBjb25maXJtYXRpb24gbnVtYmVyLlxuICAgICAgICAyLiBBc2sgdGhlIGN1c3RvbWVyIHdoYXQgdGhlaXIgZGVzaXJlZCBzZWF0IG51bWJlciBpcy5cbiAgICAgICAgMy4gVXNlIHRoZSB1cGRhdGUgc2VhdCB0b29sIHRvIHVwZGF0ZSB0aGUgc2VhdCBvbiB0aGUgZmxpZ2h0LlxuICAgICAgICBJZiB0aGUgY3VzdG9tZXIgYXNrcyBhIHF1ZXN0aW9uIHRoYXQgaXMgbm90IHJlbGF0ZWQgdG8gdGhlIHJvdXRpbmUsIHRyYW5zZmVyIGJhY2sgdG8gdGhlIHRyaWFnZSBhZ2VudC4gIiwiaW5wdXQiOlt7ImNvbnRlbnQiOiJIZWxsbyIsInJvbGUiOiJ1c2VyIn0seyJpZCI6IiIsImNvbnRlbnQiOlt7ImFubm90YXRpb25zIjpbXSwidGV4dCI6IkhpIHRoZXJlISBIb3cgY2FuIEkgYXNzaXN0IHlvdSB0b2RheT8iLCJ0eXBlIjoib3V0cHV0X3RleHQifV0sInJvbGUiOiJhc3Npc3RhbnQiLCJzdGF0dXMiOiJjb21wbGV0ZWQiLCJ0eXBlIjoibWVzc2FnZSJ9LHsiY29udGVudCI6IkJvb2sgbWUgYSBmbGlnaHQgdG8gUERYIiwicm9sZSI6InVzZXIifSx7ImFyZ3VtZW50cyI6Int9IiwiY2FsbF9pZCI6ImNhbGwiLCJuYW1lIjoidHJhbnNmZXJfdG9fc2VhdF9ib29raW5nX2FnZW50IiwidHlwZSI6ImZ1bmN0aW9uX2NhbGwiLCJpZCI6ImlkIiwic3RhdHVzIjoiY29tcGxldGVkIn0seyJjYWxsX2lkIjoiY2FsbCIsIm91dHB1dCI6IntcImFzc2lzdGFudFwiOiBcIlNlYXQgQm9va2luZyBBZ2VudFwifSIsInR5cGUiOiJmdW5jdGlvbl9jYWxsX291dHB1dCJ9XSwibW9kZWxfc2V0dGluZ3MiOnsidGVtcGVyYXR1cmUiOm51bGwsInRvcF9wIjpudWxsLCJmcmVxdWVuY3lfcGVuYWx0eSI6bnVsbCwicHJlc2VuY2VfcGVuYWx0eSI6bnVsbCwidG9vbF9jaG9pY2UiOm51bGwsInBhcmFsbGVsX3Rvb2xfY2FsbHMiOm51bGwsInRydW5jYXRpb24iOm51bGwsIm1heF90b2tlbnMiOm51bGwsInJlYXNvbmluZyI6bnVsbCwibWV0YWRhdGEiOm51bGwsInN0b3JlIjpudWxsLCJpbmNsdWRlX3VzYWdlIjpudWxsLCJleHRyYV9xdWVyeSI6bnVsbCwiZXh0cmFfYm9keSI6bnVsbCwiZXh0cmFfaGVhZGVycyI6bnVsbCwiZXh0cmFfYXJncyI6bnVsbH0sInRvb2xzIjpbeyJuYW1lIjoidXBkYXRlX3NlYXQiLCJkZXNjcmlwdGlvbiI6IlVwZGF0ZSB0aGUgc2VhdCBmb3IgYSBnaXZlbiBjb25maXJtYXRpb24gbnVtYmVyLiIsInBhcmFtc19qc29uX3NjaGVtYSI6eyJwcm9wZXJ0aWVzIjp7ImNvbmZpcm1hdGlvbl9udW1iZXIiOnsiZGVzY3JpcHRpb24iOiJUaGUgY29uZmlybWF0aW9uIG51bWJlciBmb3IgdGhlIGZsaWdodC4iLCJ0aXRsZSI6IkNvbmZpcm1hdGlvbiBOdW1iZXIiLCJ0eXBlIjoic3RyaW5nIn0sIm5ld19zZWF0Ijp7ImRlc2NyaXB0aW9uIjoiVGhlIG5ldyBzZWF0IHRvIHVwZGF0ZSB0by4iLCJ0aXRsZSI6Ik5ldyBTZWF0IiwidHlwZSI6InN0cmluZyJ9fSwicmVxdWlyZWQiOlsiY29uZmlybWF0aW9uX251bWJlciIsIm5ld19zZWF0Il0sInRpdGxlIjoidXBkYXRlX3NlYXRfYXJncyIsInR5cGUiOiJvYmplY3QiLCJhZGRpdGlvbmFsUHJvcGVydGllcyI6ZmFsc2V9LCJzdHJpY3RfanNvbl9zY2hlbWEiOnRydWV9XSwib3V0cHV0X3NjaGVtYSI6bnVsbCwiaGFuZG9mZnMiOlt7InRvb2xfbmFtZSI6InRyYW5zZmVyX3RvX3RyaWFnZV9hZ2VudCIsInRvb2xfZGVzY3JpcHRpb24iOiJIYW5kb2ZmIHRvIHRoZSBUcmlhZ2UgQWdlbnQgYWdlbnQgdG8gaGFuZGxlIHRoZSByZXF1ZXN0LiBBIHRyaWFnZSBhZ2VudCB0aGF0IGNhbiBkZWxlZ2F0ZSBhIGN1c3RvbWVyJ3MgcmVxdWVzdCB0byB0aGUgYXBwcm9wcmlhdGUgYWdlbnQuIiwiaW5wdXRfanNvbl9zY2hlbWEiOnsiYWRkaXRpb25hbFByb3BlcnRpZXMiOmZhbHNlLCJ0eXBlIjoib2JqZWN0IiwicHJvcGVydGllcyI6e30sInJlcXVpcmVkIjpbXX0sImFnZW50X25hbWUiOiJUcmlhZ2UgQWdlbnQiLCJzdHJpY3RfanNvbl9zY2hlbWEiOnRydWV9XSwidHJhY2luZyI6MSwicHJldmlvdXNfcmVzcG9uc2VfaWQiOm51bGwsInByb21wdCI6bnVsbH0=" - } - ] - }, - "scheduleToCloseTimeout": "30s", - "scheduleToStartTimeout": "30s", - "startToCloseTimeout": "10s", - "heartbeatTimeout": "0s", - "workflowTaskCompletedEventId": "25", - "retryPolicy": { - "initialInterval": "1s", - "backoffCoefficient": 2.0, - "maximumInterval": "100s" - }, - "useWorkflowBuildId": true - } - }, - { - "eventId": "27", - "eventTime": "2025-06-30T16:05:24.008726Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_STARTED", - "taskId": "1056579", - "activityTaskStartedEventAttributes": { - "scheduledEventId": "26", - "identity": "16915@Tims-MacBook-Pro.local", - "requestId": "b483ade7-523e-404a-8b26-34e3f751d9da", - "attempt": 1, - "workerVersion": { - "buildId": "01c9746b80ed5a14fc25fb053c5e410a" - } - } - }, - { - "eventId": "28", - "eventTime": "2025-06-30T16:05:24.013004Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_COMPLETED", - "taskId": "1056580", - "activityTaskCompletedEventAttributes": { - "result": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJvdXRwdXQiOlt7ImlkIjoiIiwiY29udGVudCI6W3siYW5ub3RhdGlvbnMiOltdLCJ0ZXh0IjoiQ291bGQgeW91IHBsZWFzZSBwcm92aWRlIHlvdXIgY29uZmlybWF0aW9uIG51bWJlcj8iLCJ0eXBlIjoib3V0cHV0X3RleHQifV0sInJvbGUiOiJhc3Npc3RhbnQiLCJzdGF0dXMiOiJjb21wbGV0ZWQiLCJ0eXBlIjoibWVzc2FnZSJ9XSwidXNhZ2UiOnsicmVxdWVzdHMiOjAsImlucHV0X3Rva2VucyI6MCwiaW5wdXRfdG9rZW5zX2RldGFpbHMiOnsiY2FjaGVkX3Rva2VucyI6MH0sIm91dHB1dF90b2tlbnMiOjAsIm91dHB1dF90b2tlbnNfZGV0YWlscyI6eyJyZWFzb25pbmdfdG9rZW5zIjowfSwidG90YWxfdG9rZW5zIjowfSwicmVzcG9uc2VfaWQiOm51bGx9" - } - ] - }, - "scheduledEventId": "26", - "startedEventId": "27", - "identity": "16915@Tims-MacBook-Pro.local" - } - }, - { - "eventId": "29", - "eventTime": "2025-06-30T16:05:24.013007Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", - "taskId": "1056581", - "workflowTaskScheduledEventAttributes": { - "taskQueue": { - "name": "16915@Tims-MacBook-Pro.local-b3f84e19537b4e0a99176a97b19a0c7d", - "kind": "TASK_QUEUE_KIND_STICKY", - "normalName": "be82986c-9ddc-441b-9833-c3fce7c79df1" - }, - "startToCloseTimeout": "10s", - "attempt": 1 - } - }, - { - "eventId": "30", - "eventTime": "2025-06-30T16:05:24.013661Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", - "taskId": "1056585", - "workflowTaskStartedEventAttributes": { - "scheduledEventId": "29", - "identity": "16915@Tims-MacBook-Pro.local", - "requestId": "273fc019-be1e-4174-be04-ba910235a319", - "historySizeBytes": "12696", - "workerVersion": { - "buildId": "01c9746b80ed5a14fc25fb053c5e410a" - } - } - }, - { - "eventId": "31", - "eventTime": "2025-06-30T16:05:24.018308Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", - "taskId": "1056589", - "workflowTaskCompletedEventAttributes": { - "scheduledEventId": "29", - "startedEventId": "30", - "identity": "16915@Tims-MacBook-Pro.local", - "workerVersion": { - "buildId": "01c9746b80ed5a14fc25fb053c5e410a" - }, - "sdkMetadata": {}, - "meteringMetadata": {} - } - }, - { - "eventId": "32", - "eventTime": "2025-06-30T16:05:24.018335Z", - "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_UPDATE_COMPLETED", - "taskId": "1056590", - "workflowExecutionUpdateCompletedEventAttributes": { - "meta": { - "updateId": "582a5778-a28c-4a2d-bee1-9629da75c8a5", - "identity": "16915@Tims-MacBook-Pro.local" - }, - "outcome": { - "success": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "WyJVc2VyOiBCb29rIG1lIGEgZmxpZ2h0IHRvIFBEWCIsIlRyaWFnZSBBZ2VudDogU2tpcHBpbmcgaXRlbTogSGFuZG9mZkNhbGxJdGVtIiwiSGFuZGVkIG9mZiBmcm9tIFRyaWFnZSBBZ2VudCB0byBTZWF0IEJvb2tpbmcgQWdlbnQiLCJTZWF0IEJvb2tpbmcgQWdlbnQ6IENvdWxkIHlvdSBwbGVhc2UgcHJvdmlkZSB5b3VyIGNvbmZpcm1hdGlvbiBudW1iZXI/Il0=" - } - ] - } - }, - "acceptedEventId": "19" - } - }, - { - "eventId": "33", - "eventTime": "2025-06-30T16:05:24.020885Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", - "taskId": "1056596", - "workflowTaskScheduledEventAttributes": { - "taskQueue": { - "name": "16915@Tims-MacBook-Pro.local-b3f84e19537b4e0a99176a97b19a0c7d", - "kind": "TASK_QUEUE_KIND_STICKY", - "normalName": "be82986c-9ddc-441b-9833-c3fce7c79df1" - }, - "startToCloseTimeout": "10s", - "attempt": 1 - } - }, - { - "eventId": "34", - "eventTime": "2025-06-30T16:05:24.021110Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", - "taskId": "1056597", - "workflowTaskStartedEventAttributes": { - "scheduledEventId": "33", - "identity": "16915@Tims-MacBook-Pro.local", - "requestId": "d7f1a655-e077-4c3c-8781-5b4a69d39782", - "historySizeBytes": "13272", - "workerVersion": { - "buildId": "01c9746b80ed5a14fc25fb053c5e410a" - } - } - }, - { - "eventId": "35", - "eventTime": "2025-06-30T16:05:24.028049Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", - "taskId": "1056598", - "workflowTaskCompletedEventAttributes": { - "scheduledEventId": "33", - "startedEventId": "34", - "identity": "16915@Tims-MacBook-Pro.local", - "workerVersion": { - "buildId": "01c9746b80ed5a14fc25fb053c5e410a" - }, - "sdkMetadata": {}, - "meteringMetadata": {} - } - }, - { - "eventId": "36", - "eventTime": "2025-06-30T16:05:24.028075Z", - "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_UPDATE_ACCEPTED", - "taskId": "1056599", - "workflowExecutionUpdateAcceptedEventAttributes": { - "protocolInstanceId": "602020f4-2885-4f5b-9e72-e1dfcb2db850", - "acceptedRequestMessageId": "602020f4-2885-4f5b-9e72-e1dfcb2db850/request", - "acceptedRequestSequencingEventId": "33", - "acceptedRequest": { - "meta": { - "updateId": "602020f4-2885-4f5b-9e72-e1dfcb2db850", - "identity": "16915@Tims-MacBook-Pro.local" - }, - "input": { - "name": "process_user_message", - "args": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJ1c2VyX2lucHV0IjoiMTExMTEiLCJjaGF0X2xlbmd0aCI6Nn0=" - } - ] - } - } - } - } - }, - { - "eventId": "37", - "eventTime": "2025-06-30T16:05:24.028089Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_SCHEDULED", - "taskId": "1056600", - "activityTaskScheduledEventAttributes": { - "activityId": "4", - "activityType": { - "name": "invoke_model_activity" - }, - "taskQueue": { - "name": "be82986c-9ddc-441b-9833-c3fce7c79df1", - "kind": "TASK_QUEUE_KIND_NORMAL" - }, - "header": { - "fields": { - "__openai_span": { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJ0cmFjZU5hbWUiOiJDdXN0b21lciBzZXJ2aWNlIiwic3BhbklkIjoic3Bhbl84YTYzOGIwY2ZkYzE0MjA5YWMyNTUwNGEiLCJ0cmFjZUlkIjoidHJhY2VfNjg3MTkwZDNhMDM2NGNlYWExYTlhYzhhYzNmMDgwOTkifQ==" - } - } - }, - "input": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJtb2RlbF9uYW1lIjpudWxsLCJzeXN0ZW1faW5zdHJ1Y3Rpb25zIjoiIyBTeXN0ZW0gY29udGV4dFxuWW91IGFyZSBwYXJ0IG9mIGEgbXVsdGktYWdlbnQgc3lzdGVtIGNhbGxlZCB0aGUgQWdlbnRzIFNESywgZGVzaWduZWQgdG8gbWFrZSBhZ2VudCBjb29yZGluYXRpb24gYW5kIGV4ZWN1dGlvbiBlYXN5LiBBZ2VudHMgdXNlcyB0d28gcHJpbWFyeSBhYnN0cmFjdGlvbjogKipBZ2VudHMqKiBhbmQgKipIYW5kb2ZmcyoqLiBBbiBhZ2VudCBlbmNvbXBhc3NlcyBpbnN0cnVjdGlvbnMgYW5kIHRvb2xzIGFuZCBjYW4gaGFuZCBvZmYgYSBjb252ZXJzYXRpb24gdG8gYW5vdGhlciBhZ2VudCB3aGVuIGFwcHJvcHJpYXRlLiBIYW5kb2ZmcyBhcmUgYWNoaWV2ZWQgYnkgY2FsbGluZyBhIGhhbmRvZmYgZnVuY3Rpb24sIGdlbmVyYWxseSBuYW1lZCBgdHJhbnNmZXJfdG9fPGFnZW50X25hbWU+YC4gVHJhbnNmZXJzIGJldHdlZW4gYWdlbnRzIGFyZSBoYW5kbGVkIHNlYW1sZXNzbHkgaW4gdGhlIGJhY2tncm91bmQ7IGRvIG5vdCBtZW50aW9uIG9yIGRyYXcgYXR0ZW50aW9uIHRvIHRoZXNlIHRyYW5zZmVycyBpbiB5b3VyIGNvbnZlcnNhdGlvbiB3aXRoIHRoZSB1c2VyLlxuXG4gICAgICAgIFlvdSBhcmUgYSBzZWF0IGJvb2tpbmcgYWdlbnQuIElmIHlvdSBhcmUgc3BlYWtpbmcgdG8gYSBjdXN0b21lciwgeW91IHByb2JhYmx5IHdlcmUgdHJhbnNmZXJyZWQgdG8gZnJvbSB0aGUgdHJpYWdlIGFnZW50LlxuICAgICAgICBVc2UgdGhlIGZvbGxvd2luZyByb3V0aW5lIHRvIHN1cHBvcnQgdGhlIGN1c3RvbWVyLlxuICAgICAgICAjIFJvdXRpbmVcbiAgICAgICAgMS4gQXNrIGZvciB0aGVpciBjb25maXJtYXRpb24gbnVtYmVyLlxuICAgICAgICAyLiBBc2sgdGhlIGN1c3RvbWVyIHdoYXQgdGhlaXIgZGVzaXJlZCBzZWF0IG51bWJlciBpcy5cbiAgICAgICAgMy4gVXNlIHRoZSB1cGRhdGUgc2VhdCB0b29sIHRvIHVwZGF0ZSB0aGUgc2VhdCBvbiB0aGUgZmxpZ2h0LlxuICAgICAgICBJZiB0aGUgY3VzdG9tZXIgYXNrcyBhIHF1ZXN0aW9uIHRoYXQgaXMgbm90IHJlbGF0ZWQgdG8gdGhlIHJvdXRpbmUsIHRyYW5zZmVyIGJhY2sgdG8gdGhlIHRyaWFnZSBhZ2VudC4gIiwiaW5wdXQiOlt7ImNvbnRlbnQiOiJIZWxsbyIsInJvbGUiOiJ1c2VyIn0seyJpZCI6IiIsImNvbnRlbnQiOlt7ImFubm90YXRpb25zIjpbXSwidGV4dCI6IkhpIHRoZXJlISBIb3cgY2FuIEkgYXNzaXN0IHlvdSB0b2RheT8iLCJ0eXBlIjoib3V0cHV0X3RleHQifV0sInJvbGUiOiJhc3Npc3RhbnQiLCJzdGF0dXMiOiJjb21wbGV0ZWQiLCJ0eXBlIjoibWVzc2FnZSJ9LHsiY29udGVudCI6IkJvb2sgbWUgYSBmbGlnaHQgdG8gUERYIiwicm9sZSI6InVzZXIifSx7ImFyZ3VtZW50cyI6Int9IiwiY2FsbF9pZCI6ImNhbGwiLCJuYW1lIjoidHJhbnNmZXJfdG9fc2VhdF9ib29raW5nX2FnZW50IiwidHlwZSI6ImZ1bmN0aW9uX2NhbGwiLCJpZCI6ImlkIiwic3RhdHVzIjoiY29tcGxldGVkIn0seyJjYWxsX2lkIjoiY2FsbCIsIm91dHB1dCI6IntcImFzc2lzdGFudFwiOiBcIlNlYXQgQm9va2luZyBBZ2VudFwifSIsInR5cGUiOiJmdW5jdGlvbl9jYWxsX291dHB1dCJ9LHsiaWQiOiIiLCJjb250ZW50IjpbeyJhbm5vdGF0aW9ucyI6W10sInRleHQiOiJDb3VsZCB5b3UgcGxlYXNlIHByb3ZpZGUgeW91ciBjb25maXJtYXRpb24gbnVtYmVyPyIsInR5cGUiOiJvdXRwdXRfdGV4dCJ9XSwicm9sZSI6ImFzc2lzdGFudCIsInN0YXR1cyI6ImNvbXBsZXRlZCIsInR5cGUiOiJtZXNzYWdlIn0seyJjb250ZW50IjoiMTExMTEiLCJyb2xlIjoidXNlciJ9XSwibW9kZWxfc2V0dGluZ3MiOnsidGVtcGVyYXR1cmUiOm51bGwsInRvcF9wIjpudWxsLCJmcmVxdWVuY3lfcGVuYWx0eSI6bnVsbCwicHJlc2VuY2VfcGVuYWx0eSI6bnVsbCwidG9vbF9jaG9pY2UiOm51bGwsInBhcmFsbGVsX3Rvb2xfY2FsbHMiOm51bGwsInRydW5jYXRpb24iOm51bGwsIm1heF90b2tlbnMiOm51bGwsInJlYXNvbmluZyI6bnVsbCwibWV0YWRhdGEiOm51bGwsInN0b3JlIjpudWxsLCJpbmNsdWRlX3VzYWdlIjpudWxsLCJleHRyYV9xdWVyeSI6bnVsbCwiZXh0cmFfYm9keSI6bnVsbCwiZXh0cmFfaGVhZGVycyI6bnVsbCwiZXh0cmFfYXJncyI6bnVsbH0sInRvb2xzIjpbeyJuYW1lIjoidXBkYXRlX3NlYXQiLCJkZXNjcmlwdGlvbiI6IlVwZGF0ZSB0aGUgc2VhdCBmb3IgYSBnaXZlbiBjb25maXJtYXRpb24gbnVtYmVyLiIsInBhcmFtc19qc29uX3NjaGVtYSI6eyJwcm9wZXJ0aWVzIjp7ImNvbmZpcm1hdGlvbl9udW1iZXIiOnsiZGVzY3JpcHRpb24iOiJUaGUgY29uZmlybWF0aW9uIG51bWJlciBmb3IgdGhlIGZsaWdodC4iLCJ0aXRsZSI6IkNvbmZpcm1hdGlvbiBOdW1iZXIiLCJ0eXBlIjoic3RyaW5nIn0sIm5ld19zZWF0Ijp7ImRlc2NyaXB0aW9uIjoiVGhlIG5ldyBzZWF0IHRvIHVwZGF0ZSB0by4iLCJ0aXRsZSI6Ik5ldyBTZWF0IiwidHlwZSI6InN0cmluZyJ9fSwicmVxdWlyZWQiOlsiY29uZmlybWF0aW9uX251bWJlciIsIm5ld19zZWF0Il0sInRpdGxlIjoidXBkYXRlX3NlYXRfYXJncyIsInR5cGUiOiJvYmplY3QiLCJhZGRpdGlvbmFsUHJvcGVydGllcyI6ZmFsc2V9LCJzdHJpY3RfanNvbl9zY2hlbWEiOnRydWV9XSwib3V0cHV0X3NjaGVtYSI6bnVsbCwiaGFuZG9mZnMiOlt7InRvb2xfbmFtZSI6InRyYW5zZmVyX3RvX3RyaWFnZV9hZ2VudCIsInRvb2xfZGVzY3JpcHRpb24iOiJIYW5kb2ZmIHRvIHRoZSBUcmlhZ2UgQWdlbnQgYWdlbnQgdG8gaGFuZGxlIHRoZSByZXF1ZXN0LiBBIHRyaWFnZSBhZ2VudCB0aGF0IGNhbiBkZWxlZ2F0ZSBhIGN1c3RvbWVyJ3MgcmVxdWVzdCB0byB0aGUgYXBwcm9wcmlhdGUgYWdlbnQuIiwiaW5wdXRfanNvbl9zY2hlbWEiOnsiYWRkaXRpb25hbFByb3BlcnRpZXMiOmZhbHNlLCJ0eXBlIjoib2JqZWN0IiwicHJvcGVydGllcyI6e30sInJlcXVpcmVkIjpbXX0sImFnZW50X25hbWUiOiJUcmlhZ2UgQWdlbnQiLCJzdHJpY3RfanNvbl9zY2hlbWEiOnRydWV9XSwidHJhY2luZyI6MSwicHJldmlvdXNfcmVzcG9uc2VfaWQiOm51bGwsInByb21wdCI6bnVsbH0=" - } - ] - }, - "scheduleToCloseTimeout": "30s", - "scheduleToStartTimeout": "30s", - "startToCloseTimeout": "10s", - "heartbeatTimeout": "0s", - "workflowTaskCompletedEventId": "35", - "retryPolicy": { - "initialInterval": "1s", - "backoffCoefficient": 2.0, - "maximumInterval": "100s" - }, - "useWorkflowBuildId": true - }, - "userMetadata": { - "summary": { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "IjExMTExIg==" - } - } - }, - { - "eventId": "38", - "eventTime": "2025-06-30T16:05:24.028711Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_STARTED", - "taskId": "1056606", - "activityTaskStartedEventAttributes": { - "scheduledEventId": "37", - "identity": "16915@Tims-MacBook-Pro.local", - "requestId": "c867017e-9504-4840-b8cb-278f9e06c9f0", - "attempt": 1, - "workerVersion": { - "buildId": "01c9746b80ed5a14fc25fb053c5e410a" - } - } - }, - { - "eventId": "39", - "eventTime": "2025-06-30T16:05:24.032775Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_COMPLETED", - "taskId": "1056607", - "activityTaskCompletedEventAttributes": { - "result": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJvdXRwdXQiOlt7ImlkIjoiIiwiY29udGVudCI6W3siYW5ub3RhdGlvbnMiOltdLCJ0ZXh0IjoiVGhhbmtzISBXaGF0IHNlYXQgbnVtYmVyIHdvdWxkIHlvdSBsaWtlIHRvIGNoYW5nZSB0bz8iLCJ0eXBlIjoib3V0cHV0X3RleHQifV0sInJvbGUiOiJhc3Npc3RhbnQiLCJzdGF0dXMiOiJjb21wbGV0ZWQiLCJ0eXBlIjoibWVzc2FnZSJ9XSwidXNhZ2UiOnsicmVxdWVzdHMiOjAsImlucHV0X3Rva2VucyI6MCwiaW5wdXRfdG9rZW5zX2RldGFpbHMiOnsiY2FjaGVkX3Rva2VucyI6MH0sIm91dHB1dF90b2tlbnMiOjAsIm91dHB1dF90b2tlbnNfZGV0YWlscyI6eyJyZWFzb25pbmdfdG9rZW5zIjowfSwidG90YWxfdG9rZW5zIjowfSwicmVzcG9uc2VfaWQiOm51bGx9" - } - ] - }, - "scheduledEventId": "37", - "startedEventId": "38", - "identity": "16915@Tims-MacBook-Pro.local" - } - }, - { - "eventId": "40", - "eventTime": "2025-06-30T16:05:24.032778Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", - "taskId": "1056608", - "workflowTaskScheduledEventAttributes": { - "taskQueue": { - "name": "16915@Tims-MacBook-Pro.local-b3f84e19537b4e0a99176a97b19a0c7d", - "kind": "TASK_QUEUE_KIND_STICKY", - "normalName": "be82986c-9ddc-441b-9833-c3fce7c79df1" - }, - "startToCloseTimeout": "10s", - "attempt": 1 - } - }, - { - "eventId": "41", - "eventTime": "2025-06-30T16:05:24.033341Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", - "taskId": "1056612", - "workflowTaskStartedEventAttributes": { - "scheduledEventId": "40", - "identity": "16915@Tims-MacBook-Pro.local", - "requestId": "cb909484-7de0-4129-9173-eb67c1e3bf01", - "historySizeBytes": "18123", - "workerVersion": { - "buildId": "01c9746b80ed5a14fc25fb053c5e410a" - } - } - }, - { - "eventId": "42", - "eventTime": "2025-06-30T16:05:24.037741Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", - "taskId": "1056616", - "workflowTaskCompletedEventAttributes": { - "scheduledEventId": "40", - "startedEventId": "41", - "identity": "16915@Tims-MacBook-Pro.local", - "workerVersion": { - "buildId": "01c9746b80ed5a14fc25fb053c5e410a" - }, - "sdkMetadata": {}, - "meteringMetadata": {} - } - }, - { - "eventId": "43", - "eventTime": "2025-06-30T16:05:24.037767Z", - "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_UPDATE_COMPLETED", - "taskId": "1056617", - "workflowExecutionUpdateCompletedEventAttributes": { - "meta": { - "updateId": "602020f4-2885-4f5b-9e72-e1dfcb2db850", - "identity": "16915@Tims-MacBook-Pro.local" - }, - "outcome": { - "success": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "WyJVc2VyOiAxMTExMSIsIlNlYXQgQm9va2luZyBBZ2VudDogVGhhbmtzISBXaGF0IHNlYXQgbnVtYmVyIHdvdWxkIHlvdSBsaWtlIHRvIGNoYW5nZSB0bz8iXQ==" - } - ] - } - }, - "acceptedEventId": "36" - } - }, - { - "eventId": "44", - "eventTime": "2025-06-30T16:05:24.040731Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", - "taskId": "1056623", - "workflowTaskScheduledEventAttributes": { - "taskQueue": { - "name": "16915@Tims-MacBook-Pro.local-b3f84e19537b4e0a99176a97b19a0c7d", - "kind": "TASK_QUEUE_KIND_STICKY", - "normalName": "be82986c-9ddc-441b-9833-c3fce7c79df1" - }, - "startToCloseTimeout": "10s", - "attempt": 1 - } - }, - { - "eventId": "45", - "eventTime": "2025-06-30T16:05:24.040950Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", - "taskId": "1056624", - "workflowTaskStartedEventAttributes": { - "scheduledEventId": "44", - "identity": "16915@Tims-MacBook-Pro.local", - "requestId": "58b7c276-20e4-4db6-baab-12599793bd59", - "historySizeBytes": "18581", - "workerVersion": { - "buildId": "01c9746b80ed5a14fc25fb053c5e410a" - } - } - }, - { - "eventId": "46", - "eventTime": "2025-06-30T16:05:24.048105Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", - "taskId": "1056625", - "workflowTaskCompletedEventAttributes": { - "scheduledEventId": "44", - "startedEventId": "45", - "identity": "16915@Tims-MacBook-Pro.local", - "workerVersion": { - "buildId": "01c9746b80ed5a14fc25fb053c5e410a" - }, - "sdkMetadata": {}, - "meteringMetadata": {} - } - }, - { - "eventId": "47", - "eventTime": "2025-06-30T16:05:24.048131Z", - "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_UPDATE_ACCEPTED", - "taskId": "1056626", - "workflowExecutionUpdateAcceptedEventAttributes": { - "protocolInstanceId": "221628d3-083b-4982-aa48-635abf7e8435", - "acceptedRequestMessageId": "221628d3-083b-4982-aa48-635abf7e8435/request", - "acceptedRequestSequencingEventId": "44", - "acceptedRequest": { - "meta": { - "updateId": "221628d3-083b-4982-aa48-635abf7e8435", - "identity": "16915@Tims-MacBook-Pro.local" - }, - "input": { - "name": "process_user_message", - "args": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJ1c2VyX2lucHV0IjoiQW55IHdpbmRvdyBzZWF0IiwiY2hhdF9sZW5ndGgiOjh9" - } - ] - } - } - } - } - }, - { - "eventId": "48", - "eventTime": "2025-06-30T16:05:24.048150Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_SCHEDULED", - "taskId": "1056627", - "activityTaskScheduledEventAttributes": { - "activityId": "5", - "activityType": { - "name": "invoke_model_activity" - }, - "taskQueue": { - "name": "be82986c-9ddc-441b-9833-c3fce7c79df1", - "kind": "TASK_QUEUE_KIND_NORMAL" - }, - "header": { - "fields": { - "__openai_span": { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJ0cmFjZU5hbWUiOiJDdXN0b21lciBzZXJ2aWNlIiwic3BhbklkIjoic3Bhbl9hNzQ4NTg4ZDQwNDQ0YjRkYTRhOTk4ZjYiLCJ0cmFjZUlkIjoidHJhY2VfMDhhZTY0ZGUyNzQ3NGVkY2IyMWZiYTk4ZjkyYjEyYTEifQ==" - } - } - }, - "input": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJtb2RlbF9uYW1lIjpudWxsLCJzeXN0ZW1faW5zdHJ1Y3Rpb25zIjoiIyBTeXN0ZW0gY29udGV4dFxuWW91IGFyZSBwYXJ0IG9mIGEgbXVsdGktYWdlbnQgc3lzdGVtIGNhbGxlZCB0aGUgQWdlbnRzIFNESywgZGVzaWduZWQgdG8gbWFrZSBhZ2VudCBjb29yZGluYXRpb24gYW5kIGV4ZWN1dGlvbiBlYXN5LiBBZ2VudHMgdXNlcyB0d28gcHJpbWFyeSBhYnN0cmFjdGlvbjogKipBZ2VudHMqKiBhbmQgKipIYW5kb2ZmcyoqLiBBbiBhZ2VudCBlbmNvbXBhc3NlcyBpbnN0cnVjdGlvbnMgYW5kIHRvb2xzIGFuZCBjYW4gaGFuZCBvZmYgYSBjb252ZXJzYXRpb24gdG8gYW5vdGhlciBhZ2VudCB3aGVuIGFwcHJvcHJpYXRlLiBIYW5kb2ZmcyBhcmUgYWNoaWV2ZWQgYnkgY2FsbGluZyBhIGhhbmRvZmYgZnVuY3Rpb24sIGdlbmVyYWxseSBuYW1lZCBgdHJhbnNmZXJfdG9fPGFnZW50X25hbWU+YC4gVHJhbnNmZXJzIGJldHdlZW4gYWdlbnRzIGFyZSBoYW5kbGVkIHNlYW1sZXNzbHkgaW4gdGhlIGJhY2tncm91bmQ7IGRvIG5vdCBtZW50aW9uIG9yIGRyYXcgYXR0ZW50aW9uIHRvIHRoZXNlIHRyYW5zZmVycyBpbiB5b3VyIGNvbnZlcnNhdGlvbiB3aXRoIHRoZSB1c2VyLlxuXG4gICAgICAgIFlvdSBhcmUgYSBzZWF0IGJvb2tpbmcgYWdlbnQuIElmIHlvdSBhcmUgc3BlYWtpbmcgdG8gYSBjdXN0b21lciwgeW91IHByb2JhYmx5IHdlcmUgdHJhbnNmZXJyZWQgdG8gZnJvbSB0aGUgdHJpYWdlIGFnZW50LlxuICAgICAgICBVc2UgdGhlIGZvbGxvd2luZyByb3V0aW5lIHRvIHN1cHBvcnQgdGhlIGN1c3RvbWVyLlxuICAgICAgICAjIFJvdXRpbmVcbiAgICAgICAgMS4gQXNrIGZvciB0aGVpciBjb25maXJtYXRpb24gbnVtYmVyLlxuICAgICAgICAyLiBBc2sgdGhlIGN1c3RvbWVyIHdoYXQgdGhlaXIgZGVzaXJlZCBzZWF0IG51bWJlciBpcy5cbiAgICAgICAgMy4gVXNlIHRoZSB1cGRhdGUgc2VhdCB0b29sIHRvIHVwZGF0ZSB0aGUgc2VhdCBvbiB0aGUgZmxpZ2h0LlxuICAgICAgICBJZiB0aGUgY3VzdG9tZXIgYXNrcyBhIHF1ZXN0aW9uIHRoYXQgaXMgbm90IHJlbGF0ZWQgdG8gdGhlIHJvdXRpbmUsIHRyYW5zZmVyIGJhY2sgdG8gdGhlIHRyaWFnZSBhZ2VudC4gIiwiaW5wdXQiOlt7ImNvbnRlbnQiOiJIZWxsbyIsInJvbGUiOiJ1c2VyIn0seyJpZCI6IiIsImNvbnRlbnQiOlt7ImFubm90YXRpb25zIjpbXSwidGV4dCI6IkhpIHRoZXJlISBIb3cgY2FuIEkgYXNzaXN0IHlvdSB0b2RheT8iLCJ0eXBlIjoib3V0cHV0X3RleHQifV0sInJvbGUiOiJhc3Npc3RhbnQiLCJzdGF0dXMiOiJjb21wbGV0ZWQiLCJ0eXBlIjoibWVzc2FnZSJ9LHsiY29udGVudCI6IkJvb2sgbWUgYSBmbGlnaHQgdG8gUERYIiwicm9sZSI6InVzZXIifSx7ImFyZ3VtZW50cyI6Int9IiwiY2FsbF9pZCI6ImNhbGwiLCJuYW1lIjoidHJhbnNmZXJfdG9fc2VhdF9ib29raW5nX2FnZW50IiwidHlwZSI6ImZ1bmN0aW9uX2NhbGwiLCJpZCI6ImlkIiwic3RhdHVzIjoiY29tcGxldGVkIn0seyJjYWxsX2lkIjoiY2FsbCIsIm91dHB1dCI6IntcImFzc2lzdGFudFwiOiBcIlNlYXQgQm9va2luZyBBZ2VudFwifSIsInR5cGUiOiJmdW5jdGlvbl9jYWxsX291dHB1dCJ9LHsiaWQiOiIiLCJjb250ZW50IjpbeyJhbm5vdGF0aW9ucyI6W10sInRleHQiOiJDb3VsZCB5b3UgcGxlYXNlIHByb3ZpZGUgeW91ciBjb25maXJtYXRpb24gbnVtYmVyPyIsInR5cGUiOiJvdXRwdXRfdGV4dCJ9XSwicm9sZSI6ImFzc2lzdGFudCIsInN0YXR1cyI6ImNvbXBsZXRlZCIsInR5cGUiOiJtZXNzYWdlIn0seyJjb250ZW50IjoiMTExMTEiLCJyb2xlIjoidXNlciJ9LHsiaWQiOiIiLCJjb250ZW50IjpbeyJhbm5vdGF0aW9ucyI6W10sInRleHQiOiJUaGFua3MhIFdoYXQgc2VhdCBudW1iZXIgd291bGQgeW91IGxpa2UgdG8gY2hhbmdlIHRvPyIsInR5cGUiOiJvdXRwdXRfdGV4dCJ9XSwicm9sZSI6ImFzc2lzdGFudCIsInN0YXR1cyI6ImNvbXBsZXRlZCIsInR5cGUiOiJtZXNzYWdlIn0seyJjb250ZW50IjoiQW55IHdpbmRvdyBzZWF0Iiwicm9sZSI6InVzZXIifV0sIm1vZGVsX3NldHRpbmdzIjp7InRlbXBlcmF0dXJlIjpudWxsLCJ0b3BfcCI6bnVsbCwiZnJlcXVlbmN5X3BlbmFsdHkiOm51bGwsInByZXNlbmNlX3BlbmFsdHkiOm51bGwsInRvb2xfY2hvaWNlIjpudWxsLCJwYXJhbGxlbF90b29sX2NhbGxzIjpudWxsLCJ0cnVuY2F0aW9uIjpudWxsLCJtYXhfdG9rZW5zIjpudWxsLCJyZWFzb25pbmciOm51bGwsIm1ldGFkYXRhIjpudWxsLCJzdG9yZSI6bnVsbCwiaW5jbHVkZV91c2FnZSI6bnVsbCwiZXh0cmFfcXVlcnkiOm51bGwsImV4dHJhX2JvZHkiOm51bGwsImV4dHJhX2hlYWRlcnMiOm51bGwsImV4dHJhX2FyZ3MiOm51bGx9LCJ0b29scyI6W3sibmFtZSI6InVwZGF0ZV9zZWF0IiwiZGVzY3JpcHRpb24iOiJVcGRhdGUgdGhlIHNlYXQgZm9yIGEgZ2l2ZW4gY29uZmlybWF0aW9uIG51bWJlci4iLCJwYXJhbXNfanNvbl9zY2hlbWEiOnsicHJvcGVydGllcyI6eyJjb25maXJtYXRpb25fbnVtYmVyIjp7ImRlc2NyaXB0aW9uIjoiVGhlIGNvbmZpcm1hdGlvbiBudW1iZXIgZm9yIHRoZSBmbGlnaHQuIiwidGl0bGUiOiJDb25maXJtYXRpb24gTnVtYmVyIiwidHlwZSI6InN0cmluZyJ9LCJuZXdfc2VhdCI6eyJkZXNjcmlwdGlvbiI6IlRoZSBuZXcgc2VhdCB0byB1cGRhdGUgdG8uIiwidGl0bGUiOiJOZXcgU2VhdCIsInR5cGUiOiJzdHJpbmcifX0sInJlcXVpcmVkIjpbImNvbmZpcm1hdGlvbl9udW1iZXIiLCJuZXdfc2VhdCJdLCJ0aXRsZSI6InVwZGF0ZV9zZWF0X2FyZ3MiLCJ0eXBlIjoib2JqZWN0IiwiYWRkaXRpb25hbFByb3BlcnRpZXMiOmZhbHNlfSwic3RyaWN0X2pzb25fc2NoZW1hIjp0cnVlfV0sIm91dHB1dF9zY2hlbWEiOm51bGwsImhhbmRvZmZzIjpbeyJ0b29sX25hbWUiOiJ0cmFuc2Zlcl90b190cmlhZ2VfYWdlbnQiLCJ0b29sX2Rlc2NyaXB0aW9uIjoiSGFuZG9mZiB0byB0aGUgVHJpYWdlIEFnZW50IGFnZW50IHRvIGhhbmRsZSB0aGUgcmVxdWVzdC4gQSB0cmlhZ2UgYWdlbnQgdGhhdCBjYW4gZGVsZWdhdGUgYSBjdXN0b21lcidzIHJlcXVlc3QgdG8gdGhlIGFwcHJvcHJpYXRlIGFnZW50LiIsImlucHV0X2pzb25fc2NoZW1hIjp7ImFkZGl0aW9uYWxQcm9wZXJ0aWVzIjpmYWxzZSwidHlwZSI6Im9iamVjdCIsInByb3BlcnRpZXMiOnt9LCJyZXF1aXJlZCI6W119LCJhZ2VudF9uYW1lIjoiVHJpYWdlIEFnZW50Iiwic3RyaWN0X2pzb25fc2NoZW1hIjp0cnVlfV0sInRyYWNpbmciOjEsInByZXZpb3VzX3Jlc3BvbnNlX2lkIjpudWxsLCJwcm9tcHQiOm51bGx9" - } - ] - }, - "scheduleToCloseTimeout": "30s", - "scheduleToStartTimeout": "30s", - "startToCloseTimeout": "10s", - "heartbeatTimeout": "0s", - "workflowTaskCompletedEventId": "46", - "retryPolicy": { - "initialInterval": "1s", - "backoffCoefficient": 2.0, - "maximumInterval": "100s" - }, - "useWorkflowBuildId": true - }, - "userMetadata": { - "summary": { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "IkFueSB3aW5kb3cgc2VhdCI=" - } - } - }, - { - "eventId": "49", - "eventTime": "2025-06-30T16:05:24.048892Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_STARTED", - "taskId": "1056633", - "activityTaskStartedEventAttributes": { - "scheduledEventId": "48", - "identity": "16915@Tims-MacBook-Pro.local", - "requestId": "75475818-bd91-4146-862d-a9cbcdc042af", - "attempt": 1, - "workerVersion": { - "buildId": "01c9746b80ed5a14fc25fb053c5e410a" - } - } - }, - { - "eventId": "50", - "eventTime": "2025-06-30T16:05:24.053217Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_COMPLETED", - "taskId": "1056634", - "activityTaskCompletedEventAttributes": { - "result": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJvdXRwdXQiOlt7ImFyZ3VtZW50cyI6IntcImNvbmZpcm1hdGlvbl9udW1iZXJcIjpcIjExMTExXCIsXCJuZXdfc2VhdFwiOlwid2luZG93IHNlYXRcIn0iLCJjYWxsX2lkIjoiY2FsbCIsIm5hbWUiOiJ1cGRhdGVfc2VhdCIsInR5cGUiOiJmdW5jdGlvbl9jYWxsIiwiaWQiOiJpZCIsInN0YXR1cyI6ImNvbXBsZXRlZCJ9XSwidXNhZ2UiOnsicmVxdWVzdHMiOjAsImlucHV0X3Rva2VucyI6MCwiaW5wdXRfdG9rZW5zX2RldGFpbHMiOnsiY2FjaGVkX3Rva2VucyI6MH0sIm91dHB1dF90b2tlbnMiOjAsIm91dHB1dF90b2tlbnNfZGV0YWlscyI6eyJyZWFzb25pbmdfdG9rZW5zIjowfSwidG90YWxfdG9rZW5zIjowfSwicmVzcG9uc2VfaWQiOm51bGx9" - } - ] - }, - "scheduledEventId": "48", - "startedEventId": "49", - "identity": "16915@Tims-MacBook-Pro.local" - } - }, - { - "eventId": "51", - "eventTime": "2025-06-30T16:05:24.053220Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", - "taskId": "1056635", - "workflowTaskScheduledEventAttributes": { - "taskQueue": { - "name": "16915@Tims-MacBook-Pro.local-b3f84e19537b4e0a99176a97b19a0c7d", - "kind": "TASK_QUEUE_KIND_STICKY", - "normalName": "be82986c-9ddc-441b-9833-c3fce7c79df1" - }, - "startToCloseTimeout": "10s", - "attempt": 1 - } - }, - { - "eventId": "52", - "eventTime": "2025-06-30T16:05:24.053885Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", - "taskId": "1056639", - "workflowTaskStartedEventAttributes": { - "scheduledEventId": "51", - "identity": "16915@Tims-MacBook-Pro.local", - "requestId": "993c75fa-56bf-49cc-bd1c-ed7b8998b6d0", - "historySizeBytes": "23670", - "workerVersion": { - "buildId": "01c9746b80ed5a14fc25fb053c5e410a" - } - } - }, - { - "eventId": "53", - "eventTime": "2025-06-30T16:05:24.058743Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", - "taskId": "1056643", - "workflowTaskCompletedEventAttributes": { - "scheduledEventId": "51", - "startedEventId": "52", - "identity": "16915@Tims-MacBook-Pro.local", - "workerVersion": { - "buildId": "01c9746b80ed5a14fc25fb053c5e410a" - }, - "sdkMetadata": {}, - "meteringMetadata": {} - } - }, - { - "eventId": "54", - "eventTime": "2025-06-30T16:05:24.058762Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_SCHEDULED", - "taskId": "1056644", - "activityTaskScheduledEventAttributes": { - "activityId": "6", - "activityType": { - "name": "invoke_model_activity" - }, - "taskQueue": { - "name": "be82986c-9ddc-441b-9833-c3fce7c79df1", - "kind": "TASK_QUEUE_KIND_NORMAL" - }, - "header": { - "fields": { - "__openai_span": { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJ0cmFjZU5hbWUiOiJDdXN0b21lciBzZXJ2aWNlIiwic3BhbklkIjoic3Bhbl9hZjJkZWQwZDNkNTA0NWYzYWY0NmY1YzMiLCJ0cmFjZUlkIjoidHJhY2VfMDhhZTY0ZGUyNzQ3NGVkY2IyMWZiYTk4ZjkyYjEyYTEifQ==" - } - } - }, - "input": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJtb2RlbF9uYW1lIjpudWxsLCJzeXN0ZW1faW5zdHJ1Y3Rpb25zIjoiIyBTeXN0ZW0gY29udGV4dFxuWW91IGFyZSBwYXJ0IG9mIGEgbXVsdGktYWdlbnQgc3lzdGVtIGNhbGxlZCB0aGUgQWdlbnRzIFNESywgZGVzaWduZWQgdG8gbWFrZSBhZ2VudCBjb29yZGluYXRpb24gYW5kIGV4ZWN1dGlvbiBlYXN5LiBBZ2VudHMgdXNlcyB0d28gcHJpbWFyeSBhYnN0cmFjdGlvbjogKipBZ2VudHMqKiBhbmQgKipIYW5kb2ZmcyoqLiBBbiBhZ2VudCBlbmNvbXBhc3NlcyBpbnN0cnVjdGlvbnMgYW5kIHRvb2xzIGFuZCBjYW4gaGFuZCBvZmYgYSBjb252ZXJzYXRpb24gdG8gYW5vdGhlciBhZ2VudCB3aGVuIGFwcHJvcHJpYXRlLiBIYW5kb2ZmcyBhcmUgYWNoaWV2ZWQgYnkgY2FsbGluZyBhIGhhbmRvZmYgZnVuY3Rpb24sIGdlbmVyYWxseSBuYW1lZCBgdHJhbnNmZXJfdG9fPGFnZW50X25hbWU+YC4gVHJhbnNmZXJzIGJldHdlZW4gYWdlbnRzIGFyZSBoYW5kbGVkIHNlYW1sZXNzbHkgaW4gdGhlIGJhY2tncm91bmQ7IGRvIG5vdCBtZW50aW9uIG9yIGRyYXcgYXR0ZW50aW9uIHRvIHRoZXNlIHRyYW5zZmVycyBpbiB5b3VyIGNvbnZlcnNhdGlvbiB3aXRoIHRoZSB1c2VyLlxuXG4gICAgICAgIFlvdSBhcmUgYSBzZWF0IGJvb2tpbmcgYWdlbnQuIElmIHlvdSBhcmUgc3BlYWtpbmcgdG8gYSBjdXN0b21lciwgeW91IHByb2JhYmx5IHdlcmUgdHJhbnNmZXJyZWQgdG8gZnJvbSB0aGUgdHJpYWdlIGFnZW50LlxuICAgICAgICBVc2UgdGhlIGZvbGxvd2luZyByb3V0aW5lIHRvIHN1cHBvcnQgdGhlIGN1c3RvbWVyLlxuICAgICAgICAjIFJvdXRpbmVcbiAgICAgICAgMS4gQXNrIGZvciB0aGVpciBjb25maXJtYXRpb24gbnVtYmVyLlxuICAgICAgICAyLiBBc2sgdGhlIGN1c3RvbWVyIHdoYXQgdGhlaXIgZGVzaXJlZCBzZWF0IG51bWJlciBpcy5cbiAgICAgICAgMy4gVXNlIHRoZSB1cGRhdGUgc2VhdCB0b29sIHRvIHVwZGF0ZSB0aGUgc2VhdCBvbiB0aGUgZmxpZ2h0LlxuICAgICAgICBJZiB0aGUgY3VzdG9tZXIgYXNrcyBhIHF1ZXN0aW9uIHRoYXQgaXMgbm90IHJlbGF0ZWQgdG8gdGhlIHJvdXRpbmUsIHRyYW5zZmVyIGJhY2sgdG8gdGhlIHRyaWFnZSBhZ2VudC4gIiwiaW5wdXQiOlt7ImNvbnRlbnQiOiJIZWxsbyIsInJvbGUiOiJ1c2VyIn0seyJpZCI6IiIsImNvbnRlbnQiOlt7ImFubm90YXRpb25zIjpbXSwidGV4dCI6IkhpIHRoZXJlISBIb3cgY2FuIEkgYXNzaXN0IHlvdSB0b2RheT8iLCJ0eXBlIjoib3V0cHV0X3RleHQifV0sInJvbGUiOiJhc3Npc3RhbnQiLCJzdGF0dXMiOiJjb21wbGV0ZWQiLCJ0eXBlIjoibWVzc2FnZSJ9LHsiY29udGVudCI6IkJvb2sgbWUgYSBmbGlnaHQgdG8gUERYIiwicm9sZSI6InVzZXIifSx7ImFyZ3VtZW50cyI6Int9IiwiY2FsbF9pZCI6ImNhbGwiLCJuYW1lIjoidHJhbnNmZXJfdG9fc2VhdF9ib29raW5nX2FnZW50IiwidHlwZSI6ImZ1bmN0aW9uX2NhbGwiLCJpZCI6ImlkIiwic3RhdHVzIjoiY29tcGxldGVkIn0seyJjYWxsX2lkIjoiY2FsbCIsIm91dHB1dCI6IntcImFzc2lzdGFudFwiOiBcIlNlYXQgQm9va2luZyBBZ2VudFwifSIsInR5cGUiOiJmdW5jdGlvbl9jYWxsX291dHB1dCJ9LHsiaWQiOiIiLCJjb250ZW50IjpbeyJhbm5vdGF0aW9ucyI6W10sInRleHQiOiJDb3VsZCB5b3UgcGxlYXNlIHByb3ZpZGUgeW91ciBjb25maXJtYXRpb24gbnVtYmVyPyIsInR5cGUiOiJvdXRwdXRfdGV4dCJ9XSwicm9sZSI6ImFzc2lzdGFudCIsInN0YXR1cyI6ImNvbXBsZXRlZCIsInR5cGUiOiJtZXNzYWdlIn0seyJjb250ZW50IjoiMTExMTEiLCJyb2xlIjoidXNlciJ9LHsiaWQiOiIiLCJjb250ZW50IjpbeyJhbm5vdGF0aW9ucyI6W10sInRleHQiOiJUaGFua3MhIFdoYXQgc2VhdCBudW1iZXIgd291bGQgeW91IGxpa2UgdG8gY2hhbmdlIHRvPyIsInR5cGUiOiJvdXRwdXRfdGV4dCJ9XSwicm9sZSI6ImFzc2lzdGFudCIsInN0YXR1cyI6ImNvbXBsZXRlZCIsInR5cGUiOiJtZXNzYWdlIn0seyJjb250ZW50IjoiQW55IHdpbmRvdyBzZWF0Iiwicm9sZSI6InVzZXIifSx7ImFyZ3VtZW50cyI6IntcImNvbmZpcm1hdGlvbl9udW1iZXJcIjpcIjExMTExXCIsXCJuZXdfc2VhdFwiOlwid2luZG93IHNlYXRcIn0iLCJjYWxsX2lkIjoiY2FsbCIsIm5hbWUiOiJ1cGRhdGVfc2VhdCIsInR5cGUiOiJmdW5jdGlvbl9jYWxsIiwiaWQiOiJpZCIsInN0YXR1cyI6ImNvbXBsZXRlZCJ9LHsiY2FsbF9pZCI6ImNhbGwiLCJvdXRwdXQiOiJVcGRhdGVkIHNlYXQgdG8gd2luZG93IHNlYXQgZm9yIGNvbmZpcm1hdGlvbiBudW1iZXIgMTExMTEiLCJ0eXBlIjoiZnVuY3Rpb25fY2FsbF9vdXRwdXQifV0sIm1vZGVsX3NldHRpbmdzIjp7InRlbXBlcmF0dXJlIjpudWxsLCJ0b3BfcCI6bnVsbCwiZnJlcXVlbmN5X3BlbmFsdHkiOm51bGwsInByZXNlbmNlX3BlbmFsdHkiOm51bGwsInRvb2xfY2hvaWNlIjpudWxsLCJwYXJhbGxlbF90b29sX2NhbGxzIjpudWxsLCJ0cnVuY2F0aW9uIjpudWxsLCJtYXhfdG9rZW5zIjpudWxsLCJyZWFzb25pbmciOm51bGwsIm1ldGFkYXRhIjpudWxsLCJzdG9yZSI6bnVsbCwiaW5jbHVkZV91c2FnZSI6bnVsbCwiZXh0cmFfcXVlcnkiOm51bGwsImV4dHJhX2JvZHkiOm51bGwsImV4dHJhX2hlYWRlcnMiOm51bGwsImV4dHJhX2FyZ3MiOm51bGx9LCJ0b29scyI6W3sibmFtZSI6InVwZGF0ZV9zZWF0IiwiZGVzY3JpcHRpb24iOiJVcGRhdGUgdGhlIHNlYXQgZm9yIGEgZ2l2ZW4gY29uZmlybWF0aW9uIG51bWJlci4iLCJwYXJhbXNfanNvbl9zY2hlbWEiOnsicHJvcGVydGllcyI6eyJjb25maXJtYXRpb25fbnVtYmVyIjp7ImRlc2NyaXB0aW9uIjoiVGhlIGNvbmZpcm1hdGlvbiBudW1iZXIgZm9yIHRoZSBmbGlnaHQuIiwidGl0bGUiOiJDb25maXJtYXRpb24gTnVtYmVyIiwidHlwZSI6InN0cmluZyJ9LCJuZXdfc2VhdCI6eyJkZXNjcmlwdGlvbiI6IlRoZSBuZXcgc2VhdCB0byB1cGRhdGUgdG8uIiwidGl0bGUiOiJOZXcgU2VhdCIsInR5cGUiOiJzdHJpbmcifX0sInJlcXVpcmVkIjpbImNvbmZpcm1hdGlvbl9udW1iZXIiLCJuZXdfc2VhdCJdLCJ0aXRsZSI6InVwZGF0ZV9zZWF0X2FyZ3MiLCJ0eXBlIjoib2JqZWN0IiwiYWRkaXRpb25hbFByb3BlcnRpZXMiOmZhbHNlfSwic3RyaWN0X2pzb25fc2NoZW1hIjp0cnVlfV0sIm91dHB1dF9zY2hlbWEiOm51bGwsImhhbmRvZmZzIjpbeyJ0b29sX25hbWUiOiJ0cmFuc2Zlcl90b190cmlhZ2VfYWdlbnQiLCJ0b29sX2Rlc2NyaXB0aW9uIjoiSGFuZG9mZiB0byB0aGUgVHJpYWdlIEFnZW50IGFnZW50IHRvIGhhbmRsZSB0aGUgcmVxdWVzdC4gQSB0cmlhZ2UgYWdlbnQgdGhhdCBjYW4gZGVsZWdhdGUgYSBjdXN0b21lcidzIHJlcXVlc3QgdG8gdGhlIGFwcHJvcHJpYXRlIGFnZW50LiIsImlucHV0X2pzb25fc2NoZW1hIjp7ImFkZGl0aW9uYWxQcm9wZXJ0aWVzIjpmYWxzZSwidHlwZSI6Im9iamVjdCIsInByb3BlcnRpZXMiOnt9LCJyZXF1aXJlZCI6W119LCJhZ2VudF9uYW1lIjoiVHJpYWdlIEFnZW50Iiwic3RyaWN0X2pzb25fc2NoZW1hIjp0cnVlfV0sInRyYWNpbmciOjEsInByZXZpb3VzX3Jlc3BvbnNlX2lkIjpudWxsLCJwcm9tcHQiOm51bGx9" - } - ] - }, - "scheduleToCloseTimeout": "30s", - "scheduleToStartTimeout": "30s", - "startToCloseTimeout": "10s", - "heartbeatTimeout": "0s", - "workflowTaskCompletedEventId": "53", - "retryPolicy": { - "initialInterval": "1s", - "backoffCoefficient": 2.0, - "maximumInterval": "100s" - }, - "useWorkflowBuildId": true - } - }, - { - "eventId": "55", - "eventTime": "2025-06-30T16:05:24.059426Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_STARTED", - "taskId": "1056649", - "activityTaskStartedEventAttributes": { - "scheduledEventId": "54", - "identity": "16915@Tims-MacBook-Pro.local", - "requestId": "b69cc310-3113-4c70-9ca1-a68126ef053d", - "attempt": 1, - "workerVersion": { - "buildId": "01c9746b80ed5a14fc25fb053c5e410a" - } - } - }, - { - "eventId": "56", - "eventTime": "2025-06-30T16:05:24.063888Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_COMPLETED", - "taskId": "1056650", - "activityTaskCompletedEventAttributes": { - "result": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJvdXRwdXQiOlt7ImlkIjoiIiwiY29udGVudCI6W3siYW5ub3RhdGlvbnMiOltdLCJ0ZXh0IjoiWW91ciBzZWF0IGhhcyBiZWVuIHVwZGF0ZWQgdG8gYSB3aW5kb3cgc2VhdC4gSWYgdGhlcmUncyBhbnl0aGluZyBlbHNlIHlvdSBuZWVkLCBmZWVsIGZyZWUgdG8gbGV0IG1lIGtub3chIiwidHlwZSI6Im91dHB1dF90ZXh0In1dLCJyb2xlIjoiYXNzaXN0YW50Iiwic3RhdHVzIjoiY29tcGxldGVkIiwidHlwZSI6Im1lc3NhZ2UifV0sInVzYWdlIjp7InJlcXVlc3RzIjowLCJpbnB1dF90b2tlbnMiOjAsImlucHV0X3Rva2Vuc19kZXRhaWxzIjp7ImNhY2hlZF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5zIjowLCJvdXRwdXRfdG9rZW5zX2RldGFpbHMiOnsicmVhc29uaW5nX3Rva2VucyI6MH0sInRvdGFsX3Rva2VucyI6MH0sInJlc3BvbnNlX2lkIjpudWxsfQ==" - } - ] - }, - "scheduledEventId": "54", - "startedEventId": "55", - "identity": "16915@Tims-MacBook-Pro.local" - } - }, - { - "eventId": "57", - "eventTime": "2025-06-30T16:05:24.063891Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", - "taskId": "1056651", - "workflowTaskScheduledEventAttributes": { - "taskQueue": { - "name": "16915@Tims-MacBook-Pro.local-b3f84e19537b4e0a99176a97b19a0c7d", - "kind": "TASK_QUEUE_KIND_STICKY", - "normalName": "be82986c-9ddc-441b-9833-c3fce7c79df1" - }, - "startToCloseTimeout": "10s", - "attempt": 1 - } - }, - { - "eventId": "58", - "eventTime": "2025-06-30T16:05:24.064527Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", - "taskId": "1056655", - "workflowTaskStartedEventAttributes": { - "scheduledEventId": "57", - "identity": "16915@Tims-MacBook-Pro.local", - "requestId": "225bc0a5-9c60-4007-a024-b4949bdeb592", - "historySizeBytes": "28637", - "workerVersion": { - "buildId": "01c9746b80ed5a14fc25fb053c5e410a" - } - } - }, - { - "eventId": "59", - "eventTime": "2025-06-30T16:05:24.068715Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", - "taskId": "1056659", - "workflowTaskCompletedEventAttributes": { - "scheduledEventId": "57", - "startedEventId": "58", - "identity": "16915@Tims-MacBook-Pro.local", - "workerVersion": { - "buildId": "01c9746b80ed5a14fc25fb053c5e410a" - }, - "sdkMetadata": {}, - "meteringMetadata": {} - } - }, - { - "eventId": "60", - "eventTime": "2025-06-30T16:05:24.068738Z", - "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_UPDATE_COMPLETED", - "taskId": "1056660", - "workflowExecutionUpdateCompletedEventAttributes": { - "meta": { - "updateId": "221628d3-083b-4982-aa48-635abf7e8435", - "identity": "16915@Tims-MacBook-Pro.local" - }, - "outcome": { - "success": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "WyJVc2VyOiBBbnkgd2luZG93IHNlYXQiLCJTZWF0IEJvb2tpbmcgQWdlbnQ6IENhbGxpbmcgYSB0b29sIiwiU2VhdCBCb29raW5nIEFnZW50OiBUb29sIGNhbGwgb3V0cHV0OiBVcGRhdGVkIHNlYXQgdG8gd2luZG93IHNlYXQgZm9yIGNvbmZpcm1hdGlvbiBudW1iZXIgMTExMTEiLCJTZWF0IEJvb2tpbmcgQWdlbnQ6IFlvdXIgc2VhdCBoYXMgYmVlbiB1cGRhdGVkIHRvIGEgd2luZG93IHNlYXQuIElmIHRoZXJlJ3MgYW55dGhpbmcgZWxzZSB5b3UgbmVlZCwgZmVlbCBmcmVlIHRvIGxldCBtZSBrbm93ISJd" - } - ] - } - }, - "acceptedEventId": "47" - } - }, - { - "eventId": "61", - "eventTime": "2025-06-30T16:05:24.072283Z", - "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_CANCEL_REQUESTED", - "taskId": "1056662", - "workflowExecutionCancelRequestedEventAttributes": { - "identity": "16915@Tims-MacBook-Pro.local" - } - }, - { - "eventId": "62", - "eventTime": "2025-06-30T16:05:24.072284Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", - "taskId": "1056663", - "workflowTaskScheduledEventAttributes": { - "taskQueue": { - "name": "16915@Tims-MacBook-Pro.local-b3f84e19537b4e0a99176a97b19a0c7d", - "kind": "TASK_QUEUE_KIND_STICKY", - "normalName": "be82986c-9ddc-441b-9833-c3fce7c79df1" - }, - "startToCloseTimeout": "10s", - "attempt": 1 - } - }, - { - "eventId": "63", - "eventTime": "2025-06-30T16:05:24.072701Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", - "taskId": "1056667", - "workflowTaskStartedEventAttributes": { - "scheduledEventId": "62", - "identity": "16915@Tims-MacBook-Pro.local", - "requestId": "9ab4a693-ed84-4ba7-87cd-88e836962760", - "historySizeBytes": "29489", - "workerVersion": { - "buildId": "01c9746b80ed5a14fc25fb053c5e410a" - } - } - }, - { - "eventId": "64", - "eventTime": "2025-06-30T16:05:24.078668Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", - "taskId": "1056671", - "workflowTaskCompletedEventAttributes": { - "scheduledEventId": "62", - "startedEventId": "63", - "identity": "16915@Tims-MacBook-Pro.local", - "workerVersion": { - "buildId": "01c9746b80ed5a14fc25fb053c5e410a" - }, - "sdkMetadata": {}, - "meteringMetadata": {} - } - }, - { - "eventId": "65", - "eventTime": "2025-06-30T16:05:24.078680Z", - "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_CANCELED", - "taskId": "1056672", - "workflowExecutionCanceledEventAttributes": { - "workflowTaskCompletedEventId": "64" - } - } - ] -} \ No newline at end of file diff --git a/tests/contrib/openai_agents/histories/hello-workflow-history.json b/tests/contrib/openai_agents/histories/hello-workflow-history.json deleted file mode 100644 index 9532fc831..000000000 --- a/tests/contrib/openai_agents/histories/hello-workflow-history.json +++ /dev/null @@ -1,239 +0,0 @@ -{ - "events": [ - { - "eventId": "1", - "eventTime": "2025-06-30T16:01:07.681673Z", - "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_STARTED", - "taskId": "1056333", - "workflowExecutionStartedEventAttributes": { - "workflowType": { - "name": "HelloWorldAgent" - }, - "taskQueue": { - "name": "5dd7a6ba-834c-4206-bcbc-a02599f1b54c", - "kind": "TASK_QUEUE_KIND_NORMAL" - }, - "input": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "IlRlbGwgbWUgYWJvdXQgcmVjdXJzaW9uIGluIHByb2dyYW1taW5nLiI=" - } - ] - }, - "workflowExecutionTimeout": "5s", - "workflowRunTimeout": "5s", - "workflowTaskTimeout": "5s", - "originalExecutionRunId": "0197c192-1c61-7a41-8852-56960bd15070", - "identity": "16699@Tims-MacBook-Pro.local", - "firstExecutionRunId": "0197c192-1c61-7a41-8852-56960bd15070", - "attempt": 1, - "workflowExecutionExpirationTime": "2025-06-30T16:01:12.681Z", - "firstWorkflowTaskBackoff": "0s", - "workflowId": "hello-workflow-d6d3eaae-b159-424b-aa88-e643e42a35d8" - } - }, - { - "eventId": "2", - "eventTime": "2025-06-30T16:01:07.681730Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", - "taskId": "1056334", - "workflowTaskScheduledEventAttributes": { - "taskQueue": { - "name": "5dd7a6ba-834c-4206-bcbc-a02599f1b54c", - "kind": "TASK_QUEUE_KIND_NORMAL" - }, - "startToCloseTimeout": "5s", - "attempt": 1 - } - }, - { - "eventId": "3", - "eventTime": "2025-06-30T16:01:07.683559Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", - "taskId": "1056340", - "workflowTaskStartedEventAttributes": { - "scheduledEventId": "2", - "identity": "16699@Tims-MacBook-Pro.local", - "requestId": "6c233fea-425f-46e0-9ab9-0ffd22c957e7", - "historySizeBytes": "428", - "workerVersion": { - "buildId": "01c9746b80ed5a14fc25fb053c5e410a" - } - } - }, - { - "eventId": "4", - "eventTime": "2025-06-30T16:01:07.726660Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", - "taskId": "1056344", - "workflowTaskCompletedEventAttributes": { - "scheduledEventId": "2", - "startedEventId": "3", - "identity": "16699@Tims-MacBook-Pro.local", - "workerVersion": { - "buildId": "01c9746b80ed5a14fc25fb053c5e410a" - }, - "sdkMetadata": { - "coreUsedFlags": [ - 3, - 1, - 2 - ], - "sdkName": "temporal-python", - "sdkVersion": "1.13.0" - }, - "meteringMetadata": {} - } - }, - { - "eventId": "5", - "eventTime": "2025-06-30T16:01:07.726700Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_SCHEDULED", - "taskId": "1056345", - "activityTaskScheduledEventAttributes": { - "activityId": "1", - "activityType": { - "name": "invoke_model_activity" - }, - "taskQueue": { - "name": "5dd7a6ba-834c-4206-bcbc-a02599f1b54c", - "kind": "TASK_QUEUE_KIND_NORMAL" - }, - "header": {}, - "input": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJtb2RlbF9uYW1lIjpudWxsLCJzeXN0ZW1faW5zdHJ1Y3Rpb25zIjoiWW91IG9ubHkgcmVzcG9uZCBpbiBoYWlrdXMuIiwiaW5wdXQiOlt7ImNvbnRlbnQiOiJUZWxsIG1lIGFib3V0IHJlY3Vyc2lvbiBpbiBwcm9ncmFtbWluZy4iLCJyb2xlIjoidXNlciJ9XSwibW9kZWxfc2V0dGluZ3MiOnsidGVtcGVyYXR1cmUiOm51bGwsInRvcF9wIjpudWxsLCJmcmVxdWVuY3lfcGVuYWx0eSI6bnVsbCwicHJlc2VuY2VfcGVuYWx0eSI6bnVsbCwidG9vbF9jaG9pY2UiOm51bGwsInBhcmFsbGVsX3Rvb2xfY2FsbHMiOm51bGwsInRydW5jYXRpb24iOm51bGwsIm1heF90b2tlbnMiOm51bGwsInJlYXNvbmluZyI6bnVsbCwibWV0YWRhdGEiOm51bGwsInN0b3JlIjpudWxsLCJpbmNsdWRlX3VzYWdlIjpudWxsLCJleHRyYV9xdWVyeSI6bnVsbCwiZXh0cmFfYm9keSI6bnVsbCwiZXh0cmFfaGVhZGVycyI6bnVsbCwiZXh0cmFfYXJncyI6bnVsbH0sInRvb2xzIjpbXSwib3V0cHV0X3NjaGVtYSI6bnVsbCwiaGFuZG9mZnMiOltdLCJ0cmFjaW5nIjoxLCJwcmV2aW91c19yZXNwb25zZV9pZCI6bnVsbCwicHJvbXB0IjpudWxsfQ==" - } - ] - }, - "scheduleToCloseTimeout": "5s", - "scheduleToStartTimeout": "5s", - "startToCloseTimeout": "5s", - "heartbeatTimeout": "0s", - "workflowTaskCompletedEventId": "4", - "retryPolicy": { - "initialInterval": "1s", - "backoffCoefficient": 2.0, - "maximumInterval": "100s" - }, - "useWorkflowBuildId": true - }, - "userMetadata": { - "summary": { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "IlRlbGwgbWUgYWJvdXQgcmVjdXJzaW9uIGluIHByb2dyYW1taW5nLiI=" - } - } - }, - { - "eventId": "6", - "eventTime": "2025-06-30T16:01:07.727544Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_STARTED", - "taskId": "1056350", - "activityTaskStartedEventAttributes": { - "scheduledEventId": "5", - "identity": "16699@Tims-MacBook-Pro.local", - "requestId": "009b2a1e-ebe8-456a-8791-5d66a75d45cc", - "attempt": 1, - "workerVersion": { - "buildId": "01c9746b80ed5a14fc25fb053c5e410a" - } - } - }, - { - "eventId": "7", - "eventTime": "2025-06-30T16:01:07.744810Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_COMPLETED", - "taskId": "1056351", - "activityTaskCompletedEventAttributes": { - "result": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJvdXRwdXQiOlt7ImlkIjoiIiwiY29udGVudCI6W3siYW5ub3RhdGlvbnMiOltdLCJ0ZXh0IjoidGVzdCIsInR5cGUiOiJvdXRwdXRfdGV4dCJ9XSwicm9sZSI6ImFzc2lzdGFudCIsInN0YXR1cyI6ImNvbXBsZXRlZCIsInR5cGUiOiJtZXNzYWdlIn1dLCJ1c2FnZSI6eyJyZXF1ZXN0cyI6MCwiaW5wdXRfdG9rZW5zIjowLCJpbnB1dF90b2tlbnNfZGV0YWlscyI6eyJjYWNoZWRfdG9rZW5zIjowfSwib3V0cHV0X3Rva2VucyI6MCwib3V0cHV0X3Rva2Vuc19kZXRhaWxzIjp7InJlYXNvbmluZ190b2tlbnMiOjB9LCJ0b3RhbF90b2tlbnMiOjB9LCJyZXNwb25zZV9pZCI6bnVsbH0=" - } - ] - }, - "scheduledEventId": "5", - "startedEventId": "6", - "identity": "16699@Tims-MacBook-Pro.local" - } - }, - { - "eventId": "8", - "eventTime": "2025-06-30T16:01:07.744814Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", - "taskId": "1056352", - "workflowTaskScheduledEventAttributes": { - "taskQueue": { - "name": "16699@Tims-MacBook-Pro.local-a72d517b7a12431a9fd4070796dab626", - "kind": "TASK_QUEUE_KIND_STICKY", - "normalName": "5dd7a6ba-834c-4206-bcbc-a02599f1b54c" - }, - "startToCloseTimeout": "5s", - "attempt": 1 - } - }, - { - "eventId": "9", - "eventTime": "2025-06-30T16:01:07.745636Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", - "taskId": "1056356", - "workflowTaskStartedEventAttributes": { - "scheduledEventId": "8", - "identity": "16699@Tims-MacBook-Pro.local", - "requestId": "d5cac3b9-b484-43e9-97ce-89dd5f587c07", - "historySizeBytes": "2210", - "workerVersion": { - "buildId": "01c9746b80ed5a14fc25fb053c5e410a" - } - } - }, - { - "eventId": "10", - "eventTime": "2025-06-30T16:01:07.778965Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", - "taskId": "1056360", - "workflowTaskCompletedEventAttributes": { - "scheduledEventId": "8", - "startedEventId": "9", - "identity": "16699@Tims-MacBook-Pro.local", - "workerVersion": { - "buildId": "01c9746b80ed5a14fc25fb053c5e410a" - }, - "sdkMetadata": {}, - "meteringMetadata": {} - } - }, - { - "eventId": "11", - "eventTime": "2025-06-30T16:01:07.778982Z", - "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_COMPLETED", - "taskId": "1056361", - "workflowExecutionCompletedEventAttributes": { - "result": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "InRlc3Qi" - } - ] - }, - "workflowTaskCompletedEventId": "10" - } - } - ] -} \ No newline at end of file diff --git a/tests/contrib/openai_agents/histories/input-guardrail-workflow-history.json b/tests/contrib/openai_agents/histories/input-guardrail-workflow-history.json deleted file mode 100644 index 2054b1750..000000000 --- a/tests/contrib/openai_agents/histories/input-guardrail-workflow-history.json +++ /dev/null @@ -1,577 +0,0 @@ -{ - "events": [ - { - "eventId": "1", - "eventTime": "2025-06-30T16:59:17.497431Z", - "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_STARTED", - "taskId": "1056913", - "workflowExecutionStartedEventAttributes": { - "workflowType": { - "name": "InputGuardrailWorkflow" - }, - "taskQueue": { - "name": "e488932b-c66d-48b6-97cb-7b8feaa1c41c", - "kind": "TASK_QUEUE_KIND_NORMAL" - }, - "input": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "WyJXaGF0J3MgdGhlIGNhcGl0YWwgb2YgQ2FsaWZvcm5pYT8iLCJDYW4geW91IGhlbHAgbWUgc29sdmUgZm9yIHg6IDJ4ICsgNSA9IDExIl0=" - } - ] - }, - "workflowExecutionTimeout": "10s", - "workflowRunTimeout": "10s", - "workflowTaskTimeout": "10s", - "originalExecutionRunId": "0197c1c7-5c79-768f-914e-2e1b2dfe6e9b", - "identity": "19010@Tims-MacBook-Pro.local", - "firstExecutionRunId": "0197c1c7-5c79-768f-914e-2e1b2dfe6e9b", - "attempt": 1, - "workflowExecutionExpirationTime": "2025-06-30T16:59:27.497Z", - "firstWorkflowTaskBackoff": "0s", - "workflowId": "input-guardrail-4544d2e3-6e97-4fbe-9675-6a6e93d58a18" - } - }, - { - "eventId": "2", - "eventTime": "2025-06-30T16:59:17.497457Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", - "taskId": "1056914", - "workflowTaskScheduledEventAttributes": { - "taskQueue": { - "name": "e488932b-c66d-48b6-97cb-7b8feaa1c41c", - "kind": "TASK_QUEUE_KIND_NORMAL" - }, - "startToCloseTimeout": "10s", - "attempt": 1 - } - }, - { - "eventId": "3", - "eventTime": "2025-06-30T16:59:17.499098Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", - "taskId": "1056920", - "workflowTaskStartedEventAttributes": { - "scheduledEventId": "2", - "identity": "19010@Tims-MacBook-Pro.local", - "requestId": "7fdcced4-cbc0-4089-92e2-ae9e6a9467ed", - "historySizeBytes": "475", - "workerVersion": { - "buildId": "9f32b5d9789ece25bfb8515a7023ec78" - } - } - }, - { - "eventId": "4", - "eventTime": "2025-06-30T16:59:17.539360Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", - "taskId": "1056924", - "workflowTaskCompletedEventAttributes": { - "scheduledEventId": "2", - "startedEventId": "3", - "identity": "19010@Tims-MacBook-Pro.local", - "workerVersion": { - "buildId": "9f32b5d9789ece25bfb8515a7023ec78" - }, - "sdkMetadata": { - "coreUsedFlags": [ - 2, - 3, - 1 - ], - "sdkName": "temporal-python", - "sdkVersion": "1.13.0" - }, - "meteringMetadata": {} - } - }, - { - "eventId": "5", - "eventTime": "2025-06-30T16:59:17.539386Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_SCHEDULED", - "taskId": "1056925", - "activityTaskScheduledEventAttributes": { - "activityId": "1", - "activityType": { - "name": "invoke_model_activity" - }, - "taskQueue": { - "name": "e488932b-c66d-48b6-97cb-7b8feaa1c41c", - "kind": "TASK_QUEUE_KIND_NORMAL" - }, - "header": { - "fields": { - "__openai_span": { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJ0cmFjZU5hbWUiOiJBZ2VudCB3b3JrZmxvdyIsInNwYW5JZCI6InNwYW5fMDdlMjAxZmNkNjIxNDBjNWE0NTZkMzA0IiwidHJhY2VJZCI6InRyYWNlXzY1MzMyOGZjZWQ4MTQ4NmM4MDkyYzIzMjNjYzk2NTE0In0=" - } - } - }, - "input": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJtb2RlbF9uYW1lIjpudWxsLCJzeXN0ZW1faW5zdHJ1Y3Rpb25zIjoiWW91IGFyZSBhIGN1c3RvbWVyIHN1cHBvcnQgYWdlbnQuIFlvdSBoZWxwIGN1c3RvbWVycyB3aXRoIHRoZWlyIHF1ZXN0aW9ucy4iLCJpbnB1dCI6W3sicm9sZSI6InVzZXIiLCJjb250ZW50IjoiV2hhdCdzIHRoZSBjYXBpdGFsIG9mIENhbGlmb3JuaWE/In1dLCJtb2RlbF9zZXR0aW5ncyI6eyJ0ZW1wZXJhdHVyZSI6bnVsbCwidG9wX3AiOm51bGwsImZyZXF1ZW5jeV9wZW5hbHR5IjpudWxsLCJwcmVzZW5jZV9wZW5hbHR5IjpudWxsLCJ0b29sX2Nob2ljZSI6bnVsbCwicGFyYWxsZWxfdG9vbF9jYWxscyI6bnVsbCwidHJ1bmNhdGlvbiI6bnVsbCwibWF4X3Rva2VucyI6bnVsbCwicmVhc29uaW5nIjpudWxsLCJtZXRhZGF0YSI6bnVsbCwic3RvcmUiOm51bGwsImluY2x1ZGVfdXNhZ2UiOm51bGwsImV4dHJhX3F1ZXJ5IjpudWxsLCJleHRyYV9ib2R5IjpudWxsLCJleHRyYV9oZWFkZXJzIjpudWxsLCJleHRyYV9hcmdzIjpudWxsfSwidG9vbHMiOltdLCJvdXRwdXRfc2NoZW1hIjpudWxsLCJoYW5kb2ZmcyI6W10sInRyYWNpbmciOjEsInByZXZpb3VzX3Jlc3BvbnNlX2lkIjpudWxsLCJwcm9tcHQiOm51bGx9" - } - ] - }, - "scheduleToCloseTimeout": "10s", - "scheduleToStartTimeout": "10s", - "startToCloseTimeout": "10s", - "heartbeatTimeout": "0s", - "workflowTaskCompletedEventId": "4", - "retryPolicy": { - "initialInterval": "1s", - "backoffCoefficient": 2.0, - "maximumInterval": "100s" - }, - "useWorkflowBuildId": true - }, - "userMetadata": { - "summary": { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "IldoYXQncyB0aGUgY2FwaXRhbCBvZiBDYWxpZm9ybmlhPyI=" - } - } - }, - { - "eventId": "6", - "eventTime": "2025-06-30T16:59:17.539400Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_SCHEDULED", - "taskId": "1056926", - "activityTaskScheduledEventAttributes": { - "activityId": "2", - "activityType": { - "name": "invoke_model_activity" - }, - "taskQueue": { - "name": "e488932b-c66d-48b6-97cb-7b8feaa1c41c", - "kind": "TASK_QUEUE_KIND_NORMAL" - }, - "header": { - "fields": { - "__openai_span": { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJ0cmFjZU5hbWUiOiJBZ2VudCB3b3JrZmxvdyIsInNwYW5JZCI6InNwYW5fMTFkYjBhZWEyOTZjNGJkZGFjNWEzZjVkIiwidHJhY2VJZCI6InRyYWNlXzY1MzMyOGZjZWQ4MTQ4NmM4MDkyYzIzMjNjYzk2NTE0In0=" - } - } - }, - "input": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJtb2RlbF9uYW1lIjpudWxsLCJzeXN0ZW1faW5zdHJ1Y3Rpb25zIjoiQ2hlY2sgaWYgdGhlIHVzZXIgaXMgYXNraW5nIHlvdSB0byBkbyB0aGVpciBtYXRoIGhvbWV3b3JrLiIsImlucHV0IjpbeyJyb2xlIjoidXNlciIsImNvbnRlbnQiOiJXaGF0J3MgdGhlIGNhcGl0YWwgb2YgQ2FsaWZvcm5pYT8ifV0sIm1vZGVsX3NldHRpbmdzIjp7InRlbXBlcmF0dXJlIjpudWxsLCJ0b3BfcCI6bnVsbCwiZnJlcXVlbmN5X3BlbmFsdHkiOm51bGwsInByZXNlbmNlX3BlbmFsdHkiOm51bGwsInRvb2xfY2hvaWNlIjpudWxsLCJwYXJhbGxlbF90b29sX2NhbGxzIjpudWxsLCJ0cnVuY2F0aW9uIjpudWxsLCJtYXhfdG9rZW5zIjpudWxsLCJyZWFzb25pbmciOm51bGwsIm1ldGFkYXRhIjpudWxsLCJzdG9yZSI6bnVsbCwiaW5jbHVkZV91c2FnZSI6bnVsbCwiZXh0cmFfcXVlcnkiOm51bGwsImV4dHJhX2JvZHkiOm51bGwsImV4dHJhX2hlYWRlcnMiOm51bGwsImV4dHJhX2FyZ3MiOm51bGx9LCJ0b29scyI6W10sIm91dHB1dF9zY2hlbWEiOnsib3V0cHV0X3R5cGVfbmFtZSI6Ik1hdGhIb21ld29ya091dHB1dCIsImlzX3dyYXBwZWQiOmZhbHNlLCJvdXRwdXRfc2NoZW1hIjp7ImFkZGl0aW9uYWxQcm9wZXJ0aWVzIjpmYWxzZSwicHJvcGVydGllcyI6eyJyZWFzb25pbmciOnsidGl0bGUiOiJSZWFzb25pbmciLCJ0eXBlIjoic3RyaW5nIn0sImlzX21hdGhfaG9tZXdvcmsiOnsidGl0bGUiOiJJcyBNYXRoIEhvbWV3b3JrIiwidHlwZSI6ImJvb2xlYW4ifX0sInJlcXVpcmVkIjpbInJlYXNvbmluZyIsImlzX21hdGhfaG9tZXdvcmsiXSwidGl0bGUiOiJNYXRoSG9tZXdvcmtPdXRwdXQiLCJ0eXBlIjoib2JqZWN0In0sInN0cmljdF9qc29uX3NjaGVtYSI6dHJ1ZX0sImhhbmRvZmZzIjpbXSwidHJhY2luZyI6MSwicHJldmlvdXNfcmVzcG9uc2VfaWQiOm51bGwsInByb21wdCI6bnVsbH0=" - } - ] - }, - "scheduleToCloseTimeout": "10s", - "scheduleToStartTimeout": "10s", - "startToCloseTimeout": "10s", - "heartbeatTimeout": "0s", - "workflowTaskCompletedEventId": "4", - "retryPolicy": { - "initialInterval": "1s", - "backoffCoefficient": 2.0, - "maximumInterval": "100s" - }, - "useWorkflowBuildId": true - }, - "userMetadata": { - "summary": { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "IldoYXQncyB0aGUgY2FwaXRhbCBvZiBDYWxpZm9ybmlhPyI=" - } - } - }, - { - "eventId": "7", - "eventTime": "2025-06-30T16:59:17.540172Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_STARTED", - "taskId": "1056933", - "activityTaskStartedEventAttributes": { - "scheduledEventId": "5", - "identity": "19010@Tims-MacBook-Pro.local", - "requestId": "3c026c12-8c94-42c3-b2fa-1255191d6808", - "attempt": 1, - "workerVersion": { - "buildId": "9f32b5d9789ece25bfb8515a7023ec78" - } - } - }, - { - "eventId": "8", - "eventTime": "2025-06-30T16:59:18.732803Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_COMPLETED", - "taskId": "1056934", - "activityTaskCompletedEventAttributes": { - "result": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJvdXRwdXQiOlt7ImlkIjoibXNnXzY4NjJjMjY2ODk4ODgxOTE5OWYwYjUzMTM3ODMwY2FkMGNmMzNkMmZlNjJmMDYxOCIsImNvbnRlbnQiOlt7ImFubm90YXRpb25zIjpbXSwidGV4dCI6IlRoZSBjYXBpdGFsIG9mIENhbGlmb3JuaWEgaXMgU2FjcmFtZW50by4iLCJ0eXBlIjoib3V0cHV0X3RleHQiLCJsb2dwcm9icyI6W119XSwicm9sZSI6ImFzc2lzdGFudCIsInN0YXR1cyI6ImNvbXBsZXRlZCIsInR5cGUiOiJtZXNzYWdlIn1dLCJ1c2FnZSI6eyJyZXF1ZXN0cyI6MSwiaW5wdXRfdG9rZW5zIjozMSwiaW5wdXRfdG9rZW5zX2RldGFpbHMiOnsiY2FjaGVkX3Rva2VucyI6MH0sIm91dHB1dF90b2tlbnMiOjgsIm91dHB1dF90b2tlbnNfZGV0YWlscyI6eyJyZWFzb25pbmdfdG9rZW5zIjowfSwidG90YWxfdG9rZW5zIjozOX0sInJlc3BvbnNlX2lkIjoicmVzcF82ODYyYzI2NjQxMjg4MTkxOWFlNmU1NDIyZTI5MWYyNDBjZjMzZDJmZTYyZjA2MTgifQ==" - } - ] - }, - "scheduledEventId": "5", - "startedEventId": "7", - "identity": "19010@Tims-MacBook-Pro.local" - } - }, - { - "eventId": "9", - "eventTime": "2025-06-30T16:59:18.732810Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", - "taskId": "1056935", - "workflowTaskScheduledEventAttributes": { - "taskQueue": { - "name": "19010@Tims-MacBook-Pro.local-1859b9a9e19c44e0ab0c3e2fdc507859", - "kind": "TASK_QUEUE_KIND_STICKY", - "normalName": "e488932b-c66d-48b6-97cb-7b8feaa1c41c" - }, - "startToCloseTimeout": "10s", - "attempt": 1 - } - }, - { - "eventId": "10", - "eventTime": "2025-06-30T16:59:18.734336Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", - "taskId": "1056939", - "workflowTaskStartedEventAttributes": { - "scheduledEventId": "9", - "identity": "19010@Tims-MacBook-Pro.local", - "requestId": "fb9bae7c-f784-49df-9f34-4d74b7ce1c02", - "historySizeBytes": "3970", - "workerVersion": { - "buildId": "9f32b5d9789ece25bfb8515a7023ec78" - } - } - }, - { - "eventId": "11", - "eventTime": "2025-06-30T16:59:18.754123Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", - "taskId": "1056943", - "workflowTaskCompletedEventAttributes": { - "scheduledEventId": "9", - "startedEventId": "10", - "identity": "19010@Tims-MacBook-Pro.local", - "workerVersion": { - "buildId": "9f32b5d9789ece25bfb8515a7023ec78" - }, - "sdkMetadata": {}, - "meteringMetadata": {} - } - }, - { - "eventId": "12", - "eventTime": "2025-06-30T16:59:17.540616Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_STARTED", - "taskId": "1056945", - "activityTaskStartedEventAttributes": { - "scheduledEventId": "6", - "identity": "19010@Tims-MacBook-Pro.local", - "requestId": "9c1f71c6-2535-44f8-92e6-0a22a88d3880", - "attempt": 1, - "workerVersion": { - "buildId": "9f32b5d9789ece25bfb8515a7023ec78" - } - } - }, - { - "eventId": "13", - "eventTime": "2025-06-30T16:59:19.301541Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_COMPLETED", - "taskId": "1056946", - "activityTaskCompletedEventAttributes": { - "result": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJvdXRwdXQiOlt7ImlkIjoibXNnXzY4NjJjMjY2OWFkODgxOTJiNTgyYWEyNWVjNGU1NjEzMGE4YmM4Mzc4ZjRiMjEzMiIsImNvbnRlbnQiOlt7ImFubm90YXRpb25zIjpbXSwidGV4dCI6IntcInJlYXNvbmluZ1wiOlwiVGhlIHVzZXIgaXMgYXNraW5nIGZvciB0aGUgY2FwaXRhbCBvZiBDYWxpZm9ybmlhLCB3aGljaCBpcyBTYWNyYW1lbnRvLiBUaGlzIGlzIGEgZ2VvZ3JhcGh5IHF1ZXN0aW9uLCBub3QgYSBtYXRoIGhvbWV3b3JrIHF1ZXN0aW9uLlwiLFwiaXNfbWF0aF9ob21ld29ya1wiOmZhbHNlfSIsInR5cGUiOiJvdXRwdXRfdGV4dCIsImxvZ3Byb2JzIjpbXX1dLCJyb2xlIjoiYXNzaXN0YW50Iiwic3RhdHVzIjoiY29tcGxldGVkIiwidHlwZSI6Im1lc3NhZ2UifV0sInVzYWdlIjp7InJlcXVlc3RzIjoxLCJpbnB1dF90b2tlbnMiOjgxLCJpbnB1dF90b2tlbnNfZGV0YWlscyI6eyJjYWNoZWRfdG9rZW5zIjowfSwib3V0cHV0X3Rva2VucyI6MzgsIm91dHB1dF90b2tlbnNfZGV0YWlscyI6eyJyZWFzb25pbmdfdG9rZW5zIjowfSwidG90YWxfdG9rZW5zIjoxMTl9LCJyZXNwb25zZV9pZCI6InJlc3BfNjg2MmMyNjYxZDIwODE5MmI0NWFiM2E1OTRmZWNhYWUwYThiYzgzNzhmNGIyMTMyIn0=" - } - ] - }, - "scheduledEventId": "6", - "startedEventId": "12", - "identity": "19010@Tims-MacBook-Pro.local" - } - }, - { - "eventId": "14", - "eventTime": "2025-06-30T16:59:19.301556Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", - "taskId": "1056947", - "workflowTaskScheduledEventAttributes": { - "taskQueue": { - "name": "19010@Tims-MacBook-Pro.local-1859b9a9e19c44e0ab0c3e2fdc507859", - "kind": "TASK_QUEUE_KIND_STICKY", - "normalName": "e488932b-c66d-48b6-97cb-7b8feaa1c41c" - }, - "startToCloseTimeout": "10s", - "attempt": 1 - } - }, - { - "eventId": "15", - "eventTime": "2025-06-30T16:59:19.304340Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", - "taskId": "1056951", - "workflowTaskStartedEventAttributes": { - "scheduledEventId": "14", - "identity": "19010@Tims-MacBook-Pro.local", - "requestId": "fe6e7eb1-63c7-41b7-bd00-adcbec4b5e86", - "historySizeBytes": "5199", - "workerVersion": { - "buildId": "9f32b5d9789ece25bfb8515a7023ec78" - } - } - }, - { - "eventId": "16", - "eventTime": "2025-06-30T16:59:19.330544Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", - "taskId": "1056955", - "workflowTaskCompletedEventAttributes": { - "scheduledEventId": "14", - "startedEventId": "15", - "identity": "19010@Tims-MacBook-Pro.local", - "workerVersion": { - "buildId": "9f32b5d9789ece25bfb8515a7023ec78" - }, - "sdkMetadata": {}, - "meteringMetadata": {} - } - }, - { - "eventId": "17", - "eventTime": "2025-06-30T16:59:19.330594Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_SCHEDULED", - "taskId": "1056956", - "activityTaskScheduledEventAttributes": { - "activityId": "3", - "activityType": { - "name": "invoke_model_activity" - }, - "taskQueue": { - "name": "e488932b-c66d-48b6-97cb-7b8feaa1c41c", - "kind": "TASK_QUEUE_KIND_NORMAL" - }, - "header": { - "fields": { - "__openai_span": { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJ0cmFjZU5hbWUiOiJBZ2VudCB3b3JrZmxvdyIsInNwYW5JZCI6InNwYW5fYzI5ZmFkZWZjYmY1NDMzYmEwZjVmMzcwIiwidHJhY2VJZCI6InRyYWNlXzBhMDM5MjNlMDQ5NjQ1MTQ5NmQ1NTg2YTEzYmVmNjRlIn0=" - } - } - }, - "input": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJtb2RlbF9uYW1lIjpudWxsLCJzeXN0ZW1faW5zdHJ1Y3Rpb25zIjoiWW91IGFyZSBhIGN1c3RvbWVyIHN1cHBvcnQgYWdlbnQuIFlvdSBoZWxwIGN1c3RvbWVycyB3aXRoIHRoZWlyIHF1ZXN0aW9ucy4iLCJpbnB1dCI6W3sicm9sZSI6InVzZXIiLCJjb250ZW50IjoiV2hhdCdzIHRoZSBjYXBpdGFsIG9mIENhbGlmb3JuaWE/In0seyJpZCI6Im1zZ182ODYyYzI2Njg5ODg4MTkxOTlmMGI1MzEzNzgzMGNhZDBjZjMzZDJmZTYyZjA2MTgiLCJjb250ZW50IjpbeyJhbm5vdGF0aW9ucyI6W10sInRleHQiOiJUaGUgY2FwaXRhbCBvZiBDYWxpZm9ybmlhIGlzIFNhY3JhbWVudG8uIiwidHlwZSI6Im91dHB1dF90ZXh0IiwibG9ncHJvYnMiOltdfV0sInJvbGUiOiJhc3Npc3RhbnQiLCJzdGF0dXMiOiJjb21wbGV0ZWQiLCJ0eXBlIjoibWVzc2FnZSJ9LHsicm9sZSI6InVzZXIiLCJjb250ZW50IjoiQ2FuIHlvdSBoZWxwIG1lIHNvbHZlIGZvciB4OiAyeCArIDUgPSAxMSJ9XSwibW9kZWxfc2V0dGluZ3MiOnsidGVtcGVyYXR1cmUiOm51bGwsInRvcF9wIjpudWxsLCJmcmVxdWVuY3lfcGVuYWx0eSI6bnVsbCwicHJlc2VuY2VfcGVuYWx0eSI6bnVsbCwidG9vbF9jaG9pY2UiOm51bGwsInBhcmFsbGVsX3Rvb2xfY2FsbHMiOm51bGwsInRydW5jYXRpb24iOm51bGwsIm1heF90b2tlbnMiOm51bGwsInJlYXNvbmluZyI6bnVsbCwibWV0YWRhdGEiOm51bGwsInN0b3JlIjpudWxsLCJpbmNsdWRlX3VzYWdlIjpudWxsLCJleHRyYV9xdWVyeSI6bnVsbCwiZXh0cmFfYm9keSI6bnVsbCwiZXh0cmFfaGVhZGVycyI6bnVsbCwiZXh0cmFfYXJncyI6bnVsbH0sInRvb2xzIjpbXSwib3V0cHV0X3NjaGVtYSI6bnVsbCwiaGFuZG9mZnMiOltdLCJ0cmFjaW5nIjoxLCJwcmV2aW91c19yZXNwb25zZV9pZCI6bnVsbCwicHJvbXB0IjpudWxsfQ==" - } - ] - }, - "scheduleToCloseTimeout": "10s", - "scheduleToStartTimeout": "10s", - "startToCloseTimeout": "10s", - "heartbeatTimeout": "0s", - "workflowTaskCompletedEventId": "16", - "retryPolicy": { - "initialInterval": "1s", - "backoffCoefficient": 2.0, - "maximumInterval": "100s" - }, - "useWorkflowBuildId": true - }, - "userMetadata": { - "summary": { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "IkNhbiB5b3UgaGVscCBtZSBzb2x2ZSBmb3IgeDogMnggKyA1ID0gMTEi" - } - } - }, - { - "eventId": "18", - "eventTime": "2025-06-30T16:59:19.330630Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_SCHEDULED", - "taskId": "1056957", - "activityTaskScheduledEventAttributes": { - "activityId": "4", - "activityType": { - "name": "invoke_model_activity" - }, - "taskQueue": { - "name": "e488932b-c66d-48b6-97cb-7b8feaa1c41c", - "kind": "TASK_QUEUE_KIND_NORMAL" - }, - "header": { - "fields": { - "__openai_span": { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJ0cmFjZU5hbWUiOiJBZ2VudCB3b3JrZmxvdyIsInNwYW5JZCI6InNwYW5fMjVkZWI1NzZlNDZjNDcxNzgyNWFmMjNlIiwidHJhY2VJZCI6InRyYWNlXzBhMDM5MjNlMDQ5NjQ1MTQ5NmQ1NTg2YTEzYmVmNjRlIn0=" - } - } - }, - "input": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJtb2RlbF9uYW1lIjpudWxsLCJzeXN0ZW1faW5zdHJ1Y3Rpb25zIjoiQ2hlY2sgaWYgdGhlIHVzZXIgaXMgYXNraW5nIHlvdSB0byBkbyB0aGVpciBtYXRoIGhvbWV3b3JrLiIsImlucHV0IjpbeyJyb2xlIjoidXNlciIsImNvbnRlbnQiOiJXaGF0J3MgdGhlIGNhcGl0YWwgb2YgQ2FsaWZvcm5pYT8ifSx7ImlkIjoibXNnXzY4NjJjMjY2ODk4ODgxOTE5OWYwYjUzMTM3ODMwY2FkMGNmMzNkMmZlNjJmMDYxOCIsImNvbnRlbnQiOlt7ImFubm90YXRpb25zIjpbXSwidGV4dCI6IlRoZSBjYXBpdGFsIG9mIENhbGlmb3JuaWEgaXMgU2FjcmFtZW50by4iLCJ0eXBlIjoib3V0cHV0X3RleHQiLCJsb2dwcm9icyI6W119XSwicm9sZSI6ImFzc2lzdGFudCIsInN0YXR1cyI6ImNvbXBsZXRlZCIsInR5cGUiOiJtZXNzYWdlIn0seyJyb2xlIjoidXNlciIsImNvbnRlbnQiOiJDYW4geW91IGhlbHAgbWUgc29sdmUgZm9yIHg6IDJ4ICsgNSA9IDExIn1dLCJtb2RlbF9zZXR0aW5ncyI6eyJ0ZW1wZXJhdHVyZSI6bnVsbCwidG9wX3AiOm51bGwsImZyZXF1ZW5jeV9wZW5hbHR5IjpudWxsLCJwcmVzZW5jZV9wZW5hbHR5IjpudWxsLCJ0b29sX2Nob2ljZSI6bnVsbCwicGFyYWxsZWxfdG9vbF9jYWxscyI6bnVsbCwidHJ1bmNhdGlvbiI6bnVsbCwibWF4X3Rva2VucyI6bnVsbCwicmVhc29uaW5nIjpudWxsLCJtZXRhZGF0YSI6bnVsbCwic3RvcmUiOm51bGwsImluY2x1ZGVfdXNhZ2UiOm51bGwsImV4dHJhX3F1ZXJ5IjpudWxsLCJleHRyYV9ib2R5IjpudWxsLCJleHRyYV9oZWFkZXJzIjpudWxsLCJleHRyYV9hcmdzIjpudWxsfSwidG9vbHMiOltdLCJvdXRwdXRfc2NoZW1hIjp7Im91dHB1dF90eXBlX25hbWUiOiJNYXRoSG9tZXdvcmtPdXRwdXQiLCJpc193cmFwcGVkIjpmYWxzZSwib3V0cHV0X3NjaGVtYSI6eyJhZGRpdGlvbmFsUHJvcGVydGllcyI6ZmFsc2UsInByb3BlcnRpZXMiOnsicmVhc29uaW5nIjp7InRpdGxlIjoiUmVhc29uaW5nIiwidHlwZSI6InN0cmluZyJ9LCJpc19tYXRoX2hvbWV3b3JrIjp7InRpdGxlIjoiSXMgTWF0aCBIb21ld29yayIsInR5cGUiOiJib29sZWFuIn19LCJyZXF1aXJlZCI6WyJyZWFzb25pbmciLCJpc19tYXRoX2hvbWV3b3JrIl0sInRpdGxlIjoiTWF0aEhvbWV3b3JrT3V0cHV0IiwidHlwZSI6Im9iamVjdCJ9LCJzdHJpY3RfanNvbl9zY2hlbWEiOnRydWV9LCJoYW5kb2ZmcyI6W10sInRyYWNpbmciOjEsInByZXZpb3VzX3Jlc3BvbnNlX2lkIjpudWxsLCJwcm9tcHQiOm51bGx9" - } - ] - }, - "scheduleToCloseTimeout": "10s", - "scheduleToStartTimeout": "10s", - "startToCloseTimeout": "10s", - "heartbeatTimeout": "0s", - "workflowTaskCompletedEventId": "16", - "retryPolicy": { - "initialInterval": "1s", - "backoffCoefficient": 2.0, - "maximumInterval": "100s" - }, - "useWorkflowBuildId": true - }, - "userMetadata": { - "summary": { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "IkNhbiB5b3UgaGVscCBtZSBzb2x2ZSBmb3IgeDogMnggKyA1ID0gMTEi" - } - } - }, - { - "eventId": "19", - "eventTime": "2025-06-30T16:59:19.332558Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_STARTED", - "taskId": "1056963", - "activityTaskStartedEventAttributes": { - "scheduledEventId": "18", - "identity": "19010@Tims-MacBook-Pro.local", - "requestId": "a94a749b-36e2-4e3e-b0ab-098681af2d43", - "attempt": 1, - "workerVersion": { - "buildId": "9f32b5d9789ece25bfb8515a7023ec78" - } - } - }, - { - "eventId": "20", - "eventTime": "2025-06-30T16:59:20.830431Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_COMPLETED", - "taskId": "1056964", - "activityTaskCompletedEventAttributes": { - "result": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJvdXRwdXQiOlt7ImlkIjoibXNnXzY4NjJjMjY3ZGI0YzgxOTE5NzAwY2E0YzA3N2ExNjhjMGNmMzNkMmZlNjJmMDYxOCIsImNvbnRlbnQiOlt7ImFubm90YXRpb25zIjpbXSwidGV4dCI6IntcInJlYXNvbmluZ1wiOlwiVGhlIHF1ZXN0aW9uIGludm9sdmVzIHNvbHZpbmcgYSBzaW1wbGUgYWxnZWJyYWljIGVxdWF0aW9uLCB3aGljaCBpcyB0eXBpY2FsbHkgbWF0aCBob21ld29yay5cIixcImlzX21hdGhfaG9tZXdvcmtcIjp0cnVlfSIsInR5cGUiOiJvdXRwdXRfdGV4dCIsImxvZ3Byb2JzIjpbXX1dLCJyb2xlIjoiYXNzaXN0YW50Iiwic3RhdHVzIjoiY29tcGxldGVkIiwidHlwZSI6Im1lc3NhZ2UifV0sInVzYWdlIjp7InJlcXVlc3RzIjoxLCJpbnB1dF90b2tlbnMiOjExMywiaW5wdXRfdG9rZW5zX2RldGFpbHMiOnsiY2FjaGVkX3Rva2VucyI6MH0sIm91dHB1dF90b2tlbnMiOjI4LCJvdXRwdXRfdG9rZW5zX2RldGFpbHMiOnsicmVhc29uaW5nX3Rva2VucyI6MH0sInRvdGFsX3Rva2VucyI6MTQxfSwicmVzcG9uc2VfaWQiOiJyZXNwXzY4NjJjMjY3ODNjODgxOTFiOTUwZWZjZDkyMmVkZGViMGNmMzNkMmZlNjJmMDYxOCJ9" - } - ] - }, - "scheduledEventId": "18", - "startedEventId": "19", - "identity": "19010@Tims-MacBook-Pro.local" - } - }, - { - "eventId": "21", - "eventTime": "2025-06-30T16:59:20.830437Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", - "taskId": "1056965", - "workflowTaskScheduledEventAttributes": { - "taskQueue": { - "name": "19010@Tims-MacBook-Pro.local-1859b9a9e19c44e0ab0c3e2fdc507859", - "kind": "TASK_QUEUE_KIND_STICKY", - "normalName": "e488932b-c66d-48b6-97cb-7b8feaa1c41c" - }, - "startToCloseTimeout": "10s", - "attempt": 1 - } - }, - { - "eventId": "22", - "eventTime": "2025-06-30T16:59:20.832113Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", - "taskId": "1056969", - "workflowTaskStartedEventAttributes": { - "scheduledEventId": "21", - "identity": "19010@Tims-MacBook-Pro.local", - "requestId": "a1f78a09-290c-44cc-b1f3-69bc460a789f", - "historySizeBytes": "9386", - "workerVersion": { - "buildId": "9f32b5d9789ece25bfb8515a7023ec78" - } - } - }, - { - "eventId": "23", - "eventTime": "2025-06-30T16:59:20.851289Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", - "taskId": "1056973", - "workflowTaskCompletedEventAttributes": { - "scheduledEventId": "21", - "startedEventId": "22", - "identity": "19010@Tims-MacBook-Pro.local", - "workerVersion": { - "buildId": "9f32b5d9789ece25bfb8515a7023ec78" - }, - "sdkMetadata": {}, - "meteringMetadata": {} - } - }, - { - "eventId": "24", - "eventTime": "2025-06-30T16:59:20.851318Z", - "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_COMPLETED", - "taskId": "1056974", - "workflowExecutionCompletedEventAttributes": { - "result": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "WyJUaGUgY2FwaXRhbCBvZiBDYWxpZm9ybmlhIGlzIFNhY3JhbWVudG8uIiwiU29ycnksIEkgY2FuJ3QgaGVscCB5b3Ugd2l0aCB5b3VyIG1hdGggaG9tZXdvcmsuIl0=" - } - ] - }, - "workflowTaskCompletedEventId": "23" - } - } - ] -} \ No newline at end of file diff --git a/tests/contrib/openai_agents/histories/output-guardrail-workflow-history.json b/tests/contrib/openai_agents/histories/output-guardrail-workflow-history.json deleted file mode 100644 index a7a62944c..000000000 --- a/tests/contrib/openai_agents/histories/output-guardrail-workflow-history.json +++ /dev/null @@ -1,238 +0,0 @@ -{ - "events": [ - { - "eventId": "1", - "eventTime": "2025-06-30T17:01:02.451819Z", - "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_STARTED", - "taskId": "1057012", - "workflowExecutionStartedEventAttributes": { - "workflowType": { - "name": "OutputGuardrailWorkflow" - }, - "taskQueue": { - "name": "2227feb4-0f1d-4092-9653-308e9366edb0", - "kind": "TASK_QUEUE_KIND_NORMAL" - }, - "workflowExecutionTimeout": "10s", - "workflowRunTimeout": "10s", - "workflowTaskTimeout": "10s", - "originalExecutionRunId": "0197c1c8-f673-7c7b-a3b7-5b55d2a76055", - "identity": "19075@Tims-MacBook-Pro.local", - "firstExecutionRunId": "0197c1c8-f673-7c7b-a3b7-5b55d2a76055", - "attempt": 1, - "workflowExecutionExpirationTime": "2025-06-30T17:01:12.452Z", - "firstWorkflowTaskBackoff": "0s", - "workflowId": "output-guardrail-7c013c8e-95a7-4b77-8652-2f13ef3f630a" - } - }, - { - "eventId": "2", - "eventTime": "2025-06-30T17:01:02.451853Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", - "taskId": "1057013", - "workflowTaskScheduledEventAttributes": { - "taskQueue": { - "name": "2227feb4-0f1d-4092-9653-308e9366edb0", - "kind": "TASK_QUEUE_KIND_NORMAL" - }, - "startToCloseTimeout": "10s", - "attempt": 1 - } - }, - { - "eventId": "3", - "eventTime": "2025-06-30T17:01:02.453766Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", - "taskId": "1057019", - "workflowTaskStartedEventAttributes": { - "scheduledEventId": "2", - "identity": "19075@Tims-MacBook-Pro.local", - "requestId": "1b34e4ee-2f73-41aa-b870-64018017afd6", - "historySizeBytes": "367", - "workerVersion": { - "buildId": "9f32b5d9789ece25bfb8515a7023ec78" - } - } - }, - { - "eventId": "4", - "eventTime": "2025-06-30T17:01:02.489825Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", - "taskId": "1057023", - "workflowTaskCompletedEventAttributes": { - "scheduledEventId": "2", - "startedEventId": "3", - "identity": "19075@Tims-MacBook-Pro.local", - "workerVersion": { - "buildId": "9f32b5d9789ece25bfb8515a7023ec78" - }, - "sdkMetadata": { - "coreUsedFlags": [ - 3, - 2, - 1 - ], - "sdkName": "temporal-python", - "sdkVersion": "1.13.0" - }, - "meteringMetadata": {} - } - }, - { - "eventId": "5", - "eventTime": "2025-06-30T17:01:02.489848Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_SCHEDULED", - "taskId": "1057024", - "activityTaskScheduledEventAttributes": { - "activityId": "1", - "activityType": { - "name": "invoke_model_activity" - }, - "taskQueue": { - "name": "2227feb4-0f1d-4092-9653-308e9366edb0", - "kind": "TASK_QUEUE_KIND_NORMAL" - }, - "header": { - "fields": { - "__openai_span": { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJ0cmFjZU5hbWUiOiJBZ2VudCB3b3JrZmxvdyIsInNwYW5JZCI6InNwYW5fOTM4OGFhZjhjM2MzNDRkOThiNWYyODZjIiwidHJhY2VJZCI6InRyYWNlXzFlMTcxMjY1ZTg2NDRjNmViNTQ1MDI3ZDkyZTU3NTA0In0=" - } - } - }, - "input": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJtb2RlbF9uYW1lIjpudWxsLCJzeXN0ZW1faW5zdHJ1Y3Rpb25zIjoiWW91IGFyZSBhIGhlbHBmdWwgYXNzaXN0YW50LiIsImlucHV0IjpbeyJjb250ZW50IjoiTXkgcGhvbmUgbnVtYmVyIGlzIDY1MC0xMjMtNDU2Ny4gV2hlcmUgZG8geW91IHRoaW5rIEkgbGl2ZT8iLCJyb2xlIjoidXNlciJ9XSwibW9kZWxfc2V0dGluZ3MiOnsidGVtcGVyYXR1cmUiOm51bGwsInRvcF9wIjpudWxsLCJmcmVxdWVuY3lfcGVuYWx0eSI6bnVsbCwicHJlc2VuY2VfcGVuYWx0eSI6bnVsbCwidG9vbF9jaG9pY2UiOm51bGwsInBhcmFsbGVsX3Rvb2xfY2FsbHMiOm51bGwsInRydW5jYXRpb24iOm51bGwsIm1heF90b2tlbnMiOm51bGwsInJlYXNvbmluZyI6bnVsbCwibWV0YWRhdGEiOm51bGwsInN0b3JlIjpudWxsLCJpbmNsdWRlX3VzYWdlIjpudWxsLCJleHRyYV9xdWVyeSI6bnVsbCwiZXh0cmFfYm9keSI6bnVsbCwiZXh0cmFfaGVhZGVycyI6bnVsbCwiZXh0cmFfYXJncyI6bnVsbH0sInRvb2xzIjpbXSwib3V0cHV0X3NjaGVtYSI6eyJvdXRwdXRfdHlwZV9uYW1lIjoiTWVzc2FnZU91dHB1dCIsImlzX3dyYXBwZWQiOmZhbHNlLCJvdXRwdXRfc2NoZW1hIjp7ImFkZGl0aW9uYWxQcm9wZXJ0aWVzIjpmYWxzZSwicHJvcGVydGllcyI6eyJyZWFzb25pbmciOnsiZGVzY3JpcHRpb24iOiJUaG91Z2h0cyBvbiBob3cgdG8gcmVzcG9uZCB0byB0aGUgdXNlcidzIG1lc3NhZ2UiLCJ0aXRsZSI6IlJlYXNvbmluZyIsInR5cGUiOiJzdHJpbmcifSwicmVzcG9uc2UiOnsiZGVzY3JpcHRpb24iOiJUaGUgcmVzcG9uc2UgdG8gdGhlIHVzZXIncyBtZXNzYWdlIiwidGl0bGUiOiJSZXNwb25zZSIsInR5cGUiOiJzdHJpbmcifSwidXNlcl9uYW1lIjp7ImFueU9mIjpbeyJ0eXBlIjoic3RyaW5nIn0seyJ0eXBlIjoibnVsbCJ9XSwiZGVzY3JpcHRpb24iOiJUaGUgbmFtZSBvZiB0aGUgdXNlciB3aG8gc2VudCB0aGUgbWVzc2FnZSwgaWYga25vd24iLCJ0aXRsZSI6IlVzZXIgTmFtZSJ9fSwicmVxdWlyZWQiOlsicmVhc29uaW5nIiwicmVzcG9uc2UiLCJ1c2VyX25hbWUiXSwidGl0bGUiOiJNZXNzYWdlT3V0cHV0IiwidHlwZSI6Im9iamVjdCJ9LCJzdHJpY3RfanNvbl9zY2hlbWEiOnRydWV9LCJoYW5kb2ZmcyI6W10sInRyYWNpbmciOjEsInByZXZpb3VzX3Jlc3BvbnNlX2lkIjpudWxsLCJwcm9tcHQiOm51bGx9" - } - ] - }, - "scheduleToCloseTimeout": "10s", - "scheduleToStartTimeout": "10s", - "startToCloseTimeout": "10s", - "heartbeatTimeout": "0s", - "workflowTaskCompletedEventId": "4", - "retryPolicy": { - "initialInterval": "1s", - "backoffCoefficient": 2.0, - "maximumInterval": "100s" - }, - "useWorkflowBuildId": true - }, - "userMetadata": { - "summary": { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "Ik15IHBob25lIG51bWJlciBpcyA2NTAtMTIzLTQ1NjcuIFdoZXJlIGRvIHlvdSB0aGluayBJIGxpdmU/Ig==" - } - } - }, - { - "eventId": "6", - "eventTime": "2025-06-30T17:01:02.490525Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_STARTED", - "taskId": "1057029", - "activityTaskStartedEventAttributes": { - "scheduledEventId": "5", - "identity": "19075@Tims-MacBook-Pro.local", - "requestId": "c9f74d43-742f-4fa0-8f18-ce8272e945e5", - "attempt": 1, - "workerVersion": { - "buildId": "9f32b5d9789ece25bfb8515a7023ec78" - } - } - }, - { - "eventId": "7", - "eventTime": "2025-06-30T17:01:07.210264Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_COMPLETED", - "taskId": "1057030", - "activityTaskCompletedEventAttributes": { - "result": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJvdXRwdXQiOlt7ImlkIjoibXNnXzY4NjJjMmQwNGYyODgxOTFhNWNjNGZlNTExZmFjNGQ2MDE5ZmJiMjcwODliMTk0NCIsImNvbnRlbnQiOlt7ImFubm90YXRpb25zIjpbXSwidGV4dCI6IntcInJlYXNvbmluZ1wiOlwiVGhlIGFyZWEgY29kZSA2NTAgaXMgYXNzb2NpYXRlZCB3aXRoIHRoZSBTYW4gRnJhbmNpc2NvIEJheSBBcmVhLCBzcGVjaWZpY2FsbHkgdGhlIFNhbiBNYXRlbyBDb3VudHkgYW5kIHBhcnRzIG9mIFNhbnRhIENsYXJhIENvdW50eSBpbiBDYWxpZm9ybmlhLiBIb3dldmVyLCBqdXN0IGJlY2F1c2UgYSBwaG9uZSBudW1iZXIgaGFzIGEgY2VydGFpbiBhcmVhIGNvZGUgZG9lc24ndCBndWFyYW50ZWUgdGhlIHBlcnNvbiBjdXJyZW50bHkgcmVzaWRlcyB0aGVyZSwgYXMgcGVvcGxlIGNhbiBrZWVwIHRoZWlyIG51bWJlciB3aGVuIHRoZXkgbW92ZS5cIixcInJlc3BvbnNlXCI6XCJUaGUgNjUwIGFyZWEgY29kZSBpcyBhc3NvY2lhdGVkIHdpdGggdGhlIFNhbiBGcmFuY2lzY28gQmF5IEFyZWEsIHBhcnRpY3VsYXJseSBhcm91bmQgU2FuIE1hdGVvIGFuZCBwYXJ0cyBvZiBTYW50YSBDbGFyYSBDb3VudHkgaW4gQ2FsaWZvcm5pYS4gRG8geW91IGN1cnJlbnRseSBsaXZlIGluIHRoYXQgcmVnaW9uP1wiLFwidXNlcl9uYW1lXCI6bnVsbH0iLCJ0eXBlIjoib3V0cHV0X3RleHQiLCJsb2dwcm9icyI6W119XSwicm9sZSI6ImFzc2lzdGFudCIsInN0YXR1cyI6ImNvbXBsZXRlZCIsInR5cGUiOiJtZXNzYWdlIn1dLCJ1c2FnZSI6eyJyZXF1ZXN0cyI6MSwiaW5wdXRfdG9rZW5zIjoxNDAsImlucHV0X3Rva2Vuc19kZXRhaWxzIjp7ImNhY2hlZF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5zIjoxMDYsIm91dHB1dF90b2tlbnNfZGV0YWlscyI6eyJyZWFzb25pbmdfdG9rZW5zIjowfSwidG90YWxfdG9rZW5zIjoyNDZ9LCJyZXNwb25zZV9pZCI6InJlc3BfNjg2MmMyY2VkODI4ODE5MWFmYWMwNTc3YmYwNzcxY2QwMTlmYmIyNzA4OWIxOTQ0In0=" - } - ] - }, - "scheduledEventId": "5", - "startedEventId": "6", - "identity": "19075@Tims-MacBook-Pro.local" - } - }, - { - "eventId": "8", - "eventTime": "2025-06-30T17:01:07.210272Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", - "taskId": "1057031", - "workflowTaskScheduledEventAttributes": { - "taskQueue": { - "name": "19075@Tims-MacBook-Pro.local-7257bf626c8f40a3ade67d81e59bc041", - "kind": "TASK_QUEUE_KIND_STICKY", - "normalName": "2227feb4-0f1d-4092-9653-308e9366edb0" - }, - "startToCloseTimeout": "10s", - "attempt": 1 - } - }, - { - "eventId": "9", - "eventTime": "2025-06-30T17:01:07.212046Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", - "taskId": "1057035", - "workflowTaskStartedEventAttributes": { - "scheduledEventId": "8", - "identity": "19075@Tims-MacBook-Pro.local", - "requestId": "dac29894-c41b-46f8-8c24-5e9cf456b3cf", - "historySizeBytes": "3608", - "workerVersion": { - "buildId": "9f32b5d9789ece25bfb8515a7023ec78" - } - } - }, - { - "eventId": "10", - "eventTime": "2025-06-30T17:01:07.236912Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", - "taskId": "1057039", - "workflowTaskCompletedEventAttributes": { - "scheduledEventId": "8", - "startedEventId": "9", - "identity": "19075@Tims-MacBook-Pro.local", - "workerVersion": { - "buildId": "9f32b5d9789ece25bfb8515a7023ec78" - }, - "sdkMetadata": {}, - "meteringMetadata": {} - } - }, - { - "eventId": "11", - "eventTime": "2025-06-30T17:01:07.236935Z", - "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_COMPLETED", - "taskId": "1057040", - "workflowExecutionCompletedEventAttributes": { - "result": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "ZmFsc2U=" - } - ] - }, - "workflowTaskCompletedEventId": "10" - } - } - ] -} \ No newline at end of file diff --git a/tests/contrib/openai_agents/histories/research-workflow-history.json b/tests/contrib/openai_agents/histories/research-workflow-history.json deleted file mode 100644 index be38a0be0..000000000 --- a/tests/contrib/openai_agents/histories/research-workflow-history.json +++ /dev/null @@ -1,1755 +0,0 @@ -{ - "events": [ - { - "eventId": "1", - "eventTime": "2025-06-30T17:04:02.147299Z", - "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_STARTED", - "taskId": "1057045", - "workflowExecutionStartedEventAttributes": { - "workflowType": { - "name": "ResearchWorkflow" - }, - "taskQueue": { - "name": "fc97ba3c-6c32-4f4c-a986-8d7f4f5c7f80", - "kind": "TASK_QUEUE_KIND_NORMAL" - }, - "input": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "IkNhcmliYmVhbiB2YWNhdGlvbiBzcG90cyBpbiBBcHJpbCwgb3B0aW1pemluZyBmb3Igc3VyZmluZywgaGlraW5nIGFuZCB3YXRlciBzcG9ydHMi" - } - ] - }, - "workflowExecutionTimeout": "120s", - "workflowRunTimeout": "120s", - "workflowTaskTimeout": "10s", - "originalExecutionRunId": "0197c1cb-b463-748c-b871-7ae94b2add9a", - "identity": "19167@Tims-MacBook-Pro.local", - "firstExecutionRunId": "0197c1cb-b463-748c-b871-7ae94b2add9a", - "attempt": 1, - "workflowExecutionExpirationTime": "2025-06-30T17:06:02.147Z", - "firstWorkflowTaskBackoff": "0s", - "workflowId": "research-workflow-73f9d702-cddc-4608-91d8-c9999dc21a77" - } - }, - { - "eventId": "2", - "eventTime": "2025-06-30T17:04:02.147325Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", - "taskId": "1057046", - "workflowTaskScheduledEventAttributes": { - "taskQueue": { - "name": "fc97ba3c-6c32-4f4c-a986-8d7f4f5c7f80", - "kind": "TASK_QUEUE_KIND_NORMAL" - }, - "startToCloseTimeout": "10s", - "attempt": 1 - } - }, - { - "eventId": "3", - "eventTime": "2025-06-30T17:04:02.148179Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", - "taskId": "1057052", - "workflowTaskStartedEventAttributes": { - "scheduledEventId": "2", - "identity": "19167@Tims-MacBook-Pro.local", - "requestId": "3402fa90-f9c3-4948-9fe0-71f8e7410534", - "historySizeBytes": "472", - "workerVersion": { - "buildId": "9f32b5d9789ece25bfb8515a7023ec78" - } - } - }, - { - "eventId": "4", - "eventTime": "2025-06-30T17:04:02.187114Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", - "taskId": "1057056", - "workflowTaskCompletedEventAttributes": { - "scheduledEventId": "2", - "startedEventId": "3", - "identity": "19167@Tims-MacBook-Pro.local", - "workerVersion": { - "buildId": "9f32b5d9789ece25bfb8515a7023ec78" - }, - "sdkMetadata": { - "coreUsedFlags": [ - 3, - 1, - 2 - ], - "sdkName": "temporal-python", - "sdkVersion": "1.13.0" - }, - "meteringMetadata": {} - } - }, - { - "eventId": "5", - "eventTime": "2025-06-30T17:04:02.187138Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_SCHEDULED", - "taskId": "1057057", - "activityTaskScheduledEventAttributes": { - "activityId": "1", - "activityType": { - "name": "invoke_model_activity" - }, - "taskQueue": { - "name": "fc97ba3c-6c32-4f4c-a986-8d7f4f5c7f80", - "kind": "TASK_QUEUE_KIND_NORMAL" - }, - "header": { - "fields": { - "__openai_span": { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJ0cmFjZU5hbWUiOiJSZXNlYXJjaCB0cmFjZSIsInNwYW5JZCI6InNwYW5fODk0ZTM0YTlkYTM1NDdjMjkxZDNmODQzIiwidHJhY2VJZCI6InRyYWNlXzUyYjBiNjQ1MjNmMDQ3ZDc5MTI2YTc2OTY5ZGIzMTllIn0=" - } - } - }, - "input": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJtb2RlbF9uYW1lIjpudWxsLCJzeXN0ZW1faW5zdHJ1Y3Rpb25zIjoiWW91IGFyZSBhIGhlbHBmdWwgcmVzZWFyY2ggYXNzaXN0YW50LiBHaXZlbiBhIHF1ZXJ5LCBjb21lIHVwIHdpdGggYSBzZXQgb2Ygd2ViIHNlYXJjaGVzIHRvIHBlcmZvcm0gdG8gYmVzdCBhbnN3ZXIgdGhlIHF1ZXJ5LiBPdXRwdXQgYmV0d2VlbiA1IGFuZCAyMCB0ZXJtcyB0byBxdWVyeSBmb3IuIiwiaW5wdXQiOlt7ImNvbnRlbnQiOiJRdWVyeTogQ2FyaWJiZWFuIHZhY2F0aW9uIHNwb3RzIGluIEFwcmlsLCBvcHRpbWl6aW5nIGZvciBzdXJmaW5nLCBoaWtpbmcgYW5kIHdhdGVyIHNwb3J0cyIsInJvbGUiOiJ1c2VyIn1dLCJtb2RlbF9zZXR0aW5ncyI6eyJ0ZW1wZXJhdHVyZSI6bnVsbCwidG9wX3AiOm51bGwsImZyZXF1ZW5jeV9wZW5hbHR5IjpudWxsLCJwcmVzZW5jZV9wZW5hbHR5IjpudWxsLCJ0b29sX2Nob2ljZSI6bnVsbCwicGFyYWxsZWxfdG9vbF9jYWxscyI6bnVsbCwidHJ1bmNhdGlvbiI6bnVsbCwibWF4X3Rva2VucyI6bnVsbCwicmVhc29uaW5nIjpudWxsLCJtZXRhZGF0YSI6bnVsbCwic3RvcmUiOm51bGwsImluY2x1ZGVfdXNhZ2UiOm51bGwsImV4dHJhX3F1ZXJ5IjpudWxsLCJleHRyYV9ib2R5IjpudWxsLCJleHRyYV9oZWFkZXJzIjpudWxsLCJleHRyYV9hcmdzIjpudWxsfSwidG9vbHMiOltdLCJvdXRwdXRfc2NoZW1hIjp7Im91dHB1dF90eXBlX25hbWUiOiJXZWJTZWFyY2hQbGFuIiwiaXNfd3JhcHBlZCI6ZmFsc2UsIm91dHB1dF9zY2hlbWEiOnsiJGRlZnMiOnsiV2ViU2VhcmNoSXRlbSI6eyJwcm9wZXJ0aWVzIjp7InJlYXNvbiI6eyJ0aXRsZSI6IlJlYXNvbiIsInR5cGUiOiJzdHJpbmcifSwicXVlcnkiOnsidGl0bGUiOiJRdWVyeSIsInR5cGUiOiJzdHJpbmcifX0sInJlcXVpcmVkIjpbInJlYXNvbiIsInF1ZXJ5Il0sInRpdGxlIjoiV2ViU2VhcmNoSXRlbSIsInR5cGUiOiJvYmplY3QiLCJhZGRpdGlvbmFsUHJvcGVydGllcyI6ZmFsc2V9fSwicHJvcGVydGllcyI6eyJzZWFyY2hlcyI6eyJpdGVtcyI6eyIkcmVmIjoiIy8kZGVmcy9XZWJTZWFyY2hJdGVtIn0sInRpdGxlIjoiU2VhcmNoZXMiLCJ0eXBlIjoiYXJyYXkifX0sInJlcXVpcmVkIjpbInNlYXJjaGVzIl0sInRpdGxlIjoiV2ViU2VhcmNoUGxhbiIsInR5cGUiOiJvYmplY3QiLCJhZGRpdGlvbmFsUHJvcGVydGllcyI6ZmFsc2V9LCJzdHJpY3RfanNvbl9zY2hlbWEiOnRydWV9LCJoYW5kb2ZmcyI6W10sInRyYWNpbmciOjEsInByZXZpb3VzX3Jlc3BvbnNlX2lkIjpudWxsLCJwcm9tcHQiOm51bGx9" - } - ] - }, - "scheduleToCloseTimeout": "120s", - "scheduleToStartTimeout": "120s", - "startToCloseTimeout": "120s", - "heartbeatTimeout": "0s", - "workflowTaskCompletedEventId": "4", - "retryPolicy": { - "initialInterval": "1s", - "backoffCoefficient": 2.0, - "maximumInterval": "100s" - }, - "useWorkflowBuildId": true - }, - "userMetadata": { - "summary": { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "IlF1ZXJ5OiBDYXJpYmJlYW4gdmFjYXRpb24gc3BvdHMgaW4gQXByaWwsIG9wdGltaXppbmcgZm9yIHN1cmZpbmcsIGhpa2luZyBhbmQgd2F0ZXIgc3BvcnRzIg==" - } - } - }, - { - "eventId": "6", - "eventTime": "2025-06-30T17:04:02.187940Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_STARTED", - "taskId": "1057062", - "activityTaskStartedEventAttributes": { - "scheduledEventId": "5", - "identity": "19167@Tims-MacBook-Pro.local", - "requestId": "f624e2d5-6ce2-4b33-aa8e-e60051a1e843", - "attempt": 1, - "workerVersion": { - "buildId": "9f32b5d9789ece25bfb8515a7023ec78" - } - } - }, - { - "eventId": "7", - "eventTime": "2025-06-30T17:04:08.022291Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_COMPLETED", - "taskId": "1057063", - "activityTaskCompletedEventAttributes": { - "result": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJvdXRwdXQiOlt7ImlkIjoibXNnXzY4NjJjMzgzMTU2MDgxOTJiZjA5MDQ1Nzg1NmRhY2RhMDFlMzE0ODRkMWY3MjhmYSIsImNvbnRlbnQiOlt7ImFubm90YXRpb25zIjpbXSwidGV4dCI6IntcInNlYXJjaGVzXCI6W3tcInJlYXNvblwiOlwiVG8gZmluZCBkZXN0aW5hdGlvbnMgaW4gdGhlIENhcmliYmVhbiB0aGF0IGFyZSBpZGVhbCBmb3Igc3VyZmluZyBpbiBBcHJpbC5cIixcInF1ZXJ5XCI6XCJiZXN0IENhcmliYmVhbiBzdXJmaW5nIHNwb3RzIGluIEFwcmlsXCJ9LHtcInJlYXNvblwiOlwiVG8gaWRlbnRpZnkgdG9wIGhpa2luZyBkZXN0aW5hdGlvbnMgaW4gdGhlIENhcmliYmVhbiBmb3IgQXByaWwgdmFjYXRpb25zLlwiLFwicXVlcnlcIjpcImJlc3QgQ2FyaWJiZWFuIGhpa2luZyBkZXN0aW5hdGlvbnMgQXByaWxcIn0se1wicmVhc29uXCI6XCJUbyBleHBsb3JlIGdlbmVyYWwgQ2FyaWJiZWFuIHdhdGVyIHNwb3J0cyBvcHBvcnR1bml0aWVzIGF2YWlsYWJsZSBpbiBBcHJpbC5cIixcInF1ZXJ5XCI6XCJ0b3AgQ2FyaWJiZWFuIHdhdGVyIHNwb3J0cyBpbiBBcHJpbFwifSx7XCJyZWFzb25cIjpcIlRvIGRldGVybWluZSBzcGVjaWZpYyBpc2xhbmRzIGluIHRoZSBDYXJpYmJlYW4ga25vd24gZm9yIG91dGRvb3IgYWN0aXZpdGllcyBsaWtlIHN1cmZpbmcgYW5kIGhpa2luZyBpbiBBcHJpbC5cIixcInF1ZXJ5XCI6XCJDYXJpYmJlYW4gaXNsYW5kcyBzdXJmaW5nIGhpa2luZyBBcHJpbFwifSx7XCJyZWFzb25cIjpcIlRvIGZpbmQgdHJhdmVsIGd1aWRlcyBmb2N1c2luZyBvbiBvdXRkb29yIGFkdmVudHVyZSBpbiB0aGUgQ2FyaWJiZWFuIGR1cmluZyBBcHJpbC5cIixcInF1ZXJ5XCI6XCJDYXJpYmJlYW4gYWR2ZW50dXJlIHRyYXZlbCBndWlkZXMgQXByaWxcIn0se1wicmVhc29uXCI6XCJUbyBsZWFybiBhYm91dCB3ZWF0aGVyIGNvbmRpdGlvbnMgaW4gdGhlIENhcmliYmVhbiBpbiBBcHJpbCwgZm9jdXNpbmcgb24gb3V0ZG9vciBhY3Rpdml0aWVzLlwiLFwicXVlcnlcIjpcIkNhcmliYmVhbiB3ZWF0aGVyIEFwcmlsIHN1cmZpbmcgaGlraW5nXCJ9LHtcInJlYXNvblwiOlwiVG8gZGlzY292ZXIgYWxsLWluY2x1c2l2ZSB2YWNhdGlvbiBwYWNrYWdlcyBmb3IgYW4gYWR2ZW50dXJlLWZvY3VzZWQgQXByaWwgdHJpcC5cIixcInF1ZXJ5XCI6XCJDYXJpYmJlYW4gdmFjYXRpb24gcGFja2FnZXMgc3VyZmluZyBoaWtpbmcgQXByaWxcIn0se1wicmVhc29uXCI6XCJUbyByZWFkIHJldmlld3MgZnJvbSB0cmF2ZWxlcnMgd2hvIHZpc2l0ZWQgdGhlIENhcmliYmVhbiBmb3IgYWN0aXZlIHZhY2F0aW9ucyBpbiBBcHJpbC5cIixcInF1ZXJ5XCI6XCJDYXJpYmJlYW4gYWR2ZW50dXJlIHZhY2F0aW9uIHJldmlld3MgQXByaWxcIn0se1wicmVhc29uXCI6XCJUbyBmaW5kIGluZm9ybWF0aW9uIG9uIHJlbnRhbCBlcXVpcG1lbnQgYXZhaWxhYmlsaXR5IGZvciB3YXRlciBzcG9ydHMgYW5kIGhpa2luZyBpbiB0aGUgQ2FyaWJiZWFuLlwiLFwicXVlcnlcIjpcIndhdGVyIHNwb3J0cyBnZWFyIHJlbnRhbHMgQ2FyaWJiZWFuIEFwcmlsXCJ9LHtcInJlYXNvblwiOlwiVG8gY2hlY2sgZm9yIGFueSBmZXN0aXZhbHMgb3IgZXZlbnRzIHJlbGF0ZWQgdG8gc3VyZmluZyBvciB3YXRlciBzcG9ydHMgaW4gdGhlIENhcmliYmVhbiBpbiBBcHJpbC5cIixcInF1ZXJ5XCI6XCJDYXJpYmJlYW4gc3VyZmluZyBmZXN0aXZhbHMgQXByaWxcIn1dfSIsInR5cGUiOiJvdXRwdXRfdGV4dCIsImxvZ3Byb2JzIjpbXX1dLCJyb2xlIjoiYXNzaXN0YW50Iiwic3RhdHVzIjoiY29tcGxldGVkIiwidHlwZSI6Im1lc3NhZ2UifV0sInVzYWdlIjp7InJlcXVlc3RzIjoxLCJpbnB1dF90b2tlbnMiOjE1NiwiaW5wdXRfdG9rZW5zX2RldGFpbHMiOnsiY2FjaGVkX3Rva2VucyI6MH0sIm91dHB1dF90b2tlbnMiOjI2NCwib3V0cHV0X3Rva2Vuc19kZXRhaWxzIjp7InJlYXNvbmluZ190b2tlbnMiOjB9LCJ0b3RhbF90b2tlbnMiOjQyMH0sInJlc3BvbnNlX2lkIjoicmVzcF82ODYyYzM4Mjg5MWM4MTkyYjllYmM1ZTczZTZjYjkyNzAxZTMxNDg0ZDFmNzI4ZmEifQ==" - } - ] - }, - "scheduledEventId": "5", - "startedEventId": "6", - "identity": "19167@Tims-MacBook-Pro.local" - } - }, - { - "eventId": "8", - "eventTime": "2025-06-30T17:04:08.022297Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", - "taskId": "1057064", - "workflowTaskScheduledEventAttributes": { - "taskQueue": { - "name": "19167@Tims-MacBook-Pro.local-f783ce7516bc48e59108b7b5cd31d0e5", - "kind": "TASK_QUEUE_KIND_STICKY", - "normalName": "fc97ba3c-6c32-4f4c-a986-8d7f4f5c7f80" - }, - "startToCloseTimeout": "10s", - "attempt": 1 - } - }, - { - "eventId": "9", - "eventTime": "2025-06-30T17:04:08.023559Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", - "taskId": "1057068", - "workflowTaskStartedEventAttributes": { - "scheduledEventId": "8", - "identity": "19167@Tims-MacBook-Pro.local", - "requestId": "be4c32dd-d90a-466e-890e-acf232228fe2", - "historySizeBytes": "4901", - "workerVersion": { - "buildId": "9f32b5d9789ece25bfb8515a7023ec78" - } - } - }, - { - "eventId": "10", - "eventTime": "2025-06-30T17:04:08.052244Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", - "taskId": "1057072", - "workflowTaskCompletedEventAttributes": { - "scheduledEventId": "8", - "startedEventId": "9", - "identity": "19167@Tims-MacBook-Pro.local", - "workerVersion": { - "buildId": "9f32b5d9789ece25bfb8515a7023ec78" - }, - "sdkMetadata": {}, - "meteringMetadata": {} - } - }, - { - "eventId": "11", - "eventTime": "2025-06-30T17:04:08.052279Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_SCHEDULED", - "taskId": "1057073", - "activityTaskScheduledEventAttributes": { - "activityId": "2", - "activityType": { - "name": "invoke_model_activity" - }, - "taskQueue": { - "name": "fc97ba3c-6c32-4f4c-a986-8d7f4f5c7f80", - "kind": "TASK_QUEUE_KIND_NORMAL" - }, - "header": { - "fields": { - "__openai_span": { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJ0cmFjZU5hbWUiOiJSZXNlYXJjaCB0cmFjZSIsInNwYW5JZCI6InNwYW5fMGQ4ZjIzZmU3N2ZiNDUyMTgzMjg5NWE1IiwidHJhY2VJZCI6InRyYWNlXzUyYjBiNjQ1MjNmMDQ3ZDc5MTI2YTc2OTY5ZGIzMTllIn0=" - } - } - }, - "input": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJtb2RlbF9uYW1lIjpudWxsLCJzeXN0ZW1faW5zdHJ1Y3Rpb25zIjoiWW91IGFyZSBhIHJlc2VhcmNoIGFzc2lzdGFudC4gR2l2ZW4gYSBzZWFyY2ggdGVybSwgeW91IHNlYXJjaCB0aGUgd2ViIGZvciB0aGF0IHRlcm0gYW5kIHByb2R1Y2UgYSBjb25jaXNlIHN1bW1hcnkgb2YgdGhlIHJlc3VsdHMuIFRoZSBzdW1tYXJ5IG11c3QgMi0zIHBhcmFncmFwaHMgYW5kIGxlc3MgdGhhbiAzMDAgd29yZHMuIENhcHR1cmUgdGhlIG1haW4gcG9pbnRzLiBXcml0ZSBzdWNjaW5jdGx5LCBubyBuZWVkIHRvIGhhdmUgY29tcGxldGUgc2VudGVuY2VzIG9yIGdvb2QgZ3JhbW1hci4gVGhpcyB3aWxsIGJlIGNvbnN1bWVkIGJ5IHNvbWVvbmUgc3ludGhlc2l6aW5nIGEgcmVwb3J0LCBzbyBpdHMgdml0YWwgeW91IGNhcHR1cmUgdGhlIGVzc2VuY2UgYW5kIGlnbm9yZSBhbnkgZmx1ZmYuIERvIG5vdCBpbmNsdWRlIGFueSBhZGRpdGlvbmFsIGNvbW1lbnRhcnkgb3RoZXIgdGhhbiB0aGUgc3VtbWFyeSBpdHNlbGYuIiwiaW5wdXQiOlt7ImNvbnRlbnQiOiJTZWFyY2ggdGVybTogYmVzdCBDYXJpYmJlYW4gc3VyZmluZyBzcG90cyBpbiBBcHJpbFxuUmVhc29uIGZvciBzZWFyY2hpbmc6IFRvIGZpbmQgZGVzdGluYXRpb25zIGluIHRoZSBDYXJpYmJlYW4gdGhhdCBhcmUgaWRlYWwgZm9yIHN1cmZpbmcgaW4gQXByaWwuIiwicm9sZSI6InVzZXIifV0sIm1vZGVsX3NldHRpbmdzIjp7InRlbXBlcmF0dXJlIjpudWxsLCJ0b3BfcCI6bnVsbCwiZnJlcXVlbmN5X3BlbmFsdHkiOm51bGwsInByZXNlbmNlX3BlbmFsdHkiOm51bGwsInRvb2xfY2hvaWNlIjoicmVxdWlyZWQiLCJwYXJhbGxlbF90b29sX2NhbGxzIjpudWxsLCJ0cnVuY2F0aW9uIjpudWxsLCJtYXhfdG9rZW5zIjpudWxsLCJyZWFzb25pbmciOm51bGwsIm1ldGFkYXRhIjpudWxsLCJzdG9yZSI6bnVsbCwiaW5jbHVkZV91c2FnZSI6bnVsbCwiZXh0cmFfcXVlcnkiOm51bGwsImV4dHJhX2JvZHkiOm51bGwsImV4dHJhX2hlYWRlcnMiOm51bGwsImV4dHJhX2FyZ3MiOm51bGx9LCJ0b29scyI6W3sidXNlcl9sb2NhdGlvbiI6bnVsbCwic2VhcmNoX2NvbnRleHRfc2l6ZSI6Im1lZGl1bSJ9XSwib3V0cHV0X3NjaGVtYSI6bnVsbCwiaGFuZG9mZnMiOltdLCJ0cmFjaW5nIjoxLCJwcmV2aW91c19yZXNwb25zZV9pZCI6bnVsbCwicHJvbXB0IjpudWxsfQ==" - } - ] - }, - "scheduleToCloseTimeout": "120s", - "scheduleToStartTimeout": "120s", - "startToCloseTimeout": "120s", - "heartbeatTimeout": "0s", - "workflowTaskCompletedEventId": "10", - "retryPolicy": { - "initialInterval": "1s", - "backoffCoefficient": 2.0, - "maximumInterval": "100s" - }, - "useWorkflowBuildId": true - }, - "userMetadata": { - "summary": { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "IlNlYXJjaCB0ZXJtOiBiZXN0IENhcmliYmVhbiBzdXJmaW5nIHNwb3RzIGluIEFwcmlsXG5SZWFzb24gZm9yIHNlYXJjaGluZzogVG8gZmluZCBkZXN0aW5hdGlvbnMgaW4gdGhlIg==" - } - } - }, - { - "eventId": "12", - "eventTime": "2025-06-30T17:04:08.052305Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_SCHEDULED", - "taskId": "1057074", - "activityTaskScheduledEventAttributes": { - "activityId": "3", - "activityType": { - "name": "invoke_model_activity" - }, - "taskQueue": { - "name": "fc97ba3c-6c32-4f4c-a986-8d7f4f5c7f80", - "kind": "TASK_QUEUE_KIND_NORMAL" - }, - "header": { - "fields": { - "__openai_span": { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJ0cmFjZU5hbWUiOiJSZXNlYXJjaCB0cmFjZSIsInNwYW5JZCI6InNwYW5fNzVkZGQwY2I1MGRkNDlmZTk1OGUwNDM0IiwidHJhY2VJZCI6InRyYWNlXzUyYjBiNjQ1MjNmMDQ3ZDc5MTI2YTc2OTY5ZGIzMTllIn0=" - } - } - }, - "input": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJtb2RlbF9uYW1lIjpudWxsLCJzeXN0ZW1faW5zdHJ1Y3Rpb25zIjoiWW91IGFyZSBhIHJlc2VhcmNoIGFzc2lzdGFudC4gR2l2ZW4gYSBzZWFyY2ggdGVybSwgeW91IHNlYXJjaCB0aGUgd2ViIGZvciB0aGF0IHRlcm0gYW5kIHByb2R1Y2UgYSBjb25jaXNlIHN1bW1hcnkgb2YgdGhlIHJlc3VsdHMuIFRoZSBzdW1tYXJ5IG11c3QgMi0zIHBhcmFncmFwaHMgYW5kIGxlc3MgdGhhbiAzMDAgd29yZHMuIENhcHR1cmUgdGhlIG1haW4gcG9pbnRzLiBXcml0ZSBzdWNjaW5jdGx5LCBubyBuZWVkIHRvIGhhdmUgY29tcGxldGUgc2VudGVuY2VzIG9yIGdvb2QgZ3JhbW1hci4gVGhpcyB3aWxsIGJlIGNvbnN1bWVkIGJ5IHNvbWVvbmUgc3ludGhlc2l6aW5nIGEgcmVwb3J0LCBzbyBpdHMgdml0YWwgeW91IGNhcHR1cmUgdGhlIGVzc2VuY2UgYW5kIGlnbm9yZSBhbnkgZmx1ZmYuIERvIG5vdCBpbmNsdWRlIGFueSBhZGRpdGlvbmFsIGNvbW1lbnRhcnkgb3RoZXIgdGhhbiB0aGUgc3VtbWFyeSBpdHNlbGYuIiwiaW5wdXQiOlt7ImNvbnRlbnQiOiJTZWFyY2ggdGVybTogYmVzdCBDYXJpYmJlYW4gaGlraW5nIGRlc3RpbmF0aW9ucyBBcHJpbFxuUmVhc29uIGZvciBzZWFyY2hpbmc6IFRvIGlkZW50aWZ5IHRvcCBoaWtpbmcgZGVzdGluYXRpb25zIGluIHRoZSBDYXJpYmJlYW4gZm9yIEFwcmlsIHZhY2F0aW9ucy4iLCJyb2xlIjoidXNlciJ9XSwibW9kZWxfc2V0dGluZ3MiOnsidGVtcGVyYXR1cmUiOm51bGwsInRvcF9wIjpudWxsLCJmcmVxdWVuY3lfcGVuYWx0eSI6bnVsbCwicHJlc2VuY2VfcGVuYWx0eSI6bnVsbCwidG9vbF9jaG9pY2UiOiJyZXF1aXJlZCIsInBhcmFsbGVsX3Rvb2xfY2FsbHMiOm51bGwsInRydW5jYXRpb24iOm51bGwsIm1heF90b2tlbnMiOm51bGwsInJlYXNvbmluZyI6bnVsbCwibWV0YWRhdGEiOm51bGwsInN0b3JlIjpudWxsLCJpbmNsdWRlX3VzYWdlIjpudWxsLCJleHRyYV9xdWVyeSI6bnVsbCwiZXh0cmFfYm9keSI6bnVsbCwiZXh0cmFfaGVhZGVycyI6bnVsbCwiZXh0cmFfYXJncyI6bnVsbH0sInRvb2xzIjpbeyJ1c2VyX2xvY2F0aW9uIjpudWxsLCJzZWFyY2hfY29udGV4dF9zaXplIjoibWVkaXVtIn1dLCJvdXRwdXRfc2NoZW1hIjpudWxsLCJoYW5kb2ZmcyI6W10sInRyYWNpbmciOjEsInByZXZpb3VzX3Jlc3BvbnNlX2lkIjpudWxsLCJwcm9tcHQiOm51bGx9" - } - ] - }, - "scheduleToCloseTimeout": "120s", - "scheduleToStartTimeout": "120s", - "startToCloseTimeout": "120s", - "heartbeatTimeout": "0s", - "workflowTaskCompletedEventId": "10", - "retryPolicy": { - "initialInterval": "1s", - "backoffCoefficient": 2.0, - "maximumInterval": "100s" - }, - "useWorkflowBuildId": true - }, - "userMetadata": { - "summary": { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "IlNlYXJjaCB0ZXJtOiBiZXN0IENhcmliYmVhbiBoaWtpbmcgZGVzdGluYXRpb25zIEFwcmlsXG5SZWFzb24gZm9yIHNlYXJjaGluZzogVG8gaWRlbnRpZnkgdG9wIGhpa2luZyBkIg==" - } - } - }, - { - "eventId": "13", - "eventTime": "2025-06-30T17:04:08.052314Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_SCHEDULED", - "taskId": "1057075", - "activityTaskScheduledEventAttributes": { - "activityId": "4", - "activityType": { - "name": "invoke_model_activity" - }, - "taskQueue": { - "name": "fc97ba3c-6c32-4f4c-a986-8d7f4f5c7f80", - "kind": "TASK_QUEUE_KIND_NORMAL" - }, - "header": { - "fields": { - "__openai_span": { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJ0cmFjZU5hbWUiOiJSZXNlYXJjaCB0cmFjZSIsInNwYW5JZCI6InNwYW5fZjA1ZmY2MGFmMTgwNDIyZTk5NDNiZDQwIiwidHJhY2VJZCI6InRyYWNlXzUyYjBiNjQ1MjNmMDQ3ZDc5MTI2YTc2OTY5ZGIzMTllIn0=" - } - } - }, - "input": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJtb2RlbF9uYW1lIjpudWxsLCJzeXN0ZW1faW5zdHJ1Y3Rpb25zIjoiWW91IGFyZSBhIHJlc2VhcmNoIGFzc2lzdGFudC4gR2l2ZW4gYSBzZWFyY2ggdGVybSwgeW91IHNlYXJjaCB0aGUgd2ViIGZvciB0aGF0IHRlcm0gYW5kIHByb2R1Y2UgYSBjb25jaXNlIHN1bW1hcnkgb2YgdGhlIHJlc3VsdHMuIFRoZSBzdW1tYXJ5IG11c3QgMi0zIHBhcmFncmFwaHMgYW5kIGxlc3MgdGhhbiAzMDAgd29yZHMuIENhcHR1cmUgdGhlIG1haW4gcG9pbnRzLiBXcml0ZSBzdWNjaW5jdGx5LCBubyBuZWVkIHRvIGhhdmUgY29tcGxldGUgc2VudGVuY2VzIG9yIGdvb2QgZ3JhbW1hci4gVGhpcyB3aWxsIGJlIGNvbnN1bWVkIGJ5IHNvbWVvbmUgc3ludGhlc2l6aW5nIGEgcmVwb3J0LCBzbyBpdHMgdml0YWwgeW91IGNhcHR1cmUgdGhlIGVzc2VuY2UgYW5kIGlnbm9yZSBhbnkgZmx1ZmYuIERvIG5vdCBpbmNsdWRlIGFueSBhZGRpdGlvbmFsIGNvbW1lbnRhcnkgb3RoZXIgdGhhbiB0aGUgc3VtbWFyeSBpdHNlbGYuIiwiaW5wdXQiOlt7ImNvbnRlbnQiOiJTZWFyY2ggdGVybTogdG9wIENhcmliYmVhbiB3YXRlciBzcG9ydHMgaW4gQXByaWxcblJlYXNvbiBmb3Igc2VhcmNoaW5nOiBUbyBleHBsb3JlIGdlbmVyYWwgQ2FyaWJiZWFuIHdhdGVyIHNwb3J0cyBvcHBvcnR1bml0aWVzIGF2YWlsYWJsZSBpbiBBcHJpbC4iLCJyb2xlIjoidXNlciJ9XSwibW9kZWxfc2V0dGluZ3MiOnsidGVtcGVyYXR1cmUiOm51bGwsInRvcF9wIjpudWxsLCJmcmVxdWVuY3lfcGVuYWx0eSI6bnVsbCwicHJlc2VuY2VfcGVuYWx0eSI6bnVsbCwidG9vbF9jaG9pY2UiOiJyZXF1aXJlZCIsInBhcmFsbGVsX3Rvb2xfY2FsbHMiOm51bGwsInRydW5jYXRpb24iOm51bGwsIm1heF90b2tlbnMiOm51bGwsInJlYXNvbmluZyI6bnVsbCwibWV0YWRhdGEiOm51bGwsInN0b3JlIjpudWxsLCJpbmNsdWRlX3VzYWdlIjpudWxsLCJleHRyYV9xdWVyeSI6bnVsbCwiZXh0cmFfYm9keSI6bnVsbCwiZXh0cmFfaGVhZGVycyI6bnVsbCwiZXh0cmFfYXJncyI6bnVsbH0sInRvb2xzIjpbeyJ1c2VyX2xvY2F0aW9uIjpudWxsLCJzZWFyY2hfY29udGV4dF9zaXplIjoibWVkaXVtIn1dLCJvdXRwdXRfc2NoZW1hIjpudWxsLCJoYW5kb2ZmcyI6W10sInRyYWNpbmciOjEsInByZXZpb3VzX3Jlc3BvbnNlX2lkIjpudWxsLCJwcm9tcHQiOm51bGx9" - } - ] - }, - "scheduleToCloseTimeout": "120s", - "scheduleToStartTimeout": "120s", - "startToCloseTimeout": "120s", - "heartbeatTimeout": "0s", - "workflowTaskCompletedEventId": "10", - "retryPolicy": { - "initialInterval": "1s", - "backoffCoefficient": 2.0, - "maximumInterval": "100s" - }, - "useWorkflowBuildId": true - }, - "userMetadata": { - "summary": { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "IlNlYXJjaCB0ZXJtOiB0b3AgQ2FyaWJiZWFuIHdhdGVyIHNwb3J0cyBpbiBBcHJpbFxuUmVhc29uIGZvciBzZWFyY2hpbmc6IFRvIGV4cGxvcmUgZ2VuZXJhbCBDYXJpYmJlYW4gIg==" - } - } - }, - { - "eventId": "14", - "eventTime": "2025-06-30T17:04:08.052320Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_SCHEDULED", - "taskId": "1057076", - "activityTaskScheduledEventAttributes": { - "activityId": "5", - "activityType": { - "name": "invoke_model_activity" - }, - "taskQueue": { - "name": "fc97ba3c-6c32-4f4c-a986-8d7f4f5c7f80", - "kind": "TASK_QUEUE_KIND_NORMAL" - }, - "header": { - "fields": { - "__openai_span": { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJ0cmFjZU5hbWUiOiJSZXNlYXJjaCB0cmFjZSIsInNwYW5JZCI6InNwYW5fNWNjYWE0NGVlNDUzNGM3NzljZTdjMWRmIiwidHJhY2VJZCI6InRyYWNlXzUyYjBiNjQ1MjNmMDQ3ZDc5MTI2YTc2OTY5ZGIzMTllIn0=" - } - } - }, - "input": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJtb2RlbF9uYW1lIjpudWxsLCJzeXN0ZW1faW5zdHJ1Y3Rpb25zIjoiWW91IGFyZSBhIHJlc2VhcmNoIGFzc2lzdGFudC4gR2l2ZW4gYSBzZWFyY2ggdGVybSwgeW91IHNlYXJjaCB0aGUgd2ViIGZvciB0aGF0IHRlcm0gYW5kIHByb2R1Y2UgYSBjb25jaXNlIHN1bW1hcnkgb2YgdGhlIHJlc3VsdHMuIFRoZSBzdW1tYXJ5IG11c3QgMi0zIHBhcmFncmFwaHMgYW5kIGxlc3MgdGhhbiAzMDAgd29yZHMuIENhcHR1cmUgdGhlIG1haW4gcG9pbnRzLiBXcml0ZSBzdWNjaW5jdGx5LCBubyBuZWVkIHRvIGhhdmUgY29tcGxldGUgc2VudGVuY2VzIG9yIGdvb2QgZ3JhbW1hci4gVGhpcyB3aWxsIGJlIGNvbnN1bWVkIGJ5IHNvbWVvbmUgc3ludGhlc2l6aW5nIGEgcmVwb3J0LCBzbyBpdHMgdml0YWwgeW91IGNhcHR1cmUgdGhlIGVzc2VuY2UgYW5kIGlnbm9yZSBhbnkgZmx1ZmYuIERvIG5vdCBpbmNsdWRlIGFueSBhZGRpdGlvbmFsIGNvbW1lbnRhcnkgb3RoZXIgdGhhbiB0aGUgc3VtbWFyeSBpdHNlbGYuIiwiaW5wdXQiOlt7ImNvbnRlbnQiOiJTZWFyY2ggdGVybTogQ2FyaWJiZWFuIGlzbGFuZHMgc3VyZmluZyBoaWtpbmcgQXByaWxcblJlYXNvbiBmb3Igc2VhcmNoaW5nOiBUbyBkZXRlcm1pbmUgc3BlY2lmaWMgaXNsYW5kcyBpbiB0aGUgQ2FyaWJiZWFuIGtub3duIGZvciBvdXRkb29yIGFjdGl2aXRpZXMgbGlrZSBzdXJmaW5nIGFuZCBoaWtpbmcgaW4gQXByaWwuIiwicm9sZSI6InVzZXIifV0sIm1vZGVsX3NldHRpbmdzIjp7InRlbXBlcmF0dXJlIjpudWxsLCJ0b3BfcCI6bnVsbCwiZnJlcXVlbmN5X3BlbmFsdHkiOm51bGwsInByZXNlbmNlX3BlbmFsdHkiOm51bGwsInRvb2xfY2hvaWNlIjoicmVxdWlyZWQiLCJwYXJhbGxlbF90b29sX2NhbGxzIjpudWxsLCJ0cnVuY2F0aW9uIjpudWxsLCJtYXhfdG9rZW5zIjpudWxsLCJyZWFzb25pbmciOm51bGwsIm1ldGFkYXRhIjpudWxsLCJzdG9yZSI6bnVsbCwiaW5jbHVkZV91c2FnZSI6bnVsbCwiZXh0cmFfcXVlcnkiOm51bGwsImV4dHJhX2JvZHkiOm51bGwsImV4dHJhX2hlYWRlcnMiOm51bGwsImV4dHJhX2FyZ3MiOm51bGx9LCJ0b29scyI6W3sidXNlcl9sb2NhdGlvbiI6bnVsbCwic2VhcmNoX2NvbnRleHRfc2l6ZSI6Im1lZGl1bSJ9XSwib3V0cHV0X3NjaGVtYSI6bnVsbCwiaGFuZG9mZnMiOltdLCJ0cmFjaW5nIjoxLCJwcmV2aW91c19yZXNwb25zZV9pZCI6bnVsbCwicHJvbXB0IjpudWxsfQ==" - } - ] - }, - "scheduleToCloseTimeout": "120s", - "scheduleToStartTimeout": "120s", - "startToCloseTimeout": "120s", - "heartbeatTimeout": "0s", - "workflowTaskCompletedEventId": "10", - "retryPolicy": { - "initialInterval": "1s", - "backoffCoefficient": 2.0, - "maximumInterval": "100s" - }, - "useWorkflowBuildId": true - }, - "userMetadata": { - "summary": { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "IlNlYXJjaCB0ZXJtOiBDYXJpYmJlYW4gaXNsYW5kcyBzdXJmaW5nIGhpa2luZyBBcHJpbFxuUmVhc29uIGZvciBzZWFyY2hpbmc6IFRvIGRldGVybWluZSBzcGVjaWZpYyBpc2xhIg==" - } - } - }, - { - "eventId": "15", - "eventTime": "2025-06-30T17:04:08.052331Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_SCHEDULED", - "taskId": "1057077", - "activityTaskScheduledEventAttributes": { - "activityId": "6", - "activityType": { - "name": "invoke_model_activity" - }, - "taskQueue": { - "name": "fc97ba3c-6c32-4f4c-a986-8d7f4f5c7f80", - "kind": "TASK_QUEUE_KIND_NORMAL" - }, - "header": { - "fields": { - "__openai_span": { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJ0cmFjZU5hbWUiOiJSZXNlYXJjaCB0cmFjZSIsInNwYW5JZCI6InNwYW5fZGU2MjJhMmZlNGY0NDJmMmEwMGIxZTFiIiwidHJhY2VJZCI6InRyYWNlXzUyYjBiNjQ1MjNmMDQ3ZDc5MTI2YTc2OTY5ZGIzMTllIn0=" - } - } - }, - "input": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJtb2RlbF9uYW1lIjpudWxsLCJzeXN0ZW1faW5zdHJ1Y3Rpb25zIjoiWW91IGFyZSBhIHJlc2VhcmNoIGFzc2lzdGFudC4gR2l2ZW4gYSBzZWFyY2ggdGVybSwgeW91IHNlYXJjaCB0aGUgd2ViIGZvciB0aGF0IHRlcm0gYW5kIHByb2R1Y2UgYSBjb25jaXNlIHN1bW1hcnkgb2YgdGhlIHJlc3VsdHMuIFRoZSBzdW1tYXJ5IG11c3QgMi0zIHBhcmFncmFwaHMgYW5kIGxlc3MgdGhhbiAzMDAgd29yZHMuIENhcHR1cmUgdGhlIG1haW4gcG9pbnRzLiBXcml0ZSBzdWNjaW5jdGx5LCBubyBuZWVkIHRvIGhhdmUgY29tcGxldGUgc2VudGVuY2VzIG9yIGdvb2QgZ3JhbW1hci4gVGhpcyB3aWxsIGJlIGNvbnN1bWVkIGJ5IHNvbWVvbmUgc3ludGhlc2l6aW5nIGEgcmVwb3J0LCBzbyBpdHMgdml0YWwgeW91IGNhcHR1cmUgdGhlIGVzc2VuY2UgYW5kIGlnbm9yZSBhbnkgZmx1ZmYuIERvIG5vdCBpbmNsdWRlIGFueSBhZGRpdGlvbmFsIGNvbW1lbnRhcnkgb3RoZXIgdGhhbiB0aGUgc3VtbWFyeSBpdHNlbGYuIiwiaW5wdXQiOlt7ImNvbnRlbnQiOiJTZWFyY2ggdGVybTogQ2FyaWJiZWFuIGFkdmVudHVyZSB0cmF2ZWwgZ3VpZGVzIEFwcmlsXG5SZWFzb24gZm9yIHNlYXJjaGluZzogVG8gZmluZCB0cmF2ZWwgZ3VpZGVzIGZvY3VzaW5nIG9uIG91dGRvb3IgYWR2ZW50dXJlIGluIHRoZSBDYXJpYmJlYW4gZHVyaW5nIEFwcmlsLiIsInJvbGUiOiJ1c2VyIn1dLCJtb2RlbF9zZXR0aW5ncyI6eyJ0ZW1wZXJhdHVyZSI6bnVsbCwidG9wX3AiOm51bGwsImZyZXF1ZW5jeV9wZW5hbHR5IjpudWxsLCJwcmVzZW5jZV9wZW5hbHR5IjpudWxsLCJ0b29sX2Nob2ljZSI6InJlcXVpcmVkIiwicGFyYWxsZWxfdG9vbF9jYWxscyI6bnVsbCwidHJ1bmNhdGlvbiI6bnVsbCwibWF4X3Rva2VucyI6bnVsbCwicmVhc29uaW5nIjpudWxsLCJtZXRhZGF0YSI6bnVsbCwic3RvcmUiOm51bGwsImluY2x1ZGVfdXNhZ2UiOm51bGwsImV4dHJhX3F1ZXJ5IjpudWxsLCJleHRyYV9ib2R5IjpudWxsLCJleHRyYV9oZWFkZXJzIjpudWxsLCJleHRyYV9hcmdzIjpudWxsfSwidG9vbHMiOlt7InVzZXJfbG9jYXRpb24iOm51bGwsInNlYXJjaF9jb250ZXh0X3NpemUiOiJtZWRpdW0ifV0sIm91dHB1dF9zY2hlbWEiOm51bGwsImhhbmRvZmZzIjpbXSwidHJhY2luZyI6MSwicHJldmlvdXNfcmVzcG9uc2VfaWQiOm51bGwsInByb21wdCI6bnVsbH0=" - } - ] - }, - "scheduleToCloseTimeout": "120s", - "scheduleToStartTimeout": "120s", - "startToCloseTimeout": "120s", - "heartbeatTimeout": "0s", - "workflowTaskCompletedEventId": "10", - "retryPolicy": { - "initialInterval": "1s", - "backoffCoefficient": 2.0, - "maximumInterval": "100s" - }, - "useWorkflowBuildId": true - }, - "userMetadata": { - "summary": { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "IlNlYXJjaCB0ZXJtOiBDYXJpYmJlYW4gYWR2ZW50dXJlIHRyYXZlbCBndWlkZXMgQXByaWxcblJlYXNvbiBmb3Igc2VhcmNoaW5nOiBUbyBmaW5kIHRyYXZlbCBndWlkZXMgZm9jIg==" - } - } - }, - { - "eventId": "16", - "eventTime": "2025-06-30T17:04:08.052336Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_SCHEDULED", - "taskId": "1057078", - "activityTaskScheduledEventAttributes": { - "activityId": "7", - "activityType": { - "name": "invoke_model_activity" - }, - "taskQueue": { - "name": "fc97ba3c-6c32-4f4c-a986-8d7f4f5c7f80", - "kind": "TASK_QUEUE_KIND_NORMAL" - }, - "header": { - "fields": { - "__openai_span": { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJ0cmFjZU5hbWUiOiJSZXNlYXJjaCB0cmFjZSIsInNwYW5JZCI6InNwYW5fMGI1ODRlYzQyOTE4NDA2NmE0OGQwMTM4IiwidHJhY2VJZCI6InRyYWNlXzUyYjBiNjQ1MjNmMDQ3ZDc5MTI2YTc2OTY5ZGIzMTllIn0=" - } - } - }, - "input": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJtb2RlbF9uYW1lIjpudWxsLCJzeXN0ZW1faW5zdHJ1Y3Rpb25zIjoiWW91IGFyZSBhIHJlc2VhcmNoIGFzc2lzdGFudC4gR2l2ZW4gYSBzZWFyY2ggdGVybSwgeW91IHNlYXJjaCB0aGUgd2ViIGZvciB0aGF0IHRlcm0gYW5kIHByb2R1Y2UgYSBjb25jaXNlIHN1bW1hcnkgb2YgdGhlIHJlc3VsdHMuIFRoZSBzdW1tYXJ5IG11c3QgMi0zIHBhcmFncmFwaHMgYW5kIGxlc3MgdGhhbiAzMDAgd29yZHMuIENhcHR1cmUgdGhlIG1haW4gcG9pbnRzLiBXcml0ZSBzdWNjaW5jdGx5LCBubyBuZWVkIHRvIGhhdmUgY29tcGxldGUgc2VudGVuY2VzIG9yIGdvb2QgZ3JhbW1hci4gVGhpcyB3aWxsIGJlIGNvbnN1bWVkIGJ5IHNvbWVvbmUgc3ludGhlc2l6aW5nIGEgcmVwb3J0LCBzbyBpdHMgdml0YWwgeW91IGNhcHR1cmUgdGhlIGVzc2VuY2UgYW5kIGlnbm9yZSBhbnkgZmx1ZmYuIERvIG5vdCBpbmNsdWRlIGFueSBhZGRpdGlvbmFsIGNvbW1lbnRhcnkgb3RoZXIgdGhhbiB0aGUgc3VtbWFyeSBpdHNlbGYuIiwiaW5wdXQiOlt7ImNvbnRlbnQiOiJTZWFyY2ggdGVybTogQ2FyaWJiZWFuIHdlYXRoZXIgQXByaWwgc3VyZmluZyBoaWtpbmdcblJlYXNvbiBmb3Igc2VhcmNoaW5nOiBUbyBsZWFybiBhYm91dCB3ZWF0aGVyIGNvbmRpdGlvbnMgaW4gdGhlIENhcmliYmVhbiBpbiBBcHJpbCwgZm9jdXNpbmcgb24gb3V0ZG9vciBhY3Rpdml0aWVzLiIsInJvbGUiOiJ1c2VyIn1dLCJtb2RlbF9zZXR0aW5ncyI6eyJ0ZW1wZXJhdHVyZSI6bnVsbCwidG9wX3AiOm51bGwsImZyZXF1ZW5jeV9wZW5hbHR5IjpudWxsLCJwcmVzZW5jZV9wZW5hbHR5IjpudWxsLCJ0b29sX2Nob2ljZSI6InJlcXVpcmVkIiwicGFyYWxsZWxfdG9vbF9jYWxscyI6bnVsbCwidHJ1bmNhdGlvbiI6bnVsbCwibWF4X3Rva2VucyI6bnVsbCwicmVhc29uaW5nIjpudWxsLCJtZXRhZGF0YSI6bnVsbCwic3RvcmUiOm51bGwsImluY2x1ZGVfdXNhZ2UiOm51bGwsImV4dHJhX3F1ZXJ5IjpudWxsLCJleHRyYV9ib2R5IjpudWxsLCJleHRyYV9oZWFkZXJzIjpudWxsLCJleHRyYV9hcmdzIjpudWxsfSwidG9vbHMiOlt7InVzZXJfbG9jYXRpb24iOm51bGwsInNlYXJjaF9jb250ZXh0X3NpemUiOiJtZWRpdW0ifV0sIm91dHB1dF9zY2hlbWEiOm51bGwsImhhbmRvZmZzIjpbXSwidHJhY2luZyI6MSwicHJldmlvdXNfcmVzcG9uc2VfaWQiOm51bGwsInByb21wdCI6bnVsbH0=" - } - ] - }, - "scheduleToCloseTimeout": "120s", - "scheduleToStartTimeout": "120s", - "startToCloseTimeout": "120s", - "heartbeatTimeout": "0s", - "workflowTaskCompletedEventId": "10", - "retryPolicy": { - "initialInterval": "1s", - "backoffCoefficient": 2.0, - "maximumInterval": "100s" - }, - "useWorkflowBuildId": true - }, - "userMetadata": { - "summary": { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "IlNlYXJjaCB0ZXJtOiBDYXJpYmJlYW4gd2VhdGhlciBBcHJpbCBzdXJmaW5nIGhpa2luZ1xuUmVhc29uIGZvciBzZWFyY2hpbmc6IFRvIGxlYXJuIGFib3V0IHdlYXRoZXIgY29uIg==" - } - } - }, - { - "eventId": "17", - "eventTime": "2025-06-30T17:04:08.052343Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_SCHEDULED", - "taskId": "1057079", - "activityTaskScheduledEventAttributes": { - "activityId": "8", - "activityType": { - "name": "invoke_model_activity" - }, - "taskQueue": { - "name": "fc97ba3c-6c32-4f4c-a986-8d7f4f5c7f80", - "kind": "TASK_QUEUE_KIND_NORMAL" - }, - "header": { - "fields": { - "__openai_span": { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJ0cmFjZU5hbWUiOiJSZXNlYXJjaCB0cmFjZSIsInNwYW5JZCI6InNwYW5fMTAxYTRmMjUzMzgyNDU1ZmE2NjczMDU4IiwidHJhY2VJZCI6InRyYWNlXzUyYjBiNjQ1MjNmMDQ3ZDc5MTI2YTc2OTY5ZGIzMTllIn0=" - } - } - }, - "input": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJtb2RlbF9uYW1lIjpudWxsLCJzeXN0ZW1faW5zdHJ1Y3Rpb25zIjoiWW91IGFyZSBhIHJlc2VhcmNoIGFzc2lzdGFudC4gR2l2ZW4gYSBzZWFyY2ggdGVybSwgeW91IHNlYXJjaCB0aGUgd2ViIGZvciB0aGF0IHRlcm0gYW5kIHByb2R1Y2UgYSBjb25jaXNlIHN1bW1hcnkgb2YgdGhlIHJlc3VsdHMuIFRoZSBzdW1tYXJ5IG11c3QgMi0zIHBhcmFncmFwaHMgYW5kIGxlc3MgdGhhbiAzMDAgd29yZHMuIENhcHR1cmUgdGhlIG1haW4gcG9pbnRzLiBXcml0ZSBzdWNjaW5jdGx5LCBubyBuZWVkIHRvIGhhdmUgY29tcGxldGUgc2VudGVuY2VzIG9yIGdvb2QgZ3JhbW1hci4gVGhpcyB3aWxsIGJlIGNvbnN1bWVkIGJ5IHNvbWVvbmUgc3ludGhlc2l6aW5nIGEgcmVwb3J0LCBzbyBpdHMgdml0YWwgeW91IGNhcHR1cmUgdGhlIGVzc2VuY2UgYW5kIGlnbm9yZSBhbnkgZmx1ZmYuIERvIG5vdCBpbmNsdWRlIGFueSBhZGRpdGlvbmFsIGNvbW1lbnRhcnkgb3RoZXIgdGhhbiB0aGUgc3VtbWFyeSBpdHNlbGYuIiwiaW5wdXQiOlt7ImNvbnRlbnQiOiJTZWFyY2ggdGVybTogQ2FyaWJiZWFuIHZhY2F0aW9uIHBhY2thZ2VzIHN1cmZpbmcgaGlraW5nIEFwcmlsXG5SZWFzb24gZm9yIHNlYXJjaGluZzogVG8gZGlzY292ZXIgYWxsLWluY2x1c2l2ZSB2YWNhdGlvbiBwYWNrYWdlcyBmb3IgYW4gYWR2ZW50dXJlLWZvY3VzZWQgQXByaWwgdHJpcC4iLCJyb2xlIjoidXNlciJ9XSwibW9kZWxfc2V0dGluZ3MiOnsidGVtcGVyYXR1cmUiOm51bGwsInRvcF9wIjpudWxsLCJmcmVxdWVuY3lfcGVuYWx0eSI6bnVsbCwicHJlc2VuY2VfcGVuYWx0eSI6bnVsbCwidG9vbF9jaG9pY2UiOiJyZXF1aXJlZCIsInBhcmFsbGVsX3Rvb2xfY2FsbHMiOm51bGwsInRydW5jYXRpb24iOm51bGwsIm1heF90b2tlbnMiOm51bGwsInJlYXNvbmluZyI6bnVsbCwibWV0YWRhdGEiOm51bGwsInN0b3JlIjpudWxsLCJpbmNsdWRlX3VzYWdlIjpudWxsLCJleHRyYV9xdWVyeSI6bnVsbCwiZXh0cmFfYm9keSI6bnVsbCwiZXh0cmFfaGVhZGVycyI6bnVsbCwiZXh0cmFfYXJncyI6bnVsbH0sInRvb2xzIjpbeyJ1c2VyX2xvY2F0aW9uIjpudWxsLCJzZWFyY2hfY29udGV4dF9zaXplIjoibWVkaXVtIn1dLCJvdXRwdXRfc2NoZW1hIjpudWxsLCJoYW5kb2ZmcyI6W10sInRyYWNpbmciOjEsInByZXZpb3VzX3Jlc3BvbnNlX2lkIjpudWxsLCJwcm9tcHQiOm51bGx9" - } - ] - }, - "scheduleToCloseTimeout": "120s", - "scheduleToStartTimeout": "120s", - "startToCloseTimeout": "120s", - "heartbeatTimeout": "0s", - "workflowTaskCompletedEventId": "10", - "retryPolicy": { - "initialInterval": "1s", - "backoffCoefficient": 2.0, - "maximumInterval": "100s" - }, - "useWorkflowBuildId": true - }, - "userMetadata": { - "summary": { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "IlNlYXJjaCB0ZXJtOiBDYXJpYmJlYW4gdmFjYXRpb24gcGFja2FnZXMgc3VyZmluZyBoaWtpbmcgQXByaWxcblJlYXNvbiBmb3Igc2VhcmNoaW5nOiBUbyBkaXNjb3ZlciBhbGwtIg==" - } - } - }, - { - "eventId": "18", - "eventTime": "2025-06-30T17:04:08.052350Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_SCHEDULED", - "taskId": "1057080", - "activityTaskScheduledEventAttributes": { - "activityId": "9", - "activityType": { - "name": "invoke_model_activity" - }, - "taskQueue": { - "name": "fc97ba3c-6c32-4f4c-a986-8d7f4f5c7f80", - "kind": "TASK_QUEUE_KIND_NORMAL" - }, - "header": { - "fields": { - "__openai_span": { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJ0cmFjZU5hbWUiOiJSZXNlYXJjaCB0cmFjZSIsInNwYW5JZCI6InNwYW5fMThlZjMzYmI0ODFjNDM1M2I2NjA5YTU3IiwidHJhY2VJZCI6InRyYWNlXzUyYjBiNjQ1MjNmMDQ3ZDc5MTI2YTc2OTY5ZGIzMTllIn0=" - } - } - }, - "input": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJtb2RlbF9uYW1lIjpudWxsLCJzeXN0ZW1faW5zdHJ1Y3Rpb25zIjoiWW91IGFyZSBhIHJlc2VhcmNoIGFzc2lzdGFudC4gR2l2ZW4gYSBzZWFyY2ggdGVybSwgeW91IHNlYXJjaCB0aGUgd2ViIGZvciB0aGF0IHRlcm0gYW5kIHByb2R1Y2UgYSBjb25jaXNlIHN1bW1hcnkgb2YgdGhlIHJlc3VsdHMuIFRoZSBzdW1tYXJ5IG11c3QgMi0zIHBhcmFncmFwaHMgYW5kIGxlc3MgdGhhbiAzMDAgd29yZHMuIENhcHR1cmUgdGhlIG1haW4gcG9pbnRzLiBXcml0ZSBzdWNjaW5jdGx5LCBubyBuZWVkIHRvIGhhdmUgY29tcGxldGUgc2VudGVuY2VzIG9yIGdvb2QgZ3JhbW1hci4gVGhpcyB3aWxsIGJlIGNvbnN1bWVkIGJ5IHNvbWVvbmUgc3ludGhlc2l6aW5nIGEgcmVwb3J0LCBzbyBpdHMgdml0YWwgeW91IGNhcHR1cmUgdGhlIGVzc2VuY2UgYW5kIGlnbm9yZSBhbnkgZmx1ZmYuIERvIG5vdCBpbmNsdWRlIGFueSBhZGRpdGlvbmFsIGNvbW1lbnRhcnkgb3RoZXIgdGhhbiB0aGUgc3VtbWFyeSBpdHNlbGYuIiwiaW5wdXQiOlt7ImNvbnRlbnQiOiJTZWFyY2ggdGVybTogQ2FyaWJiZWFuIGFkdmVudHVyZSB2YWNhdGlvbiByZXZpZXdzIEFwcmlsXG5SZWFzb24gZm9yIHNlYXJjaGluZzogVG8gcmVhZCByZXZpZXdzIGZyb20gdHJhdmVsZXJzIHdobyB2aXNpdGVkIHRoZSBDYXJpYmJlYW4gZm9yIGFjdGl2ZSB2YWNhdGlvbnMgaW4gQXByaWwuIiwicm9sZSI6InVzZXIifV0sIm1vZGVsX3NldHRpbmdzIjp7InRlbXBlcmF0dXJlIjpudWxsLCJ0b3BfcCI6bnVsbCwiZnJlcXVlbmN5X3BlbmFsdHkiOm51bGwsInByZXNlbmNlX3BlbmFsdHkiOm51bGwsInRvb2xfY2hvaWNlIjoicmVxdWlyZWQiLCJwYXJhbGxlbF90b29sX2NhbGxzIjpudWxsLCJ0cnVuY2F0aW9uIjpudWxsLCJtYXhfdG9rZW5zIjpudWxsLCJyZWFzb25pbmciOm51bGwsIm1ldGFkYXRhIjpudWxsLCJzdG9yZSI6bnVsbCwiaW5jbHVkZV91c2FnZSI6bnVsbCwiZXh0cmFfcXVlcnkiOm51bGwsImV4dHJhX2JvZHkiOm51bGwsImV4dHJhX2hlYWRlcnMiOm51bGwsImV4dHJhX2FyZ3MiOm51bGx9LCJ0b29scyI6W3sidXNlcl9sb2NhdGlvbiI6bnVsbCwic2VhcmNoX2NvbnRleHRfc2l6ZSI6Im1lZGl1bSJ9XSwib3V0cHV0X3NjaGVtYSI6bnVsbCwiaGFuZG9mZnMiOltdLCJ0cmFjaW5nIjoxLCJwcmV2aW91c19yZXNwb25zZV9pZCI6bnVsbCwicHJvbXB0IjpudWxsfQ==" - } - ] - }, - "scheduleToCloseTimeout": "120s", - "scheduleToStartTimeout": "120s", - "startToCloseTimeout": "120s", - "heartbeatTimeout": "0s", - "workflowTaskCompletedEventId": "10", - "retryPolicy": { - "initialInterval": "1s", - "backoffCoefficient": 2.0, - "maximumInterval": "100s" - }, - "useWorkflowBuildId": true - }, - "userMetadata": { - "summary": { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "IlNlYXJjaCB0ZXJtOiBDYXJpYmJlYW4gYWR2ZW50dXJlIHZhY2F0aW9uIHJldmlld3MgQXByaWxcblJlYXNvbiBmb3Igc2VhcmNoaW5nOiBUbyByZWFkIHJldmlld3MgZnJvbSB0Ig==" - } - } - }, - { - "eventId": "19", - "eventTime": "2025-06-30T17:04:08.052356Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_SCHEDULED", - "taskId": "1057081", - "activityTaskScheduledEventAttributes": { - "activityId": "10", - "activityType": { - "name": "invoke_model_activity" - }, - "taskQueue": { - "name": "fc97ba3c-6c32-4f4c-a986-8d7f4f5c7f80", - "kind": "TASK_QUEUE_KIND_NORMAL" - }, - "header": { - "fields": { - "__openai_span": { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJ0cmFjZU5hbWUiOiJSZXNlYXJjaCB0cmFjZSIsInNwYW5JZCI6InNwYW5fZGY5YjBmMTc5ZDgwNDZhODk1NGE2MmI4IiwidHJhY2VJZCI6InRyYWNlXzUyYjBiNjQ1MjNmMDQ3ZDc5MTI2YTc2OTY5ZGIzMTllIn0=" - } - } - }, - "input": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJtb2RlbF9uYW1lIjpudWxsLCJzeXN0ZW1faW5zdHJ1Y3Rpb25zIjoiWW91IGFyZSBhIHJlc2VhcmNoIGFzc2lzdGFudC4gR2l2ZW4gYSBzZWFyY2ggdGVybSwgeW91IHNlYXJjaCB0aGUgd2ViIGZvciB0aGF0IHRlcm0gYW5kIHByb2R1Y2UgYSBjb25jaXNlIHN1bW1hcnkgb2YgdGhlIHJlc3VsdHMuIFRoZSBzdW1tYXJ5IG11c3QgMi0zIHBhcmFncmFwaHMgYW5kIGxlc3MgdGhhbiAzMDAgd29yZHMuIENhcHR1cmUgdGhlIG1haW4gcG9pbnRzLiBXcml0ZSBzdWNjaW5jdGx5LCBubyBuZWVkIHRvIGhhdmUgY29tcGxldGUgc2VudGVuY2VzIG9yIGdvb2QgZ3JhbW1hci4gVGhpcyB3aWxsIGJlIGNvbnN1bWVkIGJ5IHNvbWVvbmUgc3ludGhlc2l6aW5nIGEgcmVwb3J0LCBzbyBpdHMgdml0YWwgeW91IGNhcHR1cmUgdGhlIGVzc2VuY2UgYW5kIGlnbm9yZSBhbnkgZmx1ZmYuIERvIG5vdCBpbmNsdWRlIGFueSBhZGRpdGlvbmFsIGNvbW1lbnRhcnkgb3RoZXIgdGhhbiB0aGUgc3VtbWFyeSBpdHNlbGYuIiwiaW5wdXQiOlt7ImNvbnRlbnQiOiJTZWFyY2ggdGVybTogd2F0ZXIgc3BvcnRzIGdlYXIgcmVudGFscyBDYXJpYmJlYW4gQXByaWxcblJlYXNvbiBmb3Igc2VhcmNoaW5nOiBUbyBmaW5kIGluZm9ybWF0aW9uIG9uIHJlbnRhbCBlcXVpcG1lbnQgYXZhaWxhYmlsaXR5IGZvciB3YXRlciBzcG9ydHMgYW5kIGhpa2luZyBpbiB0aGUgQ2FyaWJiZWFuLiIsInJvbGUiOiJ1c2VyIn1dLCJtb2RlbF9zZXR0aW5ncyI6eyJ0ZW1wZXJhdHVyZSI6bnVsbCwidG9wX3AiOm51bGwsImZyZXF1ZW5jeV9wZW5hbHR5IjpudWxsLCJwcmVzZW5jZV9wZW5hbHR5IjpudWxsLCJ0b29sX2Nob2ljZSI6InJlcXVpcmVkIiwicGFyYWxsZWxfdG9vbF9jYWxscyI6bnVsbCwidHJ1bmNhdGlvbiI6bnVsbCwibWF4X3Rva2VucyI6bnVsbCwicmVhc29uaW5nIjpudWxsLCJtZXRhZGF0YSI6bnVsbCwic3RvcmUiOm51bGwsImluY2x1ZGVfdXNhZ2UiOm51bGwsImV4dHJhX3F1ZXJ5IjpudWxsLCJleHRyYV9ib2R5IjpudWxsLCJleHRyYV9oZWFkZXJzIjpudWxsLCJleHRyYV9hcmdzIjpudWxsfSwidG9vbHMiOlt7InVzZXJfbG9jYXRpb24iOm51bGwsInNlYXJjaF9jb250ZXh0X3NpemUiOiJtZWRpdW0ifV0sIm91dHB1dF9zY2hlbWEiOm51bGwsImhhbmRvZmZzIjpbXSwidHJhY2luZyI6MSwicHJldmlvdXNfcmVzcG9uc2VfaWQiOm51bGwsInByb21wdCI6bnVsbH0=" - } - ] - }, - "scheduleToCloseTimeout": "120s", - "scheduleToStartTimeout": "120s", - "startToCloseTimeout": "120s", - "heartbeatTimeout": "0s", - "workflowTaskCompletedEventId": "10", - "retryPolicy": { - "initialInterval": "1s", - "backoffCoefficient": 2.0, - "maximumInterval": "100s" - }, - "useWorkflowBuildId": true - }, - "userMetadata": { - "summary": { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "IlNlYXJjaCB0ZXJtOiB3YXRlciBzcG9ydHMgZ2VhciByZW50YWxzIENhcmliYmVhbiBBcHJpbFxuUmVhc29uIGZvciBzZWFyY2hpbmc6IFRvIGZpbmQgaW5mb3JtYXRpb24gb24gIg==" - } - } - }, - { - "eventId": "20", - "eventTime": "2025-06-30T17:04:08.052364Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_SCHEDULED", - "taskId": "1057082", - "activityTaskScheduledEventAttributes": { - "activityId": "11", - "activityType": { - "name": "invoke_model_activity" - }, - "taskQueue": { - "name": "fc97ba3c-6c32-4f4c-a986-8d7f4f5c7f80", - "kind": "TASK_QUEUE_KIND_NORMAL" - }, - "header": { - "fields": { - "__openai_span": { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJ0cmFjZU5hbWUiOiJSZXNlYXJjaCB0cmFjZSIsInNwYW5JZCI6InNwYW5fZmQ5MDM0YzhmYjEzNDM3NmI5NTFiNTQxIiwidHJhY2VJZCI6InRyYWNlXzUyYjBiNjQ1MjNmMDQ3ZDc5MTI2YTc2OTY5ZGIzMTllIn0=" - } - } - }, - "input": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJtb2RlbF9uYW1lIjpudWxsLCJzeXN0ZW1faW5zdHJ1Y3Rpb25zIjoiWW91IGFyZSBhIHJlc2VhcmNoIGFzc2lzdGFudC4gR2l2ZW4gYSBzZWFyY2ggdGVybSwgeW91IHNlYXJjaCB0aGUgd2ViIGZvciB0aGF0IHRlcm0gYW5kIHByb2R1Y2UgYSBjb25jaXNlIHN1bW1hcnkgb2YgdGhlIHJlc3VsdHMuIFRoZSBzdW1tYXJ5IG11c3QgMi0zIHBhcmFncmFwaHMgYW5kIGxlc3MgdGhhbiAzMDAgd29yZHMuIENhcHR1cmUgdGhlIG1haW4gcG9pbnRzLiBXcml0ZSBzdWNjaW5jdGx5LCBubyBuZWVkIHRvIGhhdmUgY29tcGxldGUgc2VudGVuY2VzIG9yIGdvb2QgZ3JhbW1hci4gVGhpcyB3aWxsIGJlIGNvbnN1bWVkIGJ5IHNvbWVvbmUgc3ludGhlc2l6aW5nIGEgcmVwb3J0LCBzbyBpdHMgdml0YWwgeW91IGNhcHR1cmUgdGhlIGVzc2VuY2UgYW5kIGlnbm9yZSBhbnkgZmx1ZmYuIERvIG5vdCBpbmNsdWRlIGFueSBhZGRpdGlvbmFsIGNvbW1lbnRhcnkgb3RoZXIgdGhhbiB0aGUgc3VtbWFyeSBpdHNlbGYuIiwiaW5wdXQiOlt7ImNvbnRlbnQiOiJTZWFyY2ggdGVybTogQ2FyaWJiZWFuIHN1cmZpbmcgZmVzdGl2YWxzIEFwcmlsXG5SZWFzb24gZm9yIHNlYXJjaGluZzogVG8gY2hlY2sgZm9yIGFueSBmZXN0aXZhbHMgb3IgZXZlbnRzIHJlbGF0ZWQgdG8gc3VyZmluZyBvciB3YXRlciBzcG9ydHMgaW4gdGhlIENhcmliYmVhbiBpbiBBcHJpbC4iLCJyb2xlIjoidXNlciJ9XSwibW9kZWxfc2V0dGluZ3MiOnsidGVtcGVyYXR1cmUiOm51bGwsInRvcF9wIjpudWxsLCJmcmVxdWVuY3lfcGVuYWx0eSI6bnVsbCwicHJlc2VuY2VfcGVuYWx0eSI6bnVsbCwidG9vbF9jaG9pY2UiOiJyZXF1aXJlZCIsInBhcmFsbGVsX3Rvb2xfY2FsbHMiOm51bGwsInRydW5jYXRpb24iOm51bGwsIm1heF90b2tlbnMiOm51bGwsInJlYXNvbmluZyI6bnVsbCwibWV0YWRhdGEiOm51bGwsInN0b3JlIjpudWxsLCJpbmNsdWRlX3VzYWdlIjpudWxsLCJleHRyYV9xdWVyeSI6bnVsbCwiZXh0cmFfYm9keSI6bnVsbCwiZXh0cmFfaGVhZGVycyI6bnVsbCwiZXh0cmFfYXJncyI6bnVsbH0sInRvb2xzIjpbeyJ1c2VyX2xvY2F0aW9uIjpudWxsLCJzZWFyY2hfY29udGV4dF9zaXplIjoibWVkaXVtIn1dLCJvdXRwdXRfc2NoZW1hIjpudWxsLCJoYW5kb2ZmcyI6W10sInRyYWNpbmciOjEsInByZXZpb3VzX3Jlc3BvbnNlX2lkIjpudWxsLCJwcm9tcHQiOm51bGx9" - } - ] - }, - "scheduleToCloseTimeout": "120s", - "scheduleToStartTimeout": "120s", - "startToCloseTimeout": "120s", - "heartbeatTimeout": "0s", - "workflowTaskCompletedEventId": "10", - "retryPolicy": { - "initialInterval": "1s", - "backoffCoefficient": 2.0, - "maximumInterval": "100s" - }, - "useWorkflowBuildId": true - }, - "userMetadata": { - "summary": { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "IlNlYXJjaCB0ZXJtOiBDYXJpYmJlYW4gc3VyZmluZyBmZXN0aXZhbHMgQXByaWxcblJlYXNvbiBmb3Igc2VhcmNoaW5nOiBUbyBjaGVjayBmb3IgYW55IGZlc3RpdmFscyBvciBlIg==" - } - } - }, - { - "eventId": "21", - "eventTime": "2025-06-30T17:04:08.055354Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_STARTED", - "taskId": "1057104", - "activityTaskStartedEventAttributes": { - "scheduledEventId": "13", - "identity": "19167@Tims-MacBook-Pro.local", - "requestId": "d557b03c-0aa2-472d-81ae-a52c689c73ac", - "attempt": 1, - "workerVersion": { - "buildId": "9f32b5d9789ece25bfb8515a7023ec78" - } - } - }, - { - "eventId": "22", - "eventTime": "2025-06-30T17:04:13.729426Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_COMPLETED", - "taskId": "1057105", - "activityTaskCompletedEventAttributes": { - "result": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJvdXRwdXQiOlt7ImlkIjoid3NfNjg2MmMzODg1NzQwODE5ZGIxYzZiYmQ0ZTBhYjE3NzQwYmNhMmE4M2UyZjlmMjE0IiwiYWN0aW9uIjp7InF1ZXJ5IjoiU2VhcmNoIHRlcm06IHRvcCBDYXJpYmJlYW4gd2F0ZXIgc3BvcnRzIGluIEFwcmlsXFxuUmVhc29uIGZvciBzZWFyY2hpbmc6IFRvIGV4cGxvcmUgZ2VuZXJhbCBDYXJpYmJlYW4gd2F0ZXIgc3BvcnRzIG9wcG9ydHVuaXRpZXMgYXZhaWxhYmxlIGluIEFwcmlsLiIsInR5cGUiOiJzZWFyY2gifSwic3RhdHVzIjoiY29tcGxldGVkIiwidHlwZSI6IndlYl9zZWFyY2hfY2FsbCJ9LHsiaWQiOiJtc2dfNjg2MmMzOGI3OWUwODE5ZGFmMTEwNmNmZjQ0NTIzZDQwYmNhMmE4M2UyZjlmMjE0IiwiY29udGVudCI6W3siYW5ub3RhdGlvbnMiOlt7ImVuZF9pbmRleCI6MzgyLCJzdGFydF9pbmRleCI6Mjc1LCJ0aXRsZSI6IkNhcmliYmVhbiBXYXRlcnNwb3J0cyBIb2xpZGF5cyBHdWlkZSAtIEVsaXRlIElzbGFuZCBSZXNvcnRzIiwidHlwZSI6InVybF9jaXRhdGlvbiIsInVybCI6Imh0dHBzOi8vZWxpdGVpc2xhbmRob2xpZGF5cy5jb20vY2FyaWJiZWFuLXdhdGVyc3BvcnRzLWd1aWRlLz91dG1fc291cmNlPW9wZW5haSJ9LHsiZW5kX2luZGV4Ijo3OTgsInN0YXJ0X2luZGV4Ijo2OTEsInRpdGxlIjoiQ2FyaWJiZWFuIFdhdGVyc3BvcnRzIEhvbGlkYXlzIEd1aWRlIC0gRWxpdGUgSXNsYW5kIFJlc29ydHMiLCJ0eXBlIjoidXJsX2NpdGF0aW9uIiwidXJsIjoiaHR0cHM6Ly9lbGl0ZWlzbGFuZGhvbGlkYXlzLmNvbS9jYXJpYmJlYW4td2F0ZXJzcG9ydHMtZ3VpZGUvP3V0bV9zb3VyY2U9b3BlbmFpIn0seyJlbmRfaW5kZXgiOjExNTEsInN0YXJ0X2luZGV4IjoxMDQ0LCJ0aXRsZSI6IkNhcmliYmVhbiBXYXRlcnNwb3J0cyBIb2xpZGF5cyBHdWlkZSAtIEVsaXRlIElzbGFuZCBSZXNvcnRzIiwidHlwZSI6InVybF9jaXRhdGlvbiIsInVybCI6Imh0dHBzOi8vZWxpdGVpc2xhbmRob2xpZGF5cy5jb20vY2FyaWJiZWFuLXdhdGVyc3BvcnRzLWd1aWRlLz91dG1fc291cmNlPW9wZW5haSJ9XSwidGV4dCI6IkluIEFwcmlsLCB0aGUgQ2FyaWJiZWFuIG9mZmVycyBhIHZhcmlldHkgb2Ygd2F0ZXIgc3BvcnRzIHN1aXRhYmxlIGZvciBlbnRodXNpYXN0cyBvZiBhbGwgbGV2ZWxzLiBTdXJmaW5nIGlzIHBhcnRpY3VsYXJseSBwb3B1bGFyIGR1cmluZyB0aGlzIG1vbnRoLCBhcyBpdCBmYWxscyB3aXRoaW4gdGhlIHBlYWsgc2Vhc29uIGZyb20gTm92ZW1iZXIgdG8gQXByaWwsIHByb3ZpZGluZyBjb25zaXN0ZW50IHdhdmVzIGZvciBib3RoIHNlYXNvbmVkIHN1cmZlcnMgYW5kIGJlZ2lubmVycy4gKFtlbGl0ZWlzbGFuZGhvbGlkYXlzLmNvbV0oaHR0cHM6Ly9lbGl0ZWlzbGFuZGhvbGlkYXlzLmNvbS9jYXJpYmJlYW4td2F0ZXJzcG9ydHMtZ3VpZGUvP3V0bV9zb3VyY2U9b3BlbmFpKSlcblxuS2l0ZXN1cmZpbmcgaXMgYW5vdGhlciBmYXZvcmVkIGFjdGl2aXR5IGFjcm9zcyB0aGUgQ2FyaWJiZWFuIGlzbGFuZHMgaW4gQXByaWwsIHRoYW5rcyB0byBzdGVhZHkgd2luZHMgYW5kIGlkZWFsIHdhdmUgY29uZGl0aW9ucywgbWFraW5nIGl0IGFjY2Vzc2libGUgZm9yIGJvdGggbm92aWNlcyBhbmQgZXhwZXJ0cy4gU25vcmtlbGluZyBhbHNvIHRocml2ZXMgZHVyaW5nIHRoaXMgcGVyaW9kLCB3aXRoIGNsZWFyIHdhdGVycyBhbmQgYWJ1bmRhbnQgbWFyaW5lIGxpZmUgb2ZmZXJpbmcgdW5pcXVlIHVuZGVyd2F0ZXIgZXhwZXJpZW5jZXMuIChbZWxpdGVpc2xhbmRob2xpZGF5cy5jb21dKGh0dHBzOi8vZWxpdGVpc2xhbmRob2xpZGF5cy5jb20vY2FyaWJiZWFuLXdhdGVyc3BvcnRzLWd1aWRlLz91dG1fc291cmNlPW9wZW5haSkpXG5cblNhaWxpbmcgcmVtYWlucyBhIHRpbWVsZXNzIHB1cnN1aXQgaW4gdGhlIENhcmliYmVhbiwgd2l0aCBBcHJpbCdzIGZhdm9yYWJsZSB3ZWF0aGVyIGNvbmRpdGlvbnMgZW5oYW5jaW5nIHRoZSBleHBlcmllbmNlLiBXaGV0aGVyIHlvdSdyZSBuYXZpZ2F0aW5nIHRoZSBzZWFzIHdpdGggZmFtaWx5IG9yIGZyaWVuZHMsIHRoZSBwb3NzaWJpbGl0eSBvZiBlbmNvdW50ZXJpbmcgZG9scGhpbnMgYWRkcyB0byB0aGUgYWR2ZW50dXJlLiAoW2VsaXRlaXNsYW5kaG9saWRheXMuY29tXShodHRwczovL2VsaXRlaXNsYW5kaG9saWRheXMuY29tL2NhcmliYmVhbi13YXRlcnNwb3J0cy1ndWlkZS8/dXRtX3NvdXJjZT1vcGVuYWkpKVxuXG5PdmVyYWxsLCBBcHJpbCdzIGNsaW1hdGUgaW4gdGhlIENhcmliYmVhbiBpcyBjb25kdWNpdmUgdG8gYSB3aWRlIHJhbmdlIG9mIHdhdGVyIHNwb3J0cywgZW5zdXJpbmcgbWVtb3JhYmxlIGV4cGVyaWVuY2VzIGZvciB2aXNpdG9ycy4gIiwidHlwZSI6Im91dHB1dF90ZXh0IiwibG9ncHJvYnMiOltdfV0sInJvbGUiOiJhc3Npc3RhbnQiLCJzdGF0dXMiOiJjb21wbGV0ZWQiLCJ0eXBlIjoibWVzc2FnZSJ9XSwidXNhZ2UiOnsicmVxdWVzdHMiOjEsImlucHV0X3Rva2VucyI6NDM5LCJpbnB1dF90b2tlbnNfZGV0YWlscyI6eyJjYWNoZWRfdG9rZW5zIjowfSwib3V0cHV0X3Rva2VucyI6MjUyLCJvdXRwdXRfdG9rZW5zX2RldGFpbHMiOnsicmVhc29uaW5nX3Rva2VucyI6MH0sInRvdGFsX3Rva2VucyI6NjkxfSwicmVzcG9uc2VfaWQiOiJyZXNwXzY4NjJjMzg4NDYzMDgxOWQ5NDFlZjU4YzhiNWVjN2ZiMGJjYTJhODNlMmY5ZjIxNCJ9" - } - ] - }, - "scheduledEventId": "13", - "startedEventId": "21", - "identity": "19167@Tims-MacBook-Pro.local" - } - }, - { - "eventId": "23", - "eventTime": "2025-06-30T17:04:13.729436Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", - "taskId": "1057106", - "workflowTaskScheduledEventAttributes": { - "taskQueue": { - "name": "19167@Tims-MacBook-Pro.local-f783ce7516bc48e59108b7b5cd31d0e5", - "kind": "TASK_QUEUE_KIND_STICKY", - "normalName": "fc97ba3c-6c32-4f4c-a986-8d7f4f5c7f80" - }, - "startToCloseTimeout": "10s", - "attempt": 1 - } - }, - { - "eventId": "24", - "eventTime": "2025-06-30T17:04:13.732404Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", - "taskId": "1057110", - "workflowTaskStartedEventAttributes": { - "scheduledEventId": "23", - "identity": "19167@Tims-MacBook-Pro.local", - "requestId": "fd7235cd-3500-4ffa-8a6e-4296483f9fd7", - "historySizeBytes": "24914", - "workerVersion": { - "buildId": "9f32b5d9789ece25bfb8515a7023ec78" - } - } - }, - { - "eventId": "25", - "eventTime": "2025-06-30T17:04:13.755190Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", - "taskId": "1057114", - "workflowTaskCompletedEventAttributes": { - "scheduledEventId": "23", - "startedEventId": "24", - "identity": "19167@Tims-MacBook-Pro.local", - "workerVersion": { - "buildId": "9f32b5d9789ece25bfb8515a7023ec78" - }, - "sdkMetadata": {}, - "meteringMetadata": {} - } - }, - { - "eventId": "26", - "eventTime": "2025-06-30T17:04:08.053707Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_STARTED", - "taskId": "1057116", - "activityTaskStartedEventAttributes": { - "scheduledEventId": "20", - "identity": "19167@Tims-MacBook-Pro.local", - "requestId": "fd2ccba8-5cd9-481d-93c6-3494d352620e", - "attempt": 1, - "workerVersion": { - "buildId": "9f32b5d9789ece25bfb8515a7023ec78" - } - } - }, - { - "eventId": "27", - "eventTime": "2025-06-30T17:04:14.229949Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_COMPLETED", - "taskId": "1057117", - "activityTaskCompletedEventAttributes": { - "result": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJvdXRwdXQiOlt7ImlkIjoid3NfNjg2MmMzODgzZmMwODFhMGFmMDgxMDIyNmNlZjA4ODMwYmNhYzI3NGQwYWNmNDIxIiwiYWN0aW9uIjp7InF1ZXJ5IjoiU2VhcmNoIHRlcm06IENhcmliYmVhbiBzdXJmaW5nIGZlc3RpdmFscyBBcHJpbFxcblJlYXNvbiBmb3Igc2VhcmNoaW5nOiBUbyBjaGVjayBmb3IgYW55IGZlc3RpdmFscyBvciBldmVudHMgcmVsYXRlZCB0byBzdXJmaW5nIG9yIHdhdGVyIHNwb3J0cyBpbiB0aGUgQ2FyaWJiZWFuIGluIEFwcmlsLiIsInR5cGUiOiJzZWFyY2gifSwic3RhdHVzIjoiY29tcGxldGVkIiwidHlwZSI6IndlYl9zZWFyY2hfY2FsbCJ9LHsiaWQiOiJtc2dfNjg2MmMzOGI4MDA0ODFhMDk2YjQ0MmNjYjNhMTcwODYwYmNhYzI3NGQwYWNmNDIxIiwiY29udGVudCI6W3siYW5ub3RhdGlvbnMiOltdLCJ0ZXh0IjoiSW4gQXByaWwsIHRoZSBDYXJpYmJlYW4gaG9zdHMgc2V2ZXJhbCBub3RhYmxlIHN1cmZpbmcgYW5kIHdhdGVyIHNwb3J0cyBldmVudHMuIFRoZSBCYXJiYWRvcyBTdXJmIFBybywgcGFydCBvZiB0aGUgV29ybGQgU3VyZiBMZWFndWUncyBRdWFsaWZ5aW5nIFNlcmllcywgdHlwaWNhbGx5IHRha2VzIHBsYWNlIGluIEFwcmlsIGF0IERyaWxsIEhhbGwgQmVhY2gsIGF0dHJhY3RpbmcgaW50ZXJuYXRpb25hbCBzdXJmZXJzLiBTaW1pbGFybHksIHRoZSBNYXJ0aW5pcXVlIFN1cmYgUHJvLCBhbm90aGVyIFdTTCBRdWFsaWZ5aW5nIFNlcmllcyBldmVudCwgaXMgdXN1YWxseSBoZWxkIGluIEFwcmlsIGF0IEJhc3NlLVBvaW50ZSwgZmVhdHVyaW5nIGJvdGggbWVuJ3MgYW5kIHdvbWVuJ3MgY29tcGV0aXRpb25zLiBBZGRpdGlvbmFsbHksIHRoZSBDYXJpYmJlYW4gU3VyZiBOZXR3b3JrIG9mdGVuIG9yZ2FuaXplcyByZWdpb25hbCBjb21wZXRpdGlvbnMgZHVyaW5nIHRoaXMgbW9udGgsIHRob3VnaCBzcGVjaWZpYyBkYXRlcyBhbmQgbG9jYXRpb25zIGNhbiB2YXJ5IGFubnVhbGx5LlxuXG5CZXlvbmQgc3VyZmluZywgdGhlIENhcmliYmVhbiBvZmZlcnMgdmFyaW91cyB3YXRlciBzcG9ydHMgZmVzdGl2YWxzIGluIEFwcmlsLiBUaGUgU3QuIFRob21hcyBJbnRlcm5hdGlvbmFsIFJlZ2F0dGEgaW4gdGhlIFUuUy4gVmlyZ2luIElzbGFuZHMgaXMgYSBwcm9taW5lbnQgc2FpbGluZyBldmVudCBkcmF3aW5nIHBhcnRpY2lwYW50cyB3b3JsZHdpZGUuIEluIHRoZSBCYWhhbWFzLCB0aGUgTmF0aW9uYWwgRmFtaWx5IElzbGFuZCBSZWdhdHRhIHNob3djYXNlcyB0cmFkaXRpb25hbCBCYWhhbWlhbiBzbG9vcCBzYWlsaW5nLiBUaGVzZSBldmVudHMgaGlnaGxpZ2h0IHRoZSByZWdpb24ncyByaWNoIG1hcml0aW1lIGN1bHR1cmUgYW5kIHByb3ZpZGUgb3Bwb3J0dW5pdGllcyBmb3IgYm90aCBjb21wZXRpdGlvbiBhbmQgY3VsdHVyYWwgZXhjaGFuZ2UuICIsInR5cGUiOiJvdXRwdXRfdGV4dCIsImxvZ3Byb2JzIjpbXX1dLCJyb2xlIjoiYXNzaXN0YW50Iiwic3RhdHVzIjoiY29tcGxldGVkIiwidHlwZSI6Im1lc3NhZ2UifV0sInVzYWdlIjp7InJlcXVlc3RzIjoxLCJpbnB1dF90b2tlbnMiOjQ1MSwiaW5wdXRfdG9rZW5zX2RldGFpbHMiOnsiY2FjaGVkX3Rva2VucyI6MH0sIm91dHB1dF90b2tlbnMiOjE3Niwib3V0cHV0X3Rva2Vuc19kZXRhaWxzIjp7InJlYXNvbmluZ190b2tlbnMiOjB9LCJ0b3RhbF90b2tlbnMiOjYyN30sInJlc3BvbnNlX2lkIjoicmVzcF82ODYyYzM4ODMzNjQ4MWEwYjJlMjgyZjk0Y2I3YWVmMTBiY2FjMjc0ZDBhY2Y0MjEifQ==" - } - ] - }, - "scheduledEventId": "20", - "startedEventId": "26", - "identity": "19167@Tims-MacBook-Pro.local" - } - }, - { - "eventId": "28", - "eventTime": "2025-06-30T17:04:14.229965Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", - "taskId": "1057118", - "workflowTaskScheduledEventAttributes": { - "taskQueue": { - "name": "19167@Tims-MacBook-Pro.local-f783ce7516bc48e59108b7b5cd31d0e5", - "kind": "TASK_QUEUE_KIND_STICKY", - "normalName": "fc97ba3c-6c32-4f4c-a986-8d7f4f5c7f80" - }, - "startToCloseTimeout": "10s", - "attempt": 1 - } - }, - { - "eventId": "29", - "eventTime": "2025-06-30T17:04:14.232810Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", - "taskId": "1057122", - "workflowTaskStartedEventAttributes": { - "scheduledEventId": "28", - "identity": "19167@Tims-MacBook-Pro.local", - "requestId": "8d346ab1-ac2a-4ffd-8229-c1cd4d96c851", - "historySizeBytes": "27265", - "workerVersion": { - "buildId": "9f32b5d9789ece25bfb8515a7023ec78" - } - } - }, - { - "eventId": "30", - "eventTime": "2025-06-30T17:04:14.253917Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", - "taskId": "1057126", - "workflowTaskCompletedEventAttributes": { - "scheduledEventId": "28", - "startedEventId": "29", - "identity": "19167@Tims-MacBook-Pro.local", - "workerVersion": { - "buildId": "9f32b5d9789ece25bfb8515a7023ec78" - }, - "sdkMetadata": {}, - "meteringMetadata": {} - } - }, - { - "eventId": "31", - "eventTime": "2025-06-30T17:04:08.056512Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_STARTED", - "taskId": "1057128", - "activityTaskStartedEventAttributes": { - "scheduledEventId": "18", - "identity": "19167@Tims-MacBook-Pro.local", - "requestId": "a22ce3cb-4ed7-44b1-ab43-a0f0ffa81f7f", - "attempt": 1, - "workerVersion": { - "buildId": "9f32b5d9789ece25bfb8515a7023ec78" - } - } - }, - { - "eventId": "32", - "eventTime": "2025-06-30T17:04:14.465038Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_COMPLETED", - "taskId": "1057129", - "activityTaskCompletedEventAttributes": { - "result": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJvdXRwdXQiOlt7ImlkIjoid3NfNjg2MmMzODg1MWU0ODFhMjgxNWQ2ZGUwNWRkZjY3YzMwNGVmY2I0Y2FhMzc4OTIzIiwiYWN0aW9uIjp7InF1ZXJ5IjoiU2VhcmNoIHRlcm06IENhcmliYmVhbiBhZHZlbnR1cmUgdmFjYXRpb24gcmV2aWV3cyBBcHJpbFxcblJlYXNvbiBmb3Igc2VhcmNoaW5nOiBUbyByZWFkIHJldmlld3MgZnJvbSB0cmF2ZWxlcnMgd2hvIHZpc2l0ZWQgdGhlIENhcmliYmVhbiBmb3IgYWN0aXZlIHZhY2F0aW9ucyBpbiBBcHJpbC4iLCJ0eXBlIjoic2VhcmNoIn0sInN0YXR1cyI6ImNvbXBsZXRlZCIsInR5cGUiOiJ3ZWJfc2VhcmNoX2NhbGwifSx7ImlkIjoibXNnXzY4NjJjMzhjNTQ2NDgxYTJhMjUxOWE4NTA3MGQyYzUxMDRlZmNiNGNhYTM3ODkyMyIsImNvbnRlbnQiOlt7ImFubm90YXRpb25zIjpbeyJlbmRfaW5kZXgiOjY3NSwic3RhcnRfaW5kZXgiOjQ3OSwidGl0bGUiOiJIaWdobGlnaHQgb2Ygb3VyIHZhY2F0aW9uICEgLSBSZXZpZXcgb2YgT2NlYW4gQWR2ZW50dXJlcyAtIFNhaWxpbmcgQWR2ZW50dXJlLCBQdW50YSBDYW5hLCBEb21pbmljYW4gUmVwdWJsaWMgLSBUcmlwYWR2aXNvciIsInR5cGUiOiJ1cmxfY2l0YXRpb24iLCJ1cmwiOiJodHRwczovL3d3dy50cmlwYWR2aXNvci5jb20vU2hvd1VzZXJSZXZpZXdzLWcxNDcyOTMtZDI2MjQ3ODktcjIwNjExNDEwNS1PY2Vhbl9BZHZlbnR1cmVzX1NhaWxpbmdfQWR2ZW50dXJlLVB1bnRhX0NhbmFfTGFfQWx0YWdyYWNpYV9Qcm92aW5jZV9Eb21pbmljYW5fUi5odG1sP3V0bV9zb3VyY2U9b3BlbmFpIn0seyJlbmRfaW5kZXgiOjExNzcsInN0YXJ0X2luZGV4Ijo5ODEsInRpdGxlIjoiSGlnaGxpZ2h0IG9mIG91ciB2YWNhdGlvbiAhIC0gUmV2aWV3IG9mIE9jZWFuIEFkdmVudHVyZXMgLSBTYWlsaW5nIEFkdmVudHVyZSwgUHVudGEgQ2FuYSwgRG9taW5pY2FuIFJlcHVibGljIC0gVHJpcGFkdmlzb3IiLCJ0eXBlIjoidXJsX2NpdGF0aW9uIiwidXJsIjoiaHR0cHM6Ly93d3cudHJpcGFkdmlzb3IuY29tL1Nob3dVc2VyUmV2aWV3cy1nMTQ3MjkzLWQyNjI0Nzg5LXIyMDYxMTQxMDUtT2NlYW5fQWR2ZW50dXJlc19TYWlsaW5nX0FkdmVudHVyZS1QdW50YV9DYW5hX0xhX0FsdGFncmFjaWFfUHJvdmluY2VfRG9taW5pY2FuX1IuaHRtbD91dG1fc291cmNlPW9wZW5haSJ9XSwidGV4dCI6IkluIEFwcmlsIDIwMTQsIHRyYXZlbGVycyBoaWdobGlnaHRlZCB0aGUgT2NlYW4gQWR2ZW50dXJlcyBTYWlsaW5nIEFkdmVudHVyZSBpbiBQdW50YSBDYW5hLCBEb21pbmljYW4gUmVwdWJsaWMsIGFzIGEgc3RhbmRvdXQgZXhwZXJpZW5jZS4gT25lIHJldmlld2VyIGRlc2NyaWJlZCBpdCBhcyB0aGUgXCJoaWdobGlnaHQgb2Ygb3VyIHRyaXAsXCIgcHJhaXNpbmcgdGhlIGNyZXcncyBob3NwaXRhbGl0eSBhbmQgdGhlIHZhcmlldHkgb2YgYWN0aXZpdGllcywgaW5jbHVkaW5nIHNub3JrZWxpbmcsIHdhdGVyIHZvbGxleWJhbGwgaW4gYSBuYXR1cmFsIHBvb2wsIGRhbmNpbmcgZW50ZXJ0YWlubWVudCwgYW5kIGEgdGFzdHkgbHVuY2guIFRoZXkgYXBwcmVjaWF0ZWQgdGhhdCB0aGUgZXhjdXJzaW9uIG9mZmVyZWQgbW9yZSB0aGFuIGp1c3QgZHJpbmtpbmcgYW5kIHNpZ2h0c2VlaW5nLCBwcm92aWRpbmcgYSB3ZWxsLXJvdW5kZWQgYWR2ZW50dXJlLiAoW3RyaXBhZHZpc29yLmNvbV0oaHR0cHM6Ly93d3cudHJpcGFkdmlzb3IuY29tL1Nob3dVc2VyUmV2aWV3cy1nMTQ3MjkzLWQyNjI0Nzg5LXIyMDYxMTQxMDUtT2NlYW5fQWR2ZW50dXJlc19TYWlsaW5nX0FkdmVudHVyZS1QdW50YV9DYW5hX0xhX0FsdGFncmFjaWFfUHJvdmluY2VfRG9taW5pY2FuX1IuaHRtbD91dG1fc291cmNlPW9wZW5haSkpXG5cbkFub3RoZXIgdHJhdmVsZXIsIHdobyBoYWQgcHJldmlvdXNseSBiZWVuIG9uIGEgc2ltaWxhciBjcnVpc2UgaW4gUHVudGEgQ2FuYSwgYWxzbyBoYWQgYSBncmVhdCB0aW1lIG9uIHRoaXMgc2FpbGluZyBhZHZlbnR1cmUuIFRoZXNlIHJldmlld3Mgc3VnZ2VzdCB0aGF0IHRoZSBPY2VhbiBBZHZlbnR1cmVzIFNhaWxpbmcgQWR2ZW50dXJlIG9mZmVycyBhIG1lbW9yYWJsZSBhbmQgZW5nYWdpbmcgZXhwZXJpZW5jZSBmb3IgdmlzaXRvcnMgc2Vla2luZyBhY3RpdmUgdmFjYXRpb25zIGluIHRoZSBDYXJpYmJlYW4gZHVyaW5nIEFwcmlsLiAoW3RyaXBhZHZpc29yLmNvbV0oaHR0cHM6Ly93d3cudHJpcGFkdmlzb3IuY29tL1Nob3dVc2VyUmV2aWV3cy1nMTQ3MjkzLWQyNjI0Nzg5LXIyMDYxMTQxMDUtT2NlYW5fQWR2ZW50dXJlc19TYWlsaW5nX0FkdmVudHVyZS1QdW50YV9DYW5hX0xhX0FsdGFncmFjaWFfUHJvdmluY2VfRG9taW5pY2FuX1IuaHRtbD91dG1fc291cmNlPW9wZW5haSkpICIsInR5cGUiOiJvdXRwdXRfdGV4dCIsImxvZ3Byb2JzIjpbXX1dLCJyb2xlIjoiYXNzaXN0YW50Iiwic3RhdHVzIjoiY29tcGxldGVkIiwidHlwZSI6Im1lc3NhZ2UifV0sInVzYWdlIjp7InJlcXVlc3RzIjoxLCJpbnB1dF90b2tlbnMiOjQ0NSwiaW5wdXRfdG9rZW5zX2RldGFpbHMiOnsiY2FjaGVkX3Rva2VucyI6MH0sIm91dHB1dF90b2tlbnMiOjI1NSwib3V0cHV0X3Rva2Vuc19kZXRhaWxzIjp7InJlYXNvbmluZ190b2tlbnMiOjB9LCJ0b3RhbF90b2tlbnMiOjcwMH0sInJlc3BvbnNlX2lkIjoicmVzcF82ODYyYzM4ODQyOTg4MWEyYTA1ZWRjY2E4ZWVlZGNlYjA0ZWZjYjRjYWEzNzg5MjMifQ==" - } - ] - }, - "scheduledEventId": "18", - "startedEventId": "31", - "identity": "19167@Tims-MacBook-Pro.local" - } - }, - { - "eventId": "33", - "eventTime": "2025-06-30T17:04:14.465051Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", - "taskId": "1057130", - "workflowTaskScheduledEventAttributes": { - "taskQueue": { - "name": "19167@Tims-MacBook-Pro.local-f783ce7516bc48e59108b7b5cd31d0e5", - "kind": "TASK_QUEUE_KIND_STICKY", - "normalName": "fc97ba3c-6c32-4f4c-a986-8d7f4f5c7f80" - }, - "startToCloseTimeout": "10s", - "attempt": 1 - } - }, - { - "eventId": "34", - "eventTime": "2025-06-30T17:04:14.468099Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", - "taskId": "1057134", - "workflowTaskStartedEventAttributes": { - "scheduledEventId": "33", - "identity": "19167@Tims-MacBook-Pro.local", - "requestId": "4c859afe-32a8-43d9-a119-762c5395ed83", - "historySizeBytes": "30561", - "workerVersion": { - "buildId": "9f32b5d9789ece25bfb8515a7023ec78" - } - } - }, - { - "eventId": "35", - "eventTime": "2025-06-30T17:04:14.491467Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", - "taskId": "1057138", - "workflowTaskCompletedEventAttributes": { - "scheduledEventId": "33", - "startedEventId": "34", - "identity": "19167@Tims-MacBook-Pro.local", - "workerVersion": { - "buildId": "9f32b5d9789ece25bfb8515a7023ec78" - }, - "sdkMetadata": {}, - "meteringMetadata": {} - } - }, - { - "eventId": "36", - "eventTime": "2025-06-30T17:04:08.057968Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_STARTED", - "taskId": "1057140", - "activityTaskStartedEventAttributes": { - "scheduledEventId": "11", - "identity": "19167@Tims-MacBook-Pro.local", - "requestId": "47181d54-67b5-45e1-a8ab-1dc4ef8efe5f", - "attempt": 1, - "workerVersion": { - "buildId": "9f32b5d9789ece25bfb8515a7023ec78" - } - } - }, - { - "eventId": "37", - "eventTime": "2025-06-30T17:04:14.630040Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_COMPLETED", - "taskId": "1057141", - "activityTaskCompletedEventAttributes": { - "result": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJvdXRwdXQiOlt7ImlkIjoid3NfNjg2MmMzODg1YmFjODFhMThiMWFmNTg0MzFhZTdhNmQwMmJlNGVkODIyYmZlMTY2IiwiYWN0aW9uIjp7InF1ZXJ5IjoiU2VhcmNoIHRlcm06IGJlc3QgQ2FyaWJiZWFuIHN1cmZpbmcgc3BvdHMgaW4gQXByaWxcXG5SZWFzb24gZm9yIHNlYXJjaGluZzogVG8gZmluZCBkZXN0aW5hdGlvbnMgaW4gdGhlIENhcmliYmVhbiB0aGF0IGFyZSBpZGVhbCBmb3Igc3VyZmluZyBpbiBBcHJpbC4iLCJ0eXBlIjoic2VhcmNoIn0sInN0YXR1cyI6ImNvbXBsZXRlZCIsInR5cGUiOiJ3ZWJfc2VhcmNoX2NhbGwifSx7ImlkIjoibXNnXzY4NjJjMzhjYWExODgxYTE5YWZmZmQ4Njc5MTI1Yzk4MDJiZTRlZDgyMmJmZTE2NiIsImNvbnRlbnQiOlt7ImFubm90YXRpb25zIjpbXSwidGV4dCI6IkFwcmlsIGlzIGFuIGV4Y2VsbGVudCBtb250aCBmb3Igc3VyZmluZyBpbiB0aGUgQ2FyaWJiZWFuLCB3aXRoIGNvbnNpc3RlbnQgc3dlbGxzIGFuZCBmYXZvcmFibGUgd2VhdGhlciBjb25kaXRpb25zLiBUb3AgZGVzdGluYXRpb25zIGluY2x1ZGUgUmluY29uIGluIFB1ZXJ0byBSaWNvLCBrbm93biBmb3IgaXRzIHZhcmlldHkgb2YgYnJlYWtzIHN1aXRhYmxlIGZvciBhbGwgbGV2ZWxzOyBCYXRoc2hlYmEgaW4gQmFyYmFkb3MsIG9mZmVyaW5nIHBvd2VyZnVsIHdhdmVzIGF0IHRoZSBTb3VwIEJvd2w7IGFuZCBQbGF5YSBFbmN1ZW50cm8gaW4gdGhlIERvbWluaWNhbiBSZXB1YmxpYywgZmVhdHVyaW5nIGNvbnNpc3RlbnQgd2F2ZXMgYW5kIGEgdmlicmFudCBzdXJmIGNvbW11bml0eS4gT3RoZXIgbm90YWJsZSBzcG90cyBhcmUgQWd1YWRpbGxhIGluIFB1ZXJ0byBSaWNvLCB3aXRoIGl0cyBkaXZlcnNlIHN1cmYgYnJlYWtzOyBhbmQgTG9uZyBCYXkgaW4gSmFtYWljYSwgcHJvdmlkaW5nIG1lbGxvdyB3YXZlcyBpZGVhbCBmb3IgYmVnaW5uZXJzLlxuXG5UaGVzZSBsb2NhdGlvbnMgb2ZmZXIgYSBtaXggb2YgcmVlZiBhbmQgYmVhY2ggYnJlYWtzLCBjYXRlcmluZyB0byBzdXJmZXJzIG9mIHZhcnlpbmcgc2tpbGwgbGV2ZWxzLiBUaGUgd2FybSBDYXJpYmJlYW4gd2F0ZXJzIGFuZCBjb25zaXN0ZW50IHN3ZWxscyBkdXJpbmcgQXByaWwgbWFrZSB0aGVzZSBzcG90cyBwYXJ0aWN1bGFybHkgYXBwZWFsaW5nLiBBZGRpdGlvbmFsbHksIHRoZSBsb2NhbCBzdXJmIGN1bHR1cmVzIGFuZCBhbWVuaXRpZXMgZW5oYW5jZSB0aGUgb3ZlcmFsbCBleHBlcmllbmNlIGZvciB2aXNpdGluZyBzdXJmZXJzLiAiLCJ0eXBlIjoib3V0cHV0X3RleHQiLCJsb2dwcm9icyI6W119XSwicm9sZSI6ImFzc2lzdGFudCIsInN0YXR1cyI6ImNvbXBsZXRlZCIsInR5cGUiOiJtZXNzYWdlIn1dLCJ1c2FnZSI6eyJyZXF1ZXN0cyI6MSwiaW5wdXRfdG9rZW5zIjo0NDUsImlucHV0X3Rva2Vuc19kZXRhaWxzIjp7ImNhY2hlZF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5zIjoxNTMsIm91dHB1dF90b2tlbnNfZGV0YWlscyI6eyJyZWFzb25pbmdfdG9rZW5zIjowfSwidG90YWxfdG9rZW5zIjo1OTh9LCJyZXNwb25zZV9pZCI6InJlc3BfNjg2MmMzODg0YTU0ODFhMTk1MWYwZWJiY2Y0MTc3NTAwMmJlNGVkODIyYmZlMTY2In0=" - } - ] - }, - "scheduledEventId": "11", - "startedEventId": "36", - "identity": "19167@Tims-MacBook-Pro.local" - } - }, - { - "eventId": "38", - "eventTime": "2025-06-30T17:04:14.630050Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", - "taskId": "1057142", - "workflowTaskScheduledEventAttributes": { - "taskQueue": { - "name": "19167@Tims-MacBook-Pro.local-f783ce7516bc48e59108b7b5cd31d0e5", - "kind": "TASK_QUEUE_KIND_STICKY", - "normalName": "fc97ba3c-6c32-4f4c-a986-8d7f4f5c7f80" - }, - "startToCloseTimeout": "10s", - "attempt": 1 - } - }, - { - "eventId": "39", - "eventTime": "2025-06-30T17:04:14.632394Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", - "taskId": "1057146", - "workflowTaskStartedEventAttributes": { - "scheduledEventId": "38", - "identity": "19167@Tims-MacBook-Pro.local", - "requestId": "f1cb6087-b8e6-4a50-9593-cd4756a36eb4", - "historySizeBytes": "32756", - "workerVersion": { - "buildId": "9f32b5d9789ece25bfb8515a7023ec78" - } - } - }, - { - "eventId": "40", - "eventTime": "2025-06-30T17:04:14.654900Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", - "taskId": "1057150", - "workflowTaskCompletedEventAttributes": { - "scheduledEventId": "38", - "startedEventId": "39", - "identity": "19167@Tims-MacBook-Pro.local", - "workerVersion": { - "buildId": "9f32b5d9789ece25bfb8515a7023ec78" - }, - "sdkMetadata": {}, - "meteringMetadata": {} - } - }, - { - "eventId": "41", - "eventTime": "2025-06-30T17:04:08.055162Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_STARTED", - "taskId": "1057152", - "activityTaskStartedEventAttributes": { - "scheduledEventId": "15", - "identity": "19167@Tims-MacBook-Pro.local", - "requestId": "71ba1129-bb64-45fd-9e6a-55250b147d40", - "attempt": 1, - "workerVersion": { - "buildId": "9f32b5d9789ece25bfb8515a7023ec78" - } - } - }, - { - "eventId": "42", - "eventTime": "2025-06-30T17:04:14.959509Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_COMPLETED", - "taskId": "1057153", - "activityTaskCompletedEventAttributes": { - "result": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJvdXRwdXQiOlt7ImlkIjoid3NfNjg2MmMzODg1MWRjODFhMmE0NzA4NGU3OGRhZWY4OWQwNjNlNzllZTBiN2I1ODY3IiwiYWN0aW9uIjp7InF1ZXJ5IjoiU2VhcmNoIHRlcm06IENhcmliYmVhbiBhZHZlbnR1cmUgdHJhdmVsIGd1aWRlcyBBcHJpbFxcblJlYXNvbiBmb3Igc2VhcmNoaW5nOiBUbyBmaW5kIHRyYXZlbCBndWlkZXMgZm9jdXNpbmcgb24gb3V0ZG9vciBhZHZlbnR1cmUgaW4gdGhlIENhcmliYmVhbiBkdXJpbmcgQXByaWwuIiwidHlwZSI6InNlYXJjaCJ9LCJzdGF0dXMiOiJjb21wbGV0ZWQiLCJ0eXBlIjoid2ViX3NlYXJjaF9jYWxsIn0seyJpZCI6Im1zZ182ODYyYzM4YmI3M2M4MWEyYWUyOWFmNTgzZDJkOGIyYzA2M2U3OWVlMGI3YjU4NjciLCJjb250ZW50IjpbeyJhbm5vdGF0aW9ucyI6W10sInRleHQiOiJJbiBBcHJpbCwgdGhlIENhcmliYmVhbiBvZmZlcnMgYSBwbGV0aG9yYSBvZiBhZHZlbnR1cmUgYWN0aXZpdGllcyBzdWl0YWJsZSBmb3Igb3V0ZG9vciBlbnRodXNpYXN0cy4gVGhlIHJlZ2lvbidzIHdhcm0gdGVtcGVyYXR1cmVzIGFuZCByZWxhdGl2ZWx5IGRyeSBjb25kaXRpb25zIG1ha2UgaXQgaWRlYWwgZm9yIHZhcmlvdXMgcHVyc3VpdHMuXG5cbioqSGlraW5nIGFuZCBUcmVra2luZyoqOiBEZXN0aW5hdGlvbnMgbGlrZSBEb21pbmljYSwga25vd24gYXMgdGhlIFwiTmF0dXJlIElzbGFuZCxcIiBwcm92aWRlIGV4dGVuc2l2ZSBoaWtpbmcgdHJhaWxzIHRocm91Z2ggcmFpbmZvcmVzdHMgYW5kIHRvIHZvbGNhbmljIHBlYWtzLiBKYW1haWNhJ3MgQmx1ZSBNb3VudGFpbnMgb2ZmZXIgY2hhbGxlbmdpbmcgdHJla3Mgd2l0aCByZXdhcmRpbmcgcGFub3JhbWljIHZpZXdzLlxuXG4qKldhdGVyIFNwb3J0cyoqOiBUaGUgQmFoYW1hcyBhbmQgdGhlIENheW1hbiBJc2xhbmRzIGFyZSByZW5vd25lZCBmb3Igc2N1YmEgZGl2aW5nIGFuZCBzbm9ya2VsaW5nLCBmZWF0dXJpbmcgdmlicmFudCBjb3JhbCByZWVmcyBhbmQgZGl2ZXJzZSBtYXJpbmUgbGlmZS4gUHVlcnRvIFJpY28ncyBSaW5jb24gaXMgYSBob3RzcG90IGZvciBzdXJmaW5nLCB3aXRoIGNvbnNpc3RlbnQgd2F2ZXMgZHVyaW5nIHRoaXMgcGVyaW9kLlxuXG4qKlNhaWxpbmcgYW5kIEtheWFraW5nKio6IFRoZSBCcml0aXNoIFZpcmdpbiBJc2xhbmRzIGFyZSBhIHNhaWxvcidzIHBhcmFkaXNlLCBvZmZlcmluZyBjYWxtIHdhdGVycyBhbmQgbnVtZXJvdXMgaXNsYW5kcyB0byBleHBsb3JlLiBJbiBCZWxpemUsIGtheWFraW5nIHRocm91Z2ggdGhlIEdyZWF0IEJsdWUgSG9sZSBwcm92aWRlcyBhIHVuaXF1ZSBhZHZlbnR1cmUgZXhwZXJpZW5jZS5cblxuKipXaWxkbGlmZSBFeHBsb3JhdGlvbioqOiBBcHJpbCBpcyBwcmltZSB0aW1lIGZvciBiaXJkd2F0Y2hpbmcgaW4gVHJpbmlkYWQgYW5kIFRvYmFnbywgaG9tZSB0byBzcGVjaWVzIGxpa2UgdGhlIFNjYXJsZXQgSWJpcy4gSW4gU3QuIEx1Y2lhLCB2aXNpdG9ycyBjYW4gZXhwZXJpZW5jZSB0dXJ0bGUgd2F0Y2hpbmcgYXMgbGVhdGhlcmJhY2sgdHVydGxlcyBjb21lIGFzaG9yZSB0byBuZXN0LlxuXG4qKkN1bHR1cmFsIEZlc3RpdmFscyoqOiBUaGUgU3QuIFRob21hcyBDYXJuaXZhbCBpbiB0aGUgVS5TLiBWaXJnaW4gSXNsYW5kcyBvY2N1cnMgaW4gQXByaWwsIGJsZW5kaW5nIGN1bHR1cmFsIGZlc3Rpdml0aWVzIHdpdGggYWR2ZW50dXJlIG9wcG9ydHVuaXRpZXMuXG5cblRyYXZlbGVycyBzaG91bGQgY29uc3VsdCBzcGVjaWZpYyB0cmF2ZWwgZ3VpZGVzIGZvciBkZXRhaWxlZCBpbmZvcm1hdGlvbiBvbiB0aGVzZSBhY3Rpdml0aWVzLCBpbmNsdWRpbmcgc2FmZXR5IHRpcHMsIGVxdWlwbWVudCByZW50YWxzLCBhbmQgbG9jYWwgcmVndWxhdGlvbnMuICIsInR5cGUiOiJvdXRwdXRfdGV4dCIsImxvZ3Byb2JzIjpbXX1dLCJyb2xlIjoiYXNzaXN0YW50Iiwic3RhdHVzIjoiY29tcGxldGVkIiwidHlwZSI6Im1lc3NhZ2UifV0sInVzYWdlIjp7InJlcXVlc3RzIjoxLCJpbnB1dF90b2tlbnMiOjQ0MywiaW5wdXRfdG9rZW5zX2RldGFpbHMiOnsiY2FjaGVkX3Rva2VucyI6MH0sIm91dHB1dF90b2tlbnMiOjI3MCwib3V0cHV0X3Rva2Vuc19kZXRhaWxzIjp7InJlYXNvbmluZ190b2tlbnMiOjB9LCJ0b3RhbF90b2tlbnMiOjcxM30sInJlc3BvbnNlX2lkIjoicmVzcF82ODYyYzM4ODQ1ZWM4MWEyOWQ1MjYyYzRlNGIyODhlZTA2M2U3OWVlMGI3YjU4NjcifQ==" - } - ] - }, - "scheduledEventId": "15", - "startedEventId": "41", - "identity": "19167@Tims-MacBook-Pro.local" - } - }, - { - "eventId": "43", - "eventTime": "2025-06-30T17:04:14.959531Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", - "taskId": "1057154", - "workflowTaskScheduledEventAttributes": { - "taskQueue": { - "name": "19167@Tims-MacBook-Pro.local-f783ce7516bc48e59108b7b5cd31d0e5", - "kind": "TASK_QUEUE_KIND_STICKY", - "normalName": "fc97ba3c-6c32-4f4c-a986-8d7f4f5c7f80" - }, - "startToCloseTimeout": "10s", - "attempt": 1 - } - }, - { - "eventId": "44", - "eventTime": "2025-06-30T17:04:14.962477Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", - "taskId": "1057158", - "workflowTaskStartedEventAttributes": { - "scheduledEventId": "43", - "identity": "19167@Tims-MacBook-Pro.local", - "requestId": "84049bbb-0a74-4768-98ad-ce9f20e10a63", - "historySizeBytes": "35568", - "workerVersion": { - "buildId": "9f32b5d9789ece25bfb8515a7023ec78" - } - } - }, - { - "eventId": "45", - "eventTime": "2025-06-30T17:04:14.985519Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", - "taskId": "1057162", - "workflowTaskCompletedEventAttributes": { - "scheduledEventId": "43", - "startedEventId": "44", - "identity": "19167@Tims-MacBook-Pro.local", - "workerVersion": { - "buildId": "9f32b5d9789ece25bfb8515a7023ec78" - }, - "sdkMetadata": {}, - "meteringMetadata": {} - } - }, - { - "eventId": "46", - "eventTime": "2025-06-30T17:04:08.054957Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_STARTED", - "taskId": "1057164", - "activityTaskStartedEventAttributes": { - "scheduledEventId": "17", - "identity": "19167@Tims-MacBook-Pro.local", - "requestId": "c516d2b5-8215-4d6a-8cb0-695831a80a31", - "attempt": 1, - "workerVersion": { - "buildId": "9f32b5d9789ece25bfb8515a7023ec78" - } - } - }, - { - "eventId": "47", - "eventTime": "2025-06-30T17:04:15.153730Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_COMPLETED", - "taskId": "1057165", - "activityTaskCompletedEventAttributes": { - "result": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJvdXRwdXQiOlt7ImlkIjoid3NfNjg2MmMzODg0YjkwODFhMmFlYTYyMjg0MzA5MTJmZjEwNjlkNjI5MmViMjJlZmUwIiwiYWN0aW9uIjp7InF1ZXJ5IjoiU2VhcmNoIHRlcm06IENhcmliYmVhbiB2YWNhdGlvbiBwYWNrYWdlcyBzdXJmaW5nIGhpa2luZyBBcHJpbFxcblJlYXNvbiBmb3Igc2VhcmNoaW5nOiBUbyBkaXNjb3ZlciBhbGwtaW5jbHVzaXZlIHZhY2F0aW9uIHBhY2thZ2VzIGZvciBhbiBhZHZlbnR1cmUtZm9jdXNlZCBBcHJpbCB0cmlwLiIsInR5cGUiOiJzZWFyY2gifSwic3RhdHVzIjoiY29tcGxldGVkIiwidHlwZSI6IndlYl9zZWFyY2hfY2FsbCJ9LHsiaWQiOiJtc2dfNjg2MmMzOGI2ODdjODFhMjhiY2I2ZmM1ZmVjOWNmMTMwNjlkNjI5MmViMjJlZmUwIiwiY29udGVudCI6W3siYW5ub3RhdGlvbnMiOlt7ImVuZF9pbmRleCI6NTcyLCJzdGFydF9pbmRleCI6NDcyLCJ0aXRsZSI6IkFkdmVudHVyZSBCZWdpbnMgYXQgQ29jb251dCBCYXkiLCJ0eXBlIjoidXJsX2NpdGF0aW9uIiwidXJsIjoiaHR0cHM6Ly9jYmF5cmVzb3J0LmNvbS9hY3Rpdml0aWVzL2FkdmVudHVyZS1iZWdpbnMtY29jb251dC1iYXk/dXRtX3NvdXJjZT1vcGVuYWkifSx7ImVuZF9pbmRleCI6MTA4MSwic3RhcnRfaW5kZXgiOjk4MiwidGl0bGUiOiJXYW5kZXJsdXN0IENhcmliYmVhbiB8IERvbWluaWNhIEFkdmVudHVyZSAmIE5hdHVyZSBWYWNhdGlvbiBQYWNrYWdlcyB8IEZvciBOYXR1cmUgTG92ZXJzIGFuZCBBY3RpdmUgVHJhdmVsZXJzIiwidHlwZSI6InVybF9jaXRhdGlvbiIsInVybCI6Imh0dHBzOi8vd3d3LndhbmRlcmx1c3RjYXJpYmJlYW4uY29tL2NvcHktb2YtcGFja2FnZXM/dXRtX3NvdXJjZT1vcGVuYWkifSx7ImVuZF9pbmRleCI6MTU0NSwic3RhcnRfaW5kZXgiOjE0NzcsInRpdGxlIjoiQWxsLUluY2x1c2l2ZSBWYWNhdGlvbiBQYWNrYWdlcyB8IFNsaWNrcm9jayBCZWxpemUgQWR2ZW50dXJlcyIsInR5cGUiOiJ1cmxfY2l0YXRpb24iLCJ1cmwiOiJodHRwczovL3NsaWNrcm9jay5jb20vcGFja2FnZXMvP3V0bV9zb3VyY2U9b3BlbmFpIn1dLCJ0ZXh0IjoiRm9yIGFuIGFkdmVudHVyZS1mb2N1c2VkLCBhbGwtaW5jbHVzaXZlIENhcmliYmVhbiB2YWNhdGlvbiBpbiBBcHJpbCB0aGF0IGNvbWJpbmVzIHN1cmZpbmcgYW5kIGhpa2luZywgY29uc2lkZXIgdGhlIGZvbGxvd2luZyBvcHRpb25zOlxuXG4qKjEuIFN0LiBMdWNpYTogQ29jb251dCBCYXkgQmVhY2ggUmVzb3J0ICYgU3BhKipcblxuTG9jYXRlZCBvbiBTdC4gTHVjaWEncyBzb3V0aGVybiBjb2FzdCwgQ29jb251dCBCYXkgb2ZmZXJzIGEgcmFuZ2Ugb2YgYWR2ZW50dXJlIGFjdGl2aXRpZXMuIEd1ZXN0cyBjYW4gZW5nYWdlIGluIGtpdGUgc3VyZmluZywgaGlraW5nIHRocm91Z2ggcmFpbmZvcmVzdHMsIHNub3JrZWxpbmcsIHppcC1saW5pbmcsIGFuZCBvZmYtcm9hZGluZyBleGN1cnNpb25zLiBUaGUgcmVzb3J0IHByb3ZpZGVzIG5hdHVyZSBndWlkZXMgdG8gbGVhZCByYWluZm9yZXN0IGhpa2VzIGFuZCB3YXRlcmZhbGwgdmlzaXRzLiAoW2NiYXlyZXNvcnQuY29tXShodHRwczovL2NiYXlyZXNvcnQuY29tL2FjdGl2aXRpZXMvYWR2ZW50dXJlLWJlZ2lucy1jb2NvbnV0LWJheT91dG1fc291cmNlPW9wZW5haSkpXG5cbioqMi4gRG9taW5pY2E6IFdhbmRlcmx1c3QgQ2FyaWJiZWFuIEFkdmVudHVyZSBUcmF2ZWwgQm91dGlxdWUgSG90ZWwqKlxuXG5XYW5kZXJsdXN0IENhcmliYmVhbiBzcGVjaWFsaXplcyBpbiBhZHZlbnR1cmUgdHJhdmVsIHBhY2thZ2VzIG9uIHRoZSBpc2xhbmQgb2YgRG9taW5pY2EuIEFjdGl2aXRpZXMgaW5jbHVkZSBoaWtpbmcgdG8gd2F0ZXJmYWxscywgc25vcmtlbGluZyBpbiBwcmlzdGluZSByZWVmcywgc2N1YmEgZGl2aW5nLCBrYXlha2luZywgY2FueW9uaW5nLCB3aGFsZSB3YXRjaGluZywgYmlyZCB3YXRjaGluZywgYmlraW5nLCBhbmQgaG9yc2ViYWNrIHJpZGluZy4gVGhlIGhvdGVsIG9mZmVycyBwZXJzb25hbGl6ZWQgc21hbGwgZ3JvdXAgcGFja2FnZXMgdGFpbG9yZWQgdG8gYWN0aXZlIHRyYXZlbGVycy4gKFt3YW5kZXJsdXN0Y2FyaWJiZWFuLmNvbV0oaHR0cHM6Ly93d3cud2FuZGVybHVzdGNhcmliYmVhbi5jb20vY29weS1vZi1wYWNrYWdlcz91dG1fc291cmNlPW9wZW5haSkpXG5cbioqMy4gQmVsaXplOiBTbGlja3JvY2sgQmVsaXplIEFkdmVudHVyZXMqKlxuXG5TbGlja3JvY2sgb2ZmZXJzIGFsbC1pbmNsdXNpdmUgYWR2ZW50dXJlIHZhY2F0aW9uIHBhY2thZ2VzIG9uIExvbmcgQ2F5ZSwgcGFydCBvZiBHbG92ZXLigJlzIFJlZWYgQXRvbGwuIEd1ZXN0cyBzdGF5IGluIGJlYWNoZnJvbnQgY2FiYW5hcyBhbmQgcGFydGljaXBhdGUgaW4gYWN0aXZpdGllcyBzdWNoIGFzIHNlYSBrYXlha2luZywgc25vcmtlbGluZywgd2luZHN1cmZpbmcsIHN1cmYga2F5YWtpbmcsIGFuZCBzdGFuZC11cCBwYWRkbGVib2FyZGluZy4gVGhlIDgtbmlnaHQgcGFja2FnZSBpbmNsdWRlcyBhIG5pZ2h0IGF0IFBlbGljYW4gQmVhY2ggUmVzb3J0IGFuZCBzZXZlbiBuaWdodHMgb24gdGhlIGlzbGFuZC4gKFtzbGlja3JvY2suY29tXShodHRwczovL3NsaWNrcm9jay5jb20vcGFja2FnZXMvP3V0bV9zb3VyY2U9b3BlbmFpKSlcblxuKipXZWF0aGVyIENvbnNpZGVyYXRpb25zIGZvciBBcHJpbDoqKlxuXG4tICoqTW9udGVnbyBCYXksIEphbWFpY2E6KiogQXZlcmFnZSB0ZW1wZXJhdHVyZXMgcmFuZ2UgZnJvbSAyM8KwQyB0byAzM8KwQyAoNzPCsEYgdG8gOTHCsEYpLlxuXG4tICoqUHVlcnRvIFZpZWpvLCBDb3N0YSBSaWNhOioqIEF2ZXJhZ2UgdGVtcGVyYXR1cmVzIHJhbmdlIGZyb20gMjDCsEMgdG8gMzbCsEMgKDY4wrBGIHRvIDk3wrBGKS5cblxuLSAqKkNhYmFyZXRlLCBEb21pbmljYW4gUmVwdWJsaWM6KiogQXZlcmFnZSB0ZW1wZXJhdHVyZXMgcmFuZ2UgZnJvbSAxNcKwQyB0byAzM8KwQyAoNTnCsEYgdG8gOTHCsEYpLlxuXG5UaGVzZSBkZXN0aW5hdGlvbnMgb2ZmZXIgYSBibGVuZCBvZiBzdXJmaW5nIGFuZCBoaWtpbmcgYWN0aXZpdGllcyB3aXRoaW4gYWxsLWluY2x1c2l2ZSBwYWNrYWdlcywgY2F0ZXJpbmcgdG8gYWR2ZW50dXJlIGVudGh1c2lhc3RzIHNlZWtpbmcgYW4gYWN0aXZlIENhcmliYmVhbiB2YWNhdGlvbiBpbiBBcHJpbC4gIiwidHlwZSI6Im91dHB1dF90ZXh0IiwibG9ncHJvYnMiOltdfV0sInJvbGUiOiJhc3Npc3RhbnQiLCJzdGF0dXMiOiJjb21wbGV0ZWQiLCJ0eXBlIjoibWVzc2FnZSJ9XSwidXNhZ2UiOnsicmVxdWVzdHMiOjEsImlucHV0X3Rva2VucyI6NDQzLCJpbnB1dF90b2tlbnNfZGV0YWlscyI6eyJjYWNoZWRfdG9rZW5zIjowfSwib3V0cHV0X3Rva2VucyI6NDQyLCJvdXRwdXRfdG9rZW5zX2RldGFpbHMiOnsicmVhc29uaW5nX3Rva2VucyI6MH0sInRvdGFsX3Rva2VucyI6ODg1fSwicmVzcG9uc2VfaWQiOiJyZXNwXzY4NjJjMzg4NDNhODgxYTI4ZTAwOWE2NTExMWJhOGUxMDY5ZDYyOTJlYjIyZWZlMCJ9" - } - ] - }, - "scheduledEventId": "17", - "startedEventId": "46", - "identity": "19167@Tims-MacBook-Pro.local" - } - }, - { - "eventId": "48", - "eventTime": "2025-06-30T17:04:15.153740Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", - "taskId": "1057166", - "workflowTaskScheduledEventAttributes": { - "taskQueue": { - "name": "19167@Tims-MacBook-Pro.local-f783ce7516bc48e59108b7b5cd31d0e5", - "kind": "TASK_QUEUE_KIND_STICKY", - "normalName": "fc97ba3c-6c32-4f4c-a986-8d7f4f5c7f80" - }, - "startToCloseTimeout": "10s", - "attempt": 1 - } - }, - { - "eventId": "49", - "eventTime": "2025-06-30T17:04:15.156252Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", - "taskId": "1057170", - "workflowTaskStartedEventAttributes": { - "scheduledEventId": "48", - "identity": "19167@Tims-MacBook-Pro.local", - "requestId": "af4a9e00-a4d9-4d01-be55-b439ee29e776", - "historySizeBytes": "39649", - "workerVersion": { - "buildId": "9f32b5d9789ece25bfb8515a7023ec78" - } - } - }, - { - "eventId": "50", - "eventTime": "2025-06-30T17:04:15.178141Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", - "taskId": "1057174", - "workflowTaskCompletedEventAttributes": { - "scheduledEventId": "48", - "startedEventId": "49", - "identity": "19167@Tims-MacBook-Pro.local", - "workerVersion": { - "buildId": "9f32b5d9789ece25bfb8515a7023ec78" - }, - "sdkMetadata": {}, - "meteringMetadata": {} - } - }, - { - "eventId": "51", - "eventTime": "2025-06-30T17:04:08.054151Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_STARTED", - "taskId": "1057176", - "activityTaskStartedEventAttributes": { - "scheduledEventId": "14", - "identity": "19167@Tims-MacBook-Pro.local", - "requestId": "02e6bade-6bcb-4ea0-a4dc-343eb2398951", - "attempt": 1, - "workerVersion": { - "buildId": "9f32b5d9789ece25bfb8515a7023ec78" - } - } - }, - { - "eventId": "52", - "eventTime": "2025-06-30T17:04:15.535164Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_COMPLETED", - "taskId": "1057177", - "activityTaskCompletedEventAttributes": { - "result": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJvdXRwdXQiOlt7ImlkIjoid3NfNjg2MmMzODg0Zjc0ODFhMGIzZTBhMzQ4NjE0MmQ1MGYwZGYwZmM2ZTgwMWIzYmJkIiwiYWN0aW9uIjp7InF1ZXJ5IjoiU2VhcmNoIHRlcm06IENhcmliYmVhbiBpc2xhbmRzIHN1cmZpbmcgaGlraW5nIEFwcmlsXFxuUmVhc29uIGZvciBzZWFyY2hpbmc6IFRvIGRldGVybWluZSBzcGVjaWZpYyBpc2xhbmRzIGluIHRoZSBDYXJpYmJlYW4ga25vd24gZm9yIG91dGRvb3IgYWN0aXZpdGllcyBsaWtlIHN1cmZpbmcgYW5kIGhpa2luZyBpbiBBcHJpbC4iLCJ0eXBlIjoic2VhcmNoIn0sInN0YXR1cyI6ImNvbXBsZXRlZCIsInR5cGUiOiJ3ZWJfc2VhcmNoX2NhbGwifSx7ImlkIjoibXNnXzY4NjJjMzhjNTUyODgxYTBhMzM1OTllOGJmM2U1MDg0MGRmMGZjNmU4MDFiM2JiZCIsImNvbnRlbnQiOlt7ImFubm90YXRpb25zIjpbXSwidGV4dCI6IlNldmVyYWwgQ2FyaWJiZWFuIGlzbGFuZHMgb2ZmZXIgZXhjZWxsZW50IG9wcG9ydHVuaXRpZXMgZm9yIGJvdGggc3VyZmluZyBhbmQgaGlraW5nIGluIEFwcmlsLiBKYW1haWNhLCBwYXJ0aWN1bGFybHkgdGhlIE1vbnRlZ28gQmF5IGFyZWEsIHByb3ZpZGVzIHdhcm0gdGVtcGVyYXR1cmVzIHJhbmdpbmcgZnJvbSAyMi45wrBDIHRvIDMzLjTCsEMsIGlkZWFsIGZvciBvdXRkb29yIGFjdGl2aXRpZXMuIFRoZSBpc2xhbmQgZmVhdHVyZXMgc3VyZiBzcG90cyBsaWtlIEJvc3RvbiBCYXkgYW5kIExvbmcgQmF5LCBhbmQgaGlraW5nIHRyYWlscyBpbiB0aGUgQmx1ZSBNb3VudGFpbnMuXG5cbkJhcmJhZG9zLCB3aXRoIHRlbXBlcmF0dXJlcyBiZXR3ZWVuIDIyLjjCsEMgYW5kIDMxLjHCsEMsIGlzIGtub3duIGZvciBjb25zaXN0ZW50IHN1cmYgYnJlYWtzIHN1Y2ggYXMgU291cCBCb3dsIGFuZCBGcmVpZ2h0cyBCYXkuIFRoZSBpc2xhbmQgYWxzbyBvZmZlcnMgaGlraW5nIG9wcG9ydHVuaXRpZXMgaW4gYXJlYXMgbGlrZSB0aGUgU2NvdGxhbmQgRGlzdHJpY3QuXG5cblRoZSBEb21pbmljYW4gUmVwdWJsaWMsIGVzcGVjaWFsbHkgYXJvdW5kIFB1bnRhIENhbmEsIGV4cGVyaWVuY2VzIHRlbXBlcmF0dXJlcyBmcm9tIDIwLjPCsEMgdG8gMzEuNcKwQy4gU3VyZmluZyBpcyBwb3B1bGFyIGluIENhYmFyZXRlLCB3aGlsZSB0aGUgQ29yZGlsbGVyYSBDZW50cmFsIG1vdW50YWlucyBwcm92aWRlIGV4dGVuc2l2ZSBoaWtpbmcgdHJhaWxzLlxuXG5Db3N0YSBSaWNhJ3MgUHVlcnRvIFZpZWpvIHJlZ2lvbiwgd2l0aCB0ZW1wZXJhdHVyZXMgYmV0d2VlbiAyMC4wwrBDIGFuZCAzNi4xwrBDLCBvZmZlcnMgc3VyZiBzcG90cyBsaWtlIFNhbHNhIEJyYXZhIGFuZCBQbGF5YSBDb2NsZXMsIGFsb25nc2lkZSBoaWtpbmcgaW4gdGhlIG5lYXJieSBHYW5kb2NhLU1hbnphbmlsbG8gV2lsZGxpZmUgUmVmdWdlLlxuXG5QdWVydG8gUmljbywgcGFydGljdWxhcmx5IFNhbiBKdWFuLCBoYXMgdGVtcGVyYXR1cmVzIHJhbmdpbmcgZnJvbSAyMi4wwrBDIHRvIDMxLjXCsEMuIFRoZSBpc2xhbmQgZmVhdHVyZXMgc3VyZiBzcG90cyBsaWtlIFJpbmNvbiBhbmQgSXNhYmVsYSwgYW5kIGhpa2luZyB0cmFpbHMgaW4gRWwgWXVucXVlIE5hdGlvbmFsIEZvcmVzdC5cblxuVGhlc2UgaXNsYW5kcyBwcm92aWRlIGEgY29tYmluYXRpb24gb2Ygc3VyZmluZyBhbmQgaGlraW5nIGV4cGVyaWVuY2VzIHN1aXRhYmxlIGZvciBBcHJpbCdzIGNsaW1hdGUuICIsInR5cGUiOiJvdXRwdXRfdGV4dCIsImxvZ3Byb2JzIjpbXX1dLCJyb2xlIjoiYXNzaXN0YW50Iiwic3RhdHVzIjoiY29tcGxldGVkIiwidHlwZSI6Im1lc3NhZ2UifV0sInVzYWdlIjp7InJlcXVlc3RzIjoxLCJpbnB1dF90b2tlbnMiOjQ1MSwiaW5wdXRfdG9rZW5zX2RldGFpbHMiOnsiY2FjaGVkX3Rva2VucyI6MH0sIm91dHB1dF90b2tlbnMiOjI2Niwib3V0cHV0X3Rva2Vuc19kZXRhaWxzIjp7InJlYXNvbmluZ190b2tlbnMiOjB9LCJ0b3RhbF90b2tlbnMiOjcxN30sInJlc3BvbnNlX2lkIjoicmVzcF82ODYyYzM4ODNkZWM4MWEwYmQzM2RmMWE3MzY0MDczMzBkZjBmYzZlODAxYjNiYmQifQ==" - } - ] - }, - "scheduledEventId": "14", - "startedEventId": "51", - "identity": "19167@Tims-MacBook-Pro.local" - } - }, - { - "eventId": "53", - "eventTime": "2025-06-30T17:04:15.535176Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", - "taskId": "1057178", - "workflowTaskScheduledEventAttributes": { - "taskQueue": { - "name": "19167@Tims-MacBook-Pro.local-f783ce7516bc48e59108b7b5cd31d0e5", - "kind": "TASK_QUEUE_KIND_STICKY", - "normalName": "fc97ba3c-6c32-4f4c-a986-8d7f4f5c7f80" - }, - "startToCloseTimeout": "10s", - "attempt": 1 - } - }, - { - "eventId": "54", - "eventTime": "2025-06-30T17:04:15.537839Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", - "taskId": "1057182", - "workflowTaskStartedEventAttributes": { - "scheduledEventId": "53", - "identity": "19167@Tims-MacBook-Pro.local", - "requestId": "769bd159-6aec-4a6c-aaf0-d40e77f3c033", - "historySizeBytes": "42285", - "workerVersion": { - "buildId": "9f32b5d9789ece25bfb8515a7023ec78" - } - } - }, - { - "eventId": "55", - "eventTime": "2025-06-30T17:04:15.560851Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", - "taskId": "1057186", - "workflowTaskCompletedEventAttributes": { - "scheduledEventId": "53", - "startedEventId": "54", - "identity": "19167@Tims-MacBook-Pro.local", - "workerVersion": { - "buildId": "9f32b5d9789ece25bfb8515a7023ec78" - }, - "sdkMetadata": {}, - "meteringMetadata": {} - } - }, - { - "eventId": "56", - "eventTime": "2025-06-30T17:04:08.057282Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_STARTED", - "taskId": "1057188", - "activityTaskStartedEventAttributes": { - "scheduledEventId": "19", - "identity": "19167@Tims-MacBook-Pro.local", - "requestId": "84749c23-921d-4b23-9571-e0c651d6d0ab", - "attempt": 1, - "workerVersion": { - "buildId": "9f32b5d9789ece25bfb8515a7023ec78" - } - } - }, - { - "eventId": "57", - "eventTime": "2025-06-30T17:04:16.071889Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_COMPLETED", - "taskId": "1057189", - "activityTaskCompletedEventAttributes": { - "result": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJvdXRwdXQiOlt7ImlkIjoid3NfNjg2MmMzODg3NmYwODE5ZGFiMTA3NzIyODIzNDFhZGIwNmNlY2JlYzBjMjgyZGI1IiwiYWN0aW9uIjp7InF1ZXJ5IjoiU2VhcmNoIHRlcm06IHdhdGVyIHNwb3J0cyBnZWFyIHJlbnRhbHMgQ2FyaWJiZWFuIEFwcmlsXFxuUmVhc29uIGZvciBzZWFyY2hpbmc6IFRvIGZpbmQgaW5mb3JtYXRpb24gb24gcmVudGFsIGVxdWlwbWVudCBhdmFpbGFiaWxpdHkgZm9yIHdhdGVyIHNwb3J0cyBhbmQgaGlraW5nIGluIHRoZSBDYXJpYmJlYW4uIiwidHlwZSI6InNlYXJjaCJ9LCJzdGF0dXMiOiJjb21wbGV0ZWQiLCJ0eXBlIjoid2ViX3NlYXJjaF9jYWxsIn0seyJpZCI6Im1zZ182ODYyYzM4YWRjYzg4MTlkYTQ0MTNmYzYzMGE0NmYyODA2Y2VjYmVjMGMyODJkYjUiLCJjb250ZW50IjpbeyJhbm5vdGF0aW9ucyI6W3siZW5kX2luZGV4Ijo0NzksInN0YXJ0X2luZGV4IjozOTEsInRpdGxlIjoiUkVOVEFMUyB8IFJpbmNvbiBDYXBpdGFsIFdhdGVyIFNwb3J0cyIsInR5cGUiOiJ1cmxfY2l0YXRpb24iLCJ1cmwiOiJodHRwczovL3d3dy5jYXBpdGFsd2F0ZXJzcG9ydHMuY29tL3JlbnRhbHM/dXRtX3NvdXJjZT1vcGVuYWkifSx7ImVuZF9pbmRleCI6OTMxLCJzdGFydF9pbmRleCI6ODMyLCJ0aXRsZSI6IlBsYWNlbmNpYSBBd2Vzb21lIFdhdGVyIFNwb3J0cyIsInR5cGUiOiJ1cmxfY2l0YXRpb24iLCJ1cmwiOiJodHRwczovL3d3dy5wbGFjZW5jaWFhd2Vzb21ld2F0ZXJzcG9ydHMuY29tLz91dG1fc291cmNlPW9wZW5haSJ9LHsiZW5kX2luZGV4IjoxMzU0LCJzdGFydF9pbmRleCI6MTI3NSwidGl0bGUiOiJDYXJpYmJlYW4gUGFkZGxpbmcgfCBLYXlhayAmIFBhZGRsZWJvYXJkIFJlbnRhbHMgYXQgU3QuIE1hcnRpbiIsInR5cGUiOiJ1cmxfY2l0YXRpb24iLCJ1cmwiOiJodHRwczovL3d3dy5jYXJpYmJlYW5wYWRkbGluZy5jb20vP3V0bV9zb3VyY2U9b3BlbmFpIn0seyJlbmRfaW5kZXgiOjE2NDksInN0YXJ0X2luZGV4IjoxNTY4LCJ0aXRsZSI6IlN0LiBUaG9tYXMgV2F0ZXIgU3BvcnRzIFJlbnRhbHMgLSBJc2xhbmQgQWR2ZW50dXJlcyIsInR5cGUiOiJ1cmxfY2l0YXRpb24iLCJ1cmwiOiJodHRwczovL2Z1bmJvYXRkYXkuY29tL3dhdGVyLXNwb3J0LXJlbnRhbHMvP3V0bV9zb3VyY2U9b3BlbmFpIn0seyJlbmRfaW5kZXgiOjIwMDEsInN0YXJ0X2luZGV4IjoxOTIwLCJ0aXRsZSI6IlN0LiBLaXR0cyBXYXRlciBTcG9ydHMg4oCTIFRoZSAjMSBXYXRlciBTcG9ydHMgQ2VudGVyIGluIHRoZSBDYXJpYmJlYW4iLCJ0eXBlIjoidXJsX2NpdGF0aW9uIiwidXJsIjoiaHR0cHM6Ly93d3cuc3RraXR0c3dhdGVyc3BvcnRzLmNvbS8/dXRtX3NvdXJjZT1vcGVuYWkifSx7ImVuZF9pbmRleCI6MjM4OSwic3RhcnRfaW5kZXgiOjIyOTEsInRpdGxlIjoiSXNsYW5kIFN1cmYgJiBTYWlsIC0gQlZJIFRyYXZlbGxlciIsInR5cGUiOiJ1cmxfY2l0YXRpb24iLCJ1cmwiOiJodHRwczovL3d3dy5idml0cmF2ZWxsZXIuY29tL3dhdGVyc3BvcnRzL2lzbGFuZC1zdXJmLXNhaWwvP3V0bV9zb3VyY2U9b3BlbmFpIn0seyJlbmRfaW5kZXgiOjI3MzYsInN0YXJ0X2luZGV4IjoyNjYzLCJ0aXRsZSI6IlJlbnRhbHMgaW4gUHVlcnRvIFJpY28gfCBBcXVhdGljYSBQdWVydG8gUmljbyIsInR5cGUiOiJ1cmxfY2l0YXRpb24iLCJ1cmwiOiJodHRwczovL3d3dy5hcXVhdGljYXByLmNvbS9yZW50YWxzLz91dG1fc291cmNlPW9wZW5haSJ9XSwidGV4dCI6IkluIHRoZSBDYXJpYmJlYW4sIG51bWVyb3VzIHdhdGVyIHNwb3J0cyBnZWFyIHJlbnRhbCBzZXJ2aWNlcyBhcmUgYXZhaWxhYmxlIHRvIGVuaGFuY2UgeW91ciBhcXVhdGljIGFkdmVudHVyZXMuIEZvciBpbnN0YW5jZSwgKipSaW5jb24gQ2FwaXRhbCBXYXRlciBTcG9ydHMqKiBvZmZlcnMgYSB2YXJpZXR5IG9mIGVxdWlwbWVudCwgaW5jbHVkaW5nIGpldCBza2lzLCBrYXlha3MsIHN1cmZib2FyZHMsIHBhZGRsZWJvYXJkcywgc25vcmtlbCBhbmQgc2N1YmEgZ2VhciwgZmlzaGluZyBlcXVpcG1lbnQsIGJlYWNoIGNoYWlycywgdW1icmVsbGFzLCBhbmQgYmFuYW5hIGJvYXRzLiBUaGV5IHJlY29tbWVuZCBjb250YWN0aW5nIHRoZW0gZGlyZWN0bHkgdG8gbWFrZSBhIHJlc2VydmF0aW9uLiAoW2NhcGl0YWx3YXRlcnNwb3J0cy5jb21dKGh0dHBzOi8vd3d3LmNhcGl0YWx3YXRlcnNwb3J0cy5jb20vcmVudGFscz91dG1fc291cmNlPW9wZW5haSkpXG5cblNpbWlsYXJseSwgKipQbGFjZW5jaWEgQXdlc29tZSBXYXRlciBTcG9ydHMqKiBpbiBCZWxpemUgcHJvdmlkZXMgcmVudGFscyBmb3IgamV0IHNraXMsIGtheWFrcywgcGFkZGxlYm9hcmRzLCBhbmQgc25vcmtlbCBnZWFyLiBUaGVpciBqZXQgc2tpcyBjYW4gYWNjb21tb2RhdGUgdHdvIGFkdWx0cyBhbmQgYXJlIGF2YWlsYWJsZSBmb3IgcmVudCB0aHJvdWdob3V0IHRoZSB5ZWFyLiBUaGV5IGFsc28gb2ZmZXIgaG91cmx5LCBoYWxmLWRheSwgZnVsbC1kYXksIGFuZCB3ZWVrbHkgcmVudGFscyBmb3IgcGFkZGxlYm9hcmRzIGFuZCBrYXlha3MsIHdpdGggZGVsaXZlcnkgb3B0aW9ucyBmb3Igd2Vla2x5IHJlbnRhbHMuIChbcGxhY2VuY2lhYXdlc29tZXdhdGVyc3BvcnRzLmNvbV0oaHR0cHM6Ly93d3cucGxhY2VuY2lhYXdlc29tZXdhdGVyc3BvcnRzLmNvbS8/dXRtX3NvdXJjZT1vcGVuYWkpKVxuXG5JbiBTYWludC1NYXJ0aW4sICoqQ2FyaWJiZWFuIFBhZGRsaW5nKiogc3BlY2lhbGl6ZXMgaW4gZWNvLWZyaWVuZGx5IGFkdmVudHVyZXMsIG9mZmVyaW5nIGtheWFrIGFuZCBwYWRkbGVib2FyZCByZW50YWxzLCBhcyB3ZWxsIGFzIHNub3JrZWxpbmcgZXhwZXJpZW5jZXMuIFRoZXkgcHJvdmlkZSB0b3AtcXVhbGl0eSBlcXVpcG1lbnQgYW5kIHBlcnNvbmFsaXplZCBhZHZlbnR1cmUgcGFja2FnZXMgdGFpbG9yZWQgdG8gdmFyaW91cyBza2lsbCBsZXZlbHMuIFRoZWlyIHByaW1lIGxvY2F0aW9uIG9mZmVycyBkaXJlY3QgYWNjZXNzIHRvIHBpY3R1cmVzcXVlIHNwb3RzIGxpa2UgUGluZWwgSXNsYW5kLiAoW2NhcmliYmVhbnBhZGRsaW5nLmNvbV0oaHR0cHM6Ly93d3cuY2FyaWJiZWFucGFkZGxpbmcuY29tLz91dG1fc291cmNlPW9wZW5haSkpXG5cbkZvciB0aG9zZSB2aXNpdGluZyBTdC4gVGhvbWFzLCAqKklzbGFuZCBBZHZlbnR1cmVzKiogb2ZmZXJzIHNub3JrZWwgZ2VhciwgY29vbGVycywgbm9vZGxlcywgZmxvYXQgbWF0cywgYW5kIGZvbGRpbmcgYmVhY2ggY2hhaXJzIGZvciByZW50LiBUaGV5IGhhdmUgcGljay11cCBsb2NhdGlvbnMgYXQgTWFyZ2FyaXRhdmlsbGUgUmVzb3J0IGFuZCBFbHlzaWFuIFJlc29ydC4gKFtmdW5ib2F0ZGF5LmNvbV0oaHR0cHM6Ly9mdW5ib2F0ZGF5LmNvbS93YXRlci1zcG9ydC1yZW50YWxzLz91dG1fc291cmNlPW9wZW5haSkpXG5cbkluIFN0LiBLaXR0cywgKipTdC4gS2l0dHMgV2F0ZXIgU3BvcnRzKiogcHJvdmlkZXMgYSB3aWRlIHJhbmdlIG9mIGFjdGl2aXRpZXMsIGluY2x1ZGluZyBqZXQgc2tpIHJlbnRhbHMsIGZseWJvYXJkaW5nLCBwYWRkbGVib2FyZGluZywga2F5YWtpbmcsIGFuZCBzbm9ya2VsaW5nLiBUaGV5IG9mZmVyIHZhcmlvdXMgcGFja2FnZXMsIHN1Y2ggYXMgdGhlIEFsbCBEYXkgQWN0aW9uIFBhc3MsIHdoaWNoIGluY2x1ZGVzIG11bHRpcGxlIGFjdGl2aXRpZXMgaW4gb25lIGRheS4gKFtzdGtpdHRzd2F0ZXJzcG9ydHMuY29tXShodHRwczovL3d3dy5zdGtpdHRzd2F0ZXJzcG9ydHMuY29tLz91dG1fc291cmNlPW9wZW5haSkpXG5cbkluIFRvcnRvbGEsICoqSXNsYW5kIFN1cmYgJiBTYWlsKiogb2ZmZXJzIGEgdmFyaWV0eSBvZiB3YXRlciB0b3lzIGFuZCBzcG9ydHMgZXF1aXBtZW50IGZvciByZW50LCBpbmNsdWRpbmcga2F5YWtzLCBzdGFuZC11cCBwYWRkbGVib2FyZHMsIHN1cmZib2FyZHMsIGZpc2hpbmcgZ2Vhciwgc25vcmtlbGluZyBlcXVpcG1lbnQsIGFuZCBmbG9hdHMuIFRoZXkgcHJvdmlkZSBmcmVlIGRlbGl2ZXJ5IHRvIGNoYXJ0ZXIgY29tcGFuaWVzLCBob3RlbHMsIHZpbGxhcywgYW5kIGJlYWNoZXMgaXNsYW5kLXdpZGUuIChbYnZpdHJhdmVsbGVyLmNvbV0oaHR0cHM6Ly93d3cuYnZpdHJhdmVsbGVyLmNvbS93YXRlcnNwb3J0cy9pc2xhbmQtc3VyZi1zYWlsLz91dG1fc291cmNlPW9wZW5haSkpXG5cbkluIFB1ZXJ0byBSaWNvLCAqKkFxdWF0aWNhIFVuZGVyd2F0ZXIgQWR2ZW50dXJlcyoqIG9mZmVycyBzY3ViYSBhbmQgc25vcmtlbCBnZWFyIHJlbnRhbHMgYW5kIHNhbGVzLCBpbmNsdWRpbmcgbWFza3MsIHNub3JrZWxzLCBmaW5zLCBCQ0RzLCByZWd1bGF0b3JzLCBhbmQgdGFua3MuIFRoZXkgYXJlIGNvbnZlbmllbnRseSBsb2NhdGVkIG5lYXIgc2V2ZXJhbCBwb3B1bGFyIGJlYWNoZXMgYW5kIHByb3ZpZGUgaGlnaC1xdWFsaXR5LCBzYWZldHktdGVzdGVkIGVxdWlwbWVudC4gKFthcXVhdGljYXByLmNvbV0oaHR0cHM6Ly93d3cuYXF1YXRpY2Fwci5jb20vcmVudGFscy8/dXRtX3NvdXJjZT1vcGVuYWkpKVxuXG5SZWdhcmRpbmcgaGlraW5nIGdlYXIgcmVudGFscywgc3BlY2lmaWMgaW5mb3JtYXRpb24gaXMgbGltaXRlZC4gSXQncyBhZHZpc2FibGUgdG8gY29udGFjdCBsb2NhbCB0b3VyIG9wZXJhdG9ycyBvciBvdXRkb29yIGFkdmVudHVyZSBjb21wYW5pZXMgaW4geW91ciBjaG9zZW4gZGVzdGluYXRpb24gdG8gaW5xdWlyZSBhYm91dCB0aGUgYXZhaWxhYmlsaXR5IG9mIGhpa2luZyBlcXVpcG1lbnQgcmVudGFscy4gIiwidHlwZSI6Im91dHB1dF90ZXh0IiwibG9ncHJvYnMiOltdfV0sInJvbGUiOiJhc3Npc3RhbnQiLCJzdGF0dXMiOiJjb21wbGV0ZWQiLCJ0eXBlIjoibWVzc2FnZSJ9XSwidXNhZ2UiOnsicmVxdWVzdHMiOjEsImlucHV0X3Rva2VucyI6NDQ5LCJpbnB1dF90b2tlbnNfZGV0YWlscyI6eyJjYWNoZWRfdG9rZW5zIjowfSwib3V0cHV0X3Rva2VucyI6NjM3LCJvdXRwdXRfdG9rZW5zX2RldGFpbHMiOnsicmVhc29uaW5nX3Rva2VucyI6MH0sInRvdGFsX3Rva2VucyI6MTA4Nn0sInJlc3BvbnNlX2lkIjoicmVzcF82ODYyYzM4ODZkZTg4MTlkOTNkNDBkYmMzYTcxMTIwODA2Y2VjYmVjMGMyODJkYjUifQ==" - } - ] - }, - "scheduledEventId": "19", - "startedEventId": "56", - "identity": "19167@Tims-MacBook-Pro.local" - } - }, - { - "eventId": "58", - "eventTime": "2025-06-30T17:04:16.071903Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", - "taskId": "1057190", - "workflowTaskScheduledEventAttributes": { - "taskQueue": { - "name": "19167@Tims-MacBook-Pro.local-f783ce7516bc48e59108b7b5cd31d0e5", - "kind": "TASK_QUEUE_KIND_STICKY", - "normalName": "fc97ba3c-6c32-4f4c-a986-8d7f4f5c7f80" - }, - "startToCloseTimeout": "10s", - "attempt": 1 - } - }, - { - "eventId": "59", - "eventTime": "2025-06-30T17:04:16.074976Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", - "taskId": "1057194", - "workflowTaskStartedEventAttributes": { - "scheduledEventId": "58", - "identity": "19167@Tims-MacBook-Pro.local", - "requestId": "75ed8ca4-07ab-4876-895d-bceaecf1380b", - "historySizeBytes": "47945", - "workerVersion": { - "buildId": "9f32b5d9789ece25bfb8515a7023ec78" - } - } - }, - { - "eventId": "60", - "eventTime": "2025-06-30T17:04:16.097762Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", - "taskId": "1057198", - "workflowTaskCompletedEventAttributes": { - "scheduledEventId": "58", - "startedEventId": "59", - "identity": "19167@Tims-MacBook-Pro.local", - "workerVersion": { - "buildId": "9f32b5d9789ece25bfb8515a7023ec78" - }, - "sdkMetadata": {}, - "meteringMetadata": {} - } - }, - { - "eventId": "61", - "eventTime": "2025-06-30T17:04:08.058230Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_STARTED", - "taskId": "1057200", - "activityTaskStartedEventAttributes": { - "scheduledEventId": "12", - "identity": "19167@Tims-MacBook-Pro.local", - "requestId": "28fc40c3-6a61-47ea-8a8f-10f21da99d69", - "attempt": 1, - "workerVersion": { - "buildId": "9f32b5d9789ece25bfb8515a7023ec78" - } - } - }, - { - "eventId": "62", - "eventTime": "2025-06-30T17:04:16.487908Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_COMPLETED", - "taskId": "1057201", - "activityTaskCompletedEventAttributes": { - "result": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJvdXRwdXQiOlt7ImlkIjoid3NfNjg2MmMzODg1YWE4ODFhMWE2ODVjODNjMTA4NTRjNjEwOTkyNGRmNjIxYWNlOWUwIiwiYWN0aW9uIjp7InF1ZXJ5IjoiU2VhcmNoIHRlcm06IGJlc3QgQ2FyaWJiZWFuIGhpa2luZyBkZXN0aW5hdGlvbnMgQXByaWxcXG5SZWFzb24gZm9yIHNlYXJjaGluZzogVG8gaWRlbnRpZnkgdG9wIGhpa2luZyBkZXN0aW5hdGlvbnMgaW4gdGhlIENhcmliYmVhbiBmb3IgQXByaWwgdmFjYXRpb25zLiIsInR5cGUiOiJzZWFyY2gifSwic3RhdHVzIjoiY29tcGxldGVkIiwidHlwZSI6IndlYl9zZWFyY2hfY2FsbCJ9LHsiaWQiOiJtc2dfNjg2MmMzOGMwZjE4ODFhMWFhMmRjZTZiZjdjYmM3MDcwOTkyNGRmNjIxYWNlOWUwIiwiY29udGVudCI6W3siYW5ub3RhdGlvbnMiOlt7ImVuZF9pbmRleCI6NzQxLCJzdGFydF9pbmRleCI6NjQ1LCJ0aXRsZSI6Ikhpa2luZyBhbmQgTmF0dXJlIFRyYWlscyBpbiB0aGUgQ2FyaWJiZWFuIC0gQ2FyaWJ1bmUiLCJ0eXBlIjoidXJsX2NpdGF0aW9uIiwidXJsIjoiaHR0cHM6Ly93d3cuY2FyaWJ1bmUuY29tL2NhcmliYmVhbi1oaWtpbmctYW5kLW5hdHVyZS10cmFpbHMvP3V0bV9zb3VyY2U9b3BlbmFpIn0seyJlbmRfaW5kZXgiOjEyNzMsInN0YXJ0X2luZGV4IjoxMTc3LCJ0aXRsZSI6Ikhpa2luZyBhbmQgTmF0dXJlIFRyYWlscyBpbiB0aGUgQ2FyaWJiZWFuIC0gQ2FyaWJ1bmUiLCJ0eXBlIjoidXJsX2NpdGF0aW9uIiwidXJsIjoiaHR0cHM6Ly93d3cuY2FyaWJ1bmUuY29tL2NhcmliYmVhbi1oaWtpbmctYW5kLW5hdHVyZS10cmFpbHMvP3V0bV9zb3VyY2U9b3BlbmFpIn0seyJlbmRfaW5kZXgiOjE4MzMsInN0YXJ0X2luZGV4IjoxNzMyLCJ0aXRsZSI6IjUgYmVzdCBoaWtlcyBpbiB0aGUgQ2FyaWJiZWFuIC0gTG9uZWx5IFBsYW5ldCIsInR5cGUiOiJ1cmxfY2l0YXRpb24iLCJ1cmwiOiJodHRwczovL3d3dy5sb25lbHlwbGFuZXQuY29tL2FydGljbGVzL2Jlc3QtaGlrZXMtaW4tY2FyaWJiZWFuP3V0bV9zb3VyY2U9b3BlbmFpIn0seyJlbmRfaW5kZXgiOjIyNTEsInN0YXJ0X2luZGV4IjoyMTY4LCJ0aXRsZSI6Ik1vdW50IFNjZW5lcnkiLCJ0eXBlIjoidXJsX2NpdGF0aW9uIiwidXJsIjoiaHR0cHM6Ly9lbi53aWtpcGVkaWEub3JnL3dpa2kvTW91bnRfU2NlbmVyeT91dG1fc291cmNlPW9wZW5haSJ9LHsiZW5kX2luZGV4IjoyNzQ5LCJzdGFydF9pbmRleCI6MjY1MywidGl0bGUiOiJIaWtpbmcgYW5kIE5hdHVyZSBUcmFpbHMgaW4gdGhlIENhcmliYmVhbiAtIENhcmlidW5lIiwidHlwZSI6InVybF9jaXRhdGlvbiIsInVybCI6Imh0dHBzOi8vd3d3LmNhcmlidW5lLmNvbS9jYXJpYmJlYW4taGlraW5nLWFuZC1uYXR1cmUtdHJhaWxzLz91dG1fc291cmNlPW9wZW5haSJ9XSwidGV4dCI6IkFwcmlsIGlzIGFuIGV4Y2VsbGVudCB0aW1lIHRvIGV4cGxvcmUgdGhlIENhcmliYmVhbidzIGRpdmVyc2UgaGlraW5nIGRlc3RpbmF0aW9ucywgb2ZmZXJpbmcgZmF2b3JhYmxlIHdlYXRoZXIgY29uZGl0aW9ucyBhbmQgdmlicmFudCBuYXR1cmFsIGxhbmRzY2FwZXMuIEhlcmUgYXJlIHNvbWUgdG9wIGhpa2luZyBzcG90cyB0byBjb25zaWRlcjpcblxuKioxLiBFbCBZdW5xdWUgTmF0aW9uYWwgRm9yZXN0LCBQdWVydG8gUmljbyoqXG5cbkFzIHRoZSBvbmx5IHRyb3BpY2FsIHJhaW5mb3Jlc3QgaW4gdGhlIFUuUy4gTmF0aW9uYWwgRm9yZXN0IFN5c3RlbSwgRWwgWXVucXVlIHNwYW5zIGFwcHJveGltYXRlbHkgMjgsMDAwIGFjcmVzIHdpdGggYSBuZXR3b3JrIG9mIHdlbGwtbWFpbnRhaW5lZCB0cmFpbHMuIEhpa2VycyBjYW4gZXhwZXJpZW5jZSBkaXZlcnNlIGZsb3JhIGFuZCBmYXVuYSwgaW5jbHVkaW5nIHRoZSBQdWVydG8gUmljYW4gcGFycm90IGFuZCBjb3F1w60gZnJvZywgYW5kIHZpc2l0IHN0dW5uaW5nIHdhdGVyZmFsbHMgbGlrZSBMYSBNaW5hIEZhbGxzLiBPYnNlcnZhdGlvbiB0b3dlcnMgc3VjaCBhcyBZb2thaHUgVG93ZXIgcHJvdmlkZSBwYW5vcmFtaWMgdmlld3Mgb2YgdGhlIGZvcmVzdCBjYW5vcHkgYW5kIGNvYXN0bGluZS4gKFtjYXJpYnVuZS5jb21dKGh0dHBzOi8vd3d3LmNhcmlidW5lLmNvbS9jYXJpYmJlYW4taGlraW5nLWFuZC1uYXR1cmUtdHJhaWxzLz91dG1fc291cmNlPW9wZW5haSkpXG5cbioqMi4gQmx1ZSBNb3VudGFpbiBQZWFrLCBKYW1haWNhKipcblxuU3RhbmRpbmcgYXQgNyw0MDIgZmVldCwgQmx1ZSBNb3VudGFpbiBQZWFrIGlzIEphbWFpY2EncyBoaWdoZXN0IHBvaW50LiBUaGUgaGlrZSB0byB0aGUgc3VtbWl0IGlzIHBvcHVsYXIgZm9yIGl0cyBicmVhdGh0YWtpbmcgc3VucmlzZSB2aWV3cywgd2hlcmUsIG9uIGNsZWFyIGRheXMsIHRoZSBkaXN0YW50IGNvYXN0bGluZSBvZiBDdWJhIGlzIHZpc2libGUuIFRoZSBhcmVhIGlzIGEgVU5FU0NPIFdvcmxkIEhlcml0YWdlIFNpdGUsIGhvbWUgdG8gZW5kZW1pYyBzcGVjaWVzIGxpa2UgdGhlIEphbWFpY2FuIHN3YWxsb3d0YWlsIGJ1dHRlcmZseSwgYW5kIGlzIHJlbm93bmVkIGZvciBpdHMgQmx1ZSBNb3VudGFpbiBDb2ZmZWUgcGxhbnRhdGlvbnMgb2ZmZXJpbmcgdG91cnMgYW5kIHRhc3RpbmdzLiAoW2NhcmlidW5lLmNvbV0oaHR0cHM6Ly93d3cuY2FyaWJ1bmUuY29tL2NhcmliYmVhbi1oaWtpbmctYW5kLW5hdHVyZS10cmFpbHMvP3V0bV9zb3VyY2U9b3BlbmFpKSlcblxuKiozLiBXYWl0dWt1YnVsaSBOYXRpb25hbCBUcmFpbCwgRG9taW5pY2EqKlxuXG5TdHJldGNoaW5nIDExNSBtaWxlcyBhY3Jvc3MgRG9taW5pY2EsIHRoZSBXYWl0dWt1YnVsaSBOYXRpb25hbCBUcmFpbCBpcyB0aGUgQ2FyaWJiZWFuJ3MgZmlyc3QgbG9uZy1kaXN0YW5jZSBoaWtpbmcgdHJhaWwuIERpdmlkZWQgaW50byAxNCBzZWdtZW50cywgaXQgdHJhdmVyc2VzIHZvbGNhbmljIGNyYXRlcnMsIGhvdCBzcHJpbmdzLCBhbmQgYm9pbGluZyBsYWtlcywgc2hvd2Nhc2luZyB0aGUgaXNsYW5kJ3MgZ2VvdGhlcm1hbCBhY3Rpdml0eS4gSGlrZXJzIGNhbiB2aXNpdCBpbmRpZ2Vub3VzIEthbGluYWdvIHZpbGxhZ2VzLCBoaXN0b3JpYyBmb3J0cywgYW5kIGNvbG9uaWFsIHBsYW50YXRpb25zLCBleHBlcmllbmNpbmcgZGl2ZXJzZSBlY29zeXN0ZW1zIGZyb20gcmFpbmZvcmVzdHMgdG8gY29hc3RhbCBhcmVhcy4gKFtsb25lbHlwbGFuZXQuY29tXShodHRwczovL3d3dy5sb25lbHlwbGFuZXQuY29tL2FydGljbGVzL2Jlc3QtaGlrZXMtaW4tY2FyaWJiZWFuP3V0bV9zb3VyY2U9b3BlbmFpKSlcblxuKio0LiBNb3VudCBTY2VuZXJ5LCBTYWJhKipcblxuTW91bnQgU2NlbmVyeSwgdGhlIGhpZ2hlc3QgcG9pbnQgaW4gdGhlIEtpbmdkb20gb2YgdGhlIE5ldGhlcmxhbmRzLCBvZmZlcnMgYSBzdHJlbnVvdXMgaGlrZSB1cCAxLDA2NCBzdGVwcyB0byBpdHMgc3VtbWl0LiBUaGUgdHJhaWwgcGFzc2VzIHRocm91Z2ggcmFpbmZvcmVzdCBhbmQgY2xvdWQgZm9yZXN0LCBmZWF0dXJpbmcgdHJlZSBmZXJucywgd2lsZCBwbGFudGFpbnMsIGFuZCBNb3VudGFpbiBQYWxtcy4gQXQgdGhlIHN1bW1pdCwgaGlrZXJzIGFyZSByZXdhcmRlZCB3aXRoIHBhbm9yYW1pYyB2aWV3cyBvZiB0aGUgaXNsYW5kLiAoW2VuLndpa2lwZWRpYS5vcmddKGh0dHBzOi8vZW4ud2lraXBlZGlhLm9yZy93aWtpL01vdW50X1NjZW5lcnk/dXRtX3NvdXJjZT1vcGVuYWkpKVxuXG4qKjUuIFNvdWZyacOocmUgVm9sY2FubyBUcmFpbCwgU2FpbnQgVmluY2VudCoqXG5cblRoZSBTb3VmcmnDqHJlIFZvbGNhbm8gVHJhaWwgbGVhZHMgdG8gdGhlIHN1bW1pdCBvZiBMYSBTb3VmcmnDqHJlLCBhbiBhY3RpdmUgc3RyYXRvdm9sY2Fuby4gVGhlIGNoYWxsZW5naW5nIGhpa2Ugb2ZmZXJzIHN0dW5uaW5nIHZpZXdzIG9mIHRoZSBpc2xhbmQncyBsdXNoIGxhbmRzY2FwZSBhbmQgdGhlIENhcmliYmVhbiBTZWEuIFRoZSB0cmFpbCBwYXNzZXMgdGhyb3VnaCBkZW5zZSByYWluZm9yZXN0LCBhbHBpbmUgbWVhZG93cywgYW5kIHZvbGNhbmljIHJvY2sgZm9ybWF0aW9ucywgd2l0aCBndWlkZWQgdG91cnMgcHJvdmlkaW5nIGluc2lnaHRzIGludG8gdGhlIHZvbGNhbm8ncyBoaXN0b3J5IGFuZCBnZW9sb2d5LiAoW2NhcmlidW5lLmNvbV0oaHR0cHM6Ly93d3cuY2FyaWJ1bmUuY29tL2NhcmliYmVhbi1oaWtpbmctYW5kLW5hdHVyZS10cmFpbHMvP3V0bV9zb3VyY2U9b3BlbmFpKSlcblxuVGhlc2UgZGVzdGluYXRpb25zIG9mZmVyIGEgYmxlbmQgb2YgbmF0dXJhbCBiZWF1dHksIGN1bHR1cmFsIGV4cGVyaWVuY2VzLCBhbmQgdmFyeWluZyBsZXZlbHMgb2YgaGlraW5nIGNoYWxsZW5nZXMsIG1ha2luZyB0aGVtIGlkZWFsIGZvciBhbiBBcHJpbCBDYXJpYmJlYW4gYWR2ZW50dXJlLiAiLCJ0eXBlIjoib3V0cHV0X3RleHQiLCJsb2dwcm9icyI6W119XSwicm9sZSI6ImFzc2lzdGFudCIsInN0YXR1cyI6ImNvbXBsZXRlZCIsInR5cGUiOiJtZXNzYWdlIn1dLCJ1c2FnZSI6eyJyZXF1ZXN0cyI6MSwiaW5wdXRfdG9rZW5zIjo0MzksImlucHV0X3Rva2Vuc19kZXRhaWxzIjp7ImNhY2hlZF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5zIjo2MjMsIm91dHB1dF90b2tlbnNfZGV0YWlscyI6eyJyZWFzb25pbmdfdG9rZW5zIjowfSwidG90YWxfdG9rZW5zIjoxMDYyfSwicmVzcG9uc2VfaWQiOiJyZXNwXzY4NjJjMzg4NDk5YzgxYTE5YmZkYzE2YTkwNTRlMTFhMDk5MjRkZjYyMWFjZTllMCJ9" - } - ] - }, - "scheduledEventId": "12", - "startedEventId": "61", - "identity": "19167@Tims-MacBook-Pro.local" - } - }, - { - "eventId": "63", - "eventTime": "2025-06-30T17:04:16.487920Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", - "taskId": "1057202", - "workflowTaskScheduledEventAttributes": { - "taskQueue": { - "name": "19167@Tims-MacBook-Pro.local-f783ce7516bc48e59108b7b5cd31d0e5", - "kind": "TASK_QUEUE_KIND_STICKY", - "normalName": "fc97ba3c-6c32-4f4c-a986-8d7f4f5c7f80" - }, - "startToCloseTimeout": "10s", - "attempt": 1 - } - }, - { - "eventId": "64", - "eventTime": "2025-06-30T17:04:16.490618Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", - "taskId": "1057206", - "workflowTaskStartedEventAttributes": { - "scheduledEventId": "63", - "identity": "19167@Tims-MacBook-Pro.local", - "requestId": "8d7affff-d2aa-4ba2-b5f4-292116da9943", - "historySizeBytes": "53223", - "workerVersion": { - "buildId": "9f32b5d9789ece25bfb8515a7023ec78" - } - } - }, - { - "eventId": "65", - "eventTime": "2025-06-30T17:04:16.512689Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", - "taskId": "1057210", - "workflowTaskCompletedEventAttributes": { - "scheduledEventId": "63", - "startedEventId": "64", - "identity": "19167@Tims-MacBook-Pro.local", - "workerVersion": { - "buildId": "9f32b5d9789ece25bfb8515a7023ec78" - }, - "sdkMetadata": {}, - "meteringMetadata": {} - } - }, - { - "eventId": "66", - "eventTime": "2025-06-30T17:04:08.056909Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_STARTED", - "taskId": "1057212", - "activityTaskStartedEventAttributes": { - "scheduledEventId": "16", - "identity": "19167@Tims-MacBook-Pro.local", - "requestId": "908ff46c-062a-47dd-aaa4-758df766d4f4", - "attempt": 1, - "workerVersion": { - "buildId": "9f32b5d9789ece25bfb8515a7023ec78" - } - } - }, - { - "eventId": "67", - "eventTime": "2025-06-30T17:04:23.897293Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_COMPLETED", - "taskId": "1057213", - "activityTaskCompletedEventAttributes": { - "result": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJvdXRwdXQiOlt7ImlkIjoid3NfNjg2MmMzODg0YTk4ODFhMGE1ZTE2NzhhMWE4MWVlZDEwNmY1MDA1M2RmMTljYTdiIiwiYWN0aW9uIjp7InF1ZXJ5IjoiU2VhcmNoIHRlcm06IENhcmliYmVhbiB3ZWF0aGVyIEFwcmlsIHN1cmZpbmcgaGlraW5nXFxuUmVhc29uIGZvciBzZWFyY2hpbmc6IFRvIGxlYXJuIGFib3V0IHdlYXRoZXIgY29uZGl0aW9ucyBpbiB0aGUgQ2FyaWJiZWFuIGluIEFwcmlsLCBmb2N1c2luZyBvbiBvdXRkb29yIGFjdGl2aXRpZXMuIiwidHlwZSI6InNlYXJjaCJ9LCJzdGF0dXMiOiJjb21wbGV0ZWQiLCJ0eXBlIjoid2ViX3NlYXJjaF9jYWxsIn0seyJpZCI6Im1zZ182ODYyYzM5NTAwMTg4MWEwODkzYTA0YWVhYjJhNDQzYjA2ZjUwMDUzZGYxOWNhN2IiLCJjb250ZW50IjpbeyJhbm5vdGF0aW9ucyI6W3siZW5kX2luZGV4Ijo1MDEsInN0YXJ0X2luZGV4Ijo0MDgsInRpdGxlIjoiQXByaWwgaW4gdGhlIENhcmliYmVhbjogV2VhdGhlciBhbmQgRXZlbnQgR3VpZGUiLCJ0eXBlIjoidXJsX2NpdGF0aW9uIiwidXJsIjoiaHR0cHM6Ly93d3cudHJpcHNhdnZ5LmNvbS9hcHJpbC1pbi10aGUtY2FyaWJiZWFuLTE0ODc2NTk/dXRtX3NvdXJjZT1vcGVuYWkifSx7ImVuZF9pbmRleCI6OTQ0LCJzdGFydF9pbmRleCI6ODM1LCJ0aXRsZSI6IkJlc3QgVGltZSBUbyBWaXNpdCBDYXJpYmJlYW4gfCBCYWNrcm9hZHMiLCJ0eXBlIjoidXJsX2NpdGF0aW9uIiwidXJsIjoiaHR0cHM6Ly93d3cuYmFja3JvYWRzLmNvbS9wcm8tdGlwcy90cmF2ZWwtZ3VpZGVzL3doZW4tdG8tdmlzaXQvY2FyaWJiZWFuP3V0bV9zb3VyY2U9b3BlbmFpIn0seyJlbmRfaW5kZXgiOjEzNzQsInN0YXJ0X2luZGV4IjoxMjY1LCJ0aXRsZSI6IkJlc3QgVGltZSBUbyBWaXNpdCBDYXJpYmJlYW4gfCBCYWNrcm9hZHMiLCJ0eXBlIjoidXJsX2NpdGF0aW9uIiwidXJsIjoiaHR0cHM6Ly93d3cuYmFja3JvYWRzLmNvbS9wcm8tdGlwcy90cmF2ZWwtZ3VpZGVzL3doZW4tdG8tdmlzaXQvY2FyaWJiZWFuP3V0bV9zb3VyY2U9b3BlbmFpIn0seyJlbmRfaW5kZXgiOjE2NDQsInN0YXJ0X2luZGV4IjoxNTUxLCJ0aXRsZSI6IkFwcmlsIGluIHRoZSBDYXJpYmJlYW46IFdlYXRoZXIgYW5kIEV2ZW50IEd1aWRlIiwidHlwZSI6InVybF9jaXRhdGlvbiIsInVybCI6Imh0dHBzOi8vd3d3LnRyaXBzYXZ2eS5jb20vYXByaWwtaW4tdGhlLWNhcmliYmVhbi0xNDg3NjU5P3V0bV9zb3VyY2U9b3BlbmFpIn1dLCJ0ZXh0IjoiQXByaWwgaW4gdGhlIENhcmliYmVhbiBvZmZlcnMgd2FybSB0ZW1wZXJhdHVyZXMsIHR5cGljYWxseSByYW5naW5nIGZyb20gZGF5dGltZSBoaWdocyBpbiB0aGUgbWlkLTgwc8KwRiAoMjgtMzDCsEMpIHRvIG5pZ2h0dGltZSBsb3dzIGluIHRoZSBtaWQtNzBzwrBGICgyMC0yNMKwQykuIFRoaXMgcGVyaW9kIG1hcmtzIHRoZSBlbmQgb2YgdGhlIGRyeSBzZWFzb24sIHJlc3VsdGluZyBpbiBwcmVkb21pbmFudGx5IHN1bm55IGRheXMgd2l0aCBtaW5pbWFsIHJhaW5mYWxsLCBtYWtpbmcgaXQgaWRlYWwgZm9yIG91dGRvb3IgYWN0aXZpdGllcy4gV2F0ZXIgdGVtcGVyYXR1cmVzIGFyZSBhbHNvIHdhcm0sIGF2ZXJhZ2luZyBhcm91bmQgODDCsEYgKDI3wrBDKSwgcGVyZmVjdCBmb3Igc3dpbW1pbmcgYW5kIHdhdGVyIHNwb3J0cy4gKFt0cmlwc2F2dnkuY29tXShodHRwczovL3d3dy50cmlwc2F2dnkuY29tL2FwcmlsLWluLXRoZS1jYXJpYmJlYW4tMTQ4NzY1OT91dG1fc291cmNlPW9wZW5haSkpXG5cbkZvciBzdXJmaW5nIGVudGh1c2lhc3RzLCB0aGUgQ2FyaWJiZWFuIHByb3ZpZGVzIGZhdm9yYWJsZSBjb25kaXRpb25zIGluIEFwcmlsLiBXaGlsZSB0aGUgbGFyZ2VzdCBzd2VsbHMgb2NjdXIgZHVyaW5nIHRoZSBsYXRlIHJhaW55IHNlYXNvbiwgdGhlIHBlcmlvZCBmcm9tIE5vdmVtYmVyIHRvIEFwcmlsIG9mZmVycyBjb25zaXN0ZW50IHdhdmVzIHN1aXRhYmxlIGZvciBzdXJmZXJzIG9mIHZhcmlvdXMgc2tpbGwgbGV2ZWxzLiBOb3RhYmxlIHN1cmZpbmcgZGVzdGluYXRpb25zIGluY2x1ZGUgUHVlcnRvIFJpY28sIHRoZSBEb21pbmljYW4gUmVwdWJsaWMsIGFuZCBCYXJiYWRvcy4gKFtiYWNrcm9hZHMuY29tXShodHRwczovL3d3dy5iYWNrcm9hZHMuY29tL3Byby10aXBzL3RyYXZlbC1ndWlkZXMvd2hlbi10by12aXNpdC9jYXJpYmJlYW4/dXRtX3NvdXJjZT1vcGVuYWkpKVxuXG5IaWtpbmcgb3Bwb3J0dW5pdGllcyBhYm91bmQgaW4gdGhlIENhcmliYmVhbidzIG1vdW50YWlub3VzIHJlZ2lvbnMuIERlc3RpbmF0aW9ucyBzdWNoIGFzIEVsIFl1bnF1ZSBpbiBQdWVydG8gUmljbywgdGhlIEJsdWUgTW91bnRhaW5zIGluIEphbWFpY2EsIGFuZCB0aGUgYXJlYSBhcm91bmQgSmFyYWJhY29hIGluIHRoZSBEb21pbmljYW4gUmVwdWJsaWMgb2ZmZXIgZXhjZWxsZW50IHRyYWlscy4gQXByaWwncyB3YXJtIGFuZCBkcnkgd2VhdGhlciBwcm92aWRlcyBjb21mb3J0YWJsZSBjb25kaXRpb25zIGZvciBleHBsb3JpbmcgdGhlc2UgbGFuZHNjYXBlcy4gKFtiYWNrcm9hZHMuY29tXShodHRwczovL3d3dy5iYWNrcm9hZHMuY29tL3Byby10aXBzL3RyYXZlbC1ndWlkZXMvd2hlbi10by12aXNpdC9jYXJpYmJlYW4/dXRtX3NvdXJjZT1vcGVuYWkpKVxuXG5BZGRpdGlvbmFsbHksIEFwcmlsIGhvc3RzIHZhcmlvdXMgY3VsdHVyYWwgZXZlbnRzIGFjcm9zcyB0aGUgQ2FyaWJiZWFuLCBpbmNsdWRpbmcgRWFzdGVyIGNlbGVicmF0aW9ucyBhbmQgcmVnYXR0YXMgbGlrZSBBbnRpZ3VhIFNhaWxpbmcgV2VlaywgZW5oYW5jaW5nIHRoZSB0cmF2ZWwgZXhwZXJpZW5jZS4gKFt0cmlwc2F2dnkuY29tXShodHRwczovL3d3dy50cmlwc2F2dnkuY29tL2FwcmlsLWluLXRoZS1jYXJpYmJlYW4tMTQ4NzY1OT91dG1fc291cmNlPW9wZW5haSkpICIsInR5cGUiOiJvdXRwdXRfdGV4dCIsImxvZ3Byb2JzIjpbXX1dLCJyb2xlIjoiYXNzaXN0YW50Iiwic3RhdHVzIjoiY29tcGxldGVkIiwidHlwZSI6Im1lc3NhZ2UifV0sInVzYWdlIjp7InJlcXVlc3RzIjoxLCJpbnB1dF90b2tlbnMiOjQ0NywiaW5wdXRfdG9rZW5zX2RldGFpbHMiOnsiY2FjaGVkX3Rva2VucyI6MH0sIm91dHB1dF90b2tlbnMiOjM1Nywib3V0cHV0X3Rva2Vuc19kZXRhaWxzIjp7InJlYXNvbmluZ190b2tlbnMiOjB9LCJ0b3RhbF90b2tlbnMiOjgwNH0sInJlc3BvbnNlX2lkIjoicmVzcF82ODYyYzM4ODQ0Mzg4MWEwOWFhZDhjMDA5YTA4NTRlYjA2ZjUwMDUzZGYxOWNhN2IifQ==" - } - ] - }, - "scheduledEventId": "16", - "startedEventId": "66", - "identity": "19167@Tims-MacBook-Pro.local" - } - }, - { - "eventId": "68", - "eventTime": "2025-06-30T17:04:23.897299Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", - "taskId": "1057214", - "workflowTaskScheduledEventAttributes": { - "taskQueue": { - "name": "19167@Tims-MacBook-Pro.local-f783ce7516bc48e59108b7b5cd31d0e5", - "kind": "TASK_QUEUE_KIND_STICKY", - "normalName": "fc97ba3c-6c32-4f4c-a986-8d7f4f5c7f80" - }, - "startToCloseTimeout": "10s", - "attempt": 1 - } - }, - { - "eventId": "69", - "eventTime": "2025-06-30T17:04:23.898956Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", - "taskId": "1057218", - "workflowTaskStartedEventAttributes": { - "scheduledEventId": "68", - "identity": "19167@Tims-MacBook-Pro.local", - "requestId": "18f86b97-7a89-42c6-b337-73336ac8bd84", - "historySizeBytes": "57064", - "workerVersion": { - "buildId": "9f32b5d9789ece25bfb8515a7023ec78" - } - } - }, - { - "eventId": "70", - "eventTime": "2025-06-30T17:04:23.918626Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", - "taskId": "1057222", - "workflowTaskCompletedEventAttributes": { - "scheduledEventId": "68", - "startedEventId": "69", - "identity": "19167@Tims-MacBook-Pro.local", - "workerVersion": { - "buildId": "9f32b5d9789ece25bfb8515a7023ec78" - }, - "sdkMetadata": {}, - "meteringMetadata": {} - } - }, - { - "eventId": "71", - "eventTime": "2025-06-30T17:04:23.918651Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_SCHEDULED", - "taskId": "1057223", - "activityTaskScheduledEventAttributes": { - "activityId": "12", - "activityType": { - "name": "invoke_model_activity" - }, - "taskQueue": { - "name": "fc97ba3c-6c32-4f4c-a986-8d7f4f5c7f80", - "kind": "TASK_QUEUE_KIND_NORMAL" - }, - "header": { - "fields": { - "__openai_span": { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJ0cmFjZU5hbWUiOiJSZXNlYXJjaCB0cmFjZSIsInNwYW5JZCI6InNwYW5fNjlmM2NiZTkyZDJlNGU3NGEyNDFjYmY1IiwidHJhY2VJZCI6InRyYWNlXzUyYjBiNjQ1MjNmMDQ3ZDc5MTI2YTc2OTY5ZGIzMTllIn0=" - } - } - }, - "input": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJtb2RlbF9uYW1lIjpudWxsLCJzeXN0ZW1faW5zdHJ1Y3Rpb25zIjoiWW91IGFyZSBhIHNlbmlvciByZXNlYXJjaGVyIHRhc2tlZCB3aXRoIHdyaXRpbmcgYSBjb2hlc2l2ZSByZXBvcnQgZm9yIGEgcmVzZWFyY2ggcXVlcnkuIFlvdSB3aWxsIGJlIHByb3ZpZGVkIHdpdGggdGhlIG9yaWdpbmFsIHF1ZXJ5LCBhbmQgc29tZSBpbml0aWFsIHJlc2VhcmNoIGRvbmUgYnkgYSByZXNlYXJjaCBhc3Npc3RhbnQuXG5Zb3Ugc2hvdWxkIGZpcnN0IGNvbWUgdXAgd2l0aCBhbiBvdXRsaW5lIGZvciB0aGUgcmVwb3J0IHRoYXQgZGVzY3JpYmVzIHRoZSBzdHJ1Y3R1cmUgYW5kIGZsb3cgb2YgdGhlIHJlcG9ydC4gVGhlbiwgZ2VuZXJhdGUgdGhlIHJlcG9ydCBhbmQgcmV0dXJuIHRoYXQgYXMgeW91ciBmaW5hbCBvdXRwdXQuXG5UaGUgZmluYWwgb3V0cHV0IHNob3VsZCBiZSBpbiBtYXJrZG93biBmb3JtYXQsIGFuZCBpdCBzaG91bGQgYmUgbGVuZ3RoeSBhbmQgZGV0YWlsZWQuIEFpbSBmb3IgNS0xMCBwYWdlcyBvZiBjb250ZW50LCBhdCBsZWFzdCAxMDAwIHdvcmRzLiIsImlucHV0IjpbeyJjb250ZW50IjoiT3JpZ2luYWwgcXVlcnk6IENhcmliYmVhbiB2YWNhdGlvbiBzcG90cyBpbiBBcHJpbCwgb3B0aW1pemluZyBmb3Igc3VyZmluZywgaGlraW5nIGFuZCB3YXRlciBzcG9ydHNcblN1bW1hcml6ZWQgc2VhcmNoIHJlc3VsdHM6IFtcIkluIEFwcmlsLCB0aGUgQ2FyaWJiZWFuIG9mZmVycyBhIHZhcmlldHkgb2Ygd2F0ZXIgc3BvcnRzIHN1aXRhYmxlIGZvciBlbnRodXNpYXN0cyBvZiBhbGwgbGV2ZWxzLiBTdXJmaW5nIGlzIHBhcnRpY3VsYXJseSBwb3B1bGFyIGR1cmluZyB0aGlzIG1vbnRoLCBhcyBpdCBmYWxscyB3aXRoaW4gdGhlIHBlYWsgc2Vhc29uIGZyb20gTm92ZW1iZXIgdG8gQXByaWwsIHByb3ZpZGluZyBjb25zaXN0ZW50IHdhdmVzIGZvciBib3RoIHNlYXNvbmVkIHN1cmZlcnMgYW5kIGJlZ2lubmVycy4gKFtlbGl0ZWlzbGFuZGhvbGlkYXlzLmNvbV0oaHR0cHM6Ly9lbGl0ZWlzbGFuZGhvbGlkYXlzLmNvbS9jYXJpYmJlYW4td2F0ZXJzcG9ydHMtZ3VpZGUvP3V0bV9zb3VyY2U9b3BlbmFpKSlcXG5cXG5LaXRlc3VyZmluZyBpcyBhbm90aGVyIGZhdm9yZWQgYWN0aXZpdHkgYWNyb3NzIHRoZSBDYXJpYmJlYW4gaXNsYW5kcyBpbiBBcHJpbCwgdGhhbmtzIHRvIHN0ZWFkeSB3aW5kcyBhbmQgaWRlYWwgd2F2ZSBjb25kaXRpb25zLCBtYWtpbmcgaXQgYWNjZXNzaWJsZSBmb3IgYm90aCBub3ZpY2VzIGFuZCBleHBlcnRzLiBTbm9ya2VsaW5nIGFsc28gdGhyaXZlcyBkdXJpbmcgdGhpcyBwZXJpb2QsIHdpdGggY2xlYXIgd2F0ZXJzIGFuZCBhYnVuZGFudCBtYXJpbmUgbGlmZSBvZmZlcmluZyB1bmlxdWUgdW5kZXJ3YXRlciBleHBlcmllbmNlcy4gKFtlbGl0ZWlzbGFuZGhvbGlkYXlzLmNvbV0oaHR0cHM6Ly9lbGl0ZWlzbGFuZGhvbGlkYXlzLmNvbS9jYXJpYmJlYW4td2F0ZXJzcG9ydHMtZ3VpZGUvP3V0bV9zb3VyY2U9b3BlbmFpKSlcXG5cXG5TYWlsaW5nIHJlbWFpbnMgYSB0aW1lbGVzcyBwdXJzdWl0IGluIHRoZSBDYXJpYmJlYW4sIHdpdGggQXByaWwncyBmYXZvcmFibGUgd2VhdGhlciBjb25kaXRpb25zIGVuaGFuY2luZyB0aGUgZXhwZXJpZW5jZS4gV2hldGhlciB5b3UncmUgbmF2aWdhdGluZyB0aGUgc2VhcyB3aXRoIGZhbWlseSBvciBmcmllbmRzLCB0aGUgcG9zc2liaWxpdHkgb2YgZW5jb3VudGVyaW5nIGRvbHBoaW5zIGFkZHMgdG8gdGhlIGFkdmVudHVyZS4gKFtlbGl0ZWlzbGFuZGhvbGlkYXlzLmNvbV0oaHR0cHM6Ly9lbGl0ZWlzbGFuZGhvbGlkYXlzLmNvbS9jYXJpYmJlYW4td2F0ZXJzcG9ydHMtZ3VpZGUvP3V0bV9zb3VyY2U9b3BlbmFpKSlcXG5cXG5PdmVyYWxsLCBBcHJpbCdzIGNsaW1hdGUgaW4gdGhlIENhcmliYmVhbiBpcyBjb25kdWNpdmUgdG8gYSB3aWRlIHJhbmdlIG9mIHdhdGVyIHNwb3J0cywgZW5zdXJpbmcgbWVtb3JhYmxlIGV4cGVyaWVuY2VzIGZvciB2aXNpdG9ycy4gXCIsIFwiSW4gQXByaWwsIHRoZSBDYXJpYmJlYW4gaG9zdHMgc2V2ZXJhbCBub3RhYmxlIHN1cmZpbmcgYW5kIHdhdGVyIHNwb3J0cyBldmVudHMuIFRoZSBCYXJiYWRvcyBTdXJmIFBybywgcGFydCBvZiB0aGUgV29ybGQgU3VyZiBMZWFndWUncyBRdWFsaWZ5aW5nIFNlcmllcywgdHlwaWNhbGx5IHRha2VzIHBsYWNlIGluIEFwcmlsIGF0IERyaWxsIEhhbGwgQmVhY2gsIGF0dHJhY3RpbmcgaW50ZXJuYXRpb25hbCBzdXJmZXJzLiBTaW1pbGFybHksIHRoZSBNYXJ0aW5pcXVlIFN1cmYgUHJvLCBhbm90aGVyIFdTTCBRdWFsaWZ5aW5nIFNlcmllcyBldmVudCwgaXMgdXN1YWxseSBoZWxkIGluIEFwcmlsIGF0IEJhc3NlLVBvaW50ZSwgZmVhdHVyaW5nIGJvdGggbWVuJ3MgYW5kIHdvbWVuJ3MgY29tcGV0aXRpb25zLiBBZGRpdGlvbmFsbHksIHRoZSBDYXJpYmJlYW4gU3VyZiBOZXR3b3JrIG9mdGVuIG9yZ2FuaXplcyByZWdpb25hbCBjb21wZXRpdGlvbnMgZHVyaW5nIHRoaXMgbW9udGgsIHRob3VnaCBzcGVjaWZpYyBkYXRlcyBhbmQgbG9jYXRpb25zIGNhbiB2YXJ5IGFubnVhbGx5LlxcblxcbkJleW9uZCBzdXJmaW5nLCB0aGUgQ2FyaWJiZWFuIG9mZmVycyB2YXJpb3VzIHdhdGVyIHNwb3J0cyBmZXN0aXZhbHMgaW4gQXByaWwuIFRoZSBTdC4gVGhvbWFzIEludGVybmF0aW9uYWwgUmVnYXR0YSBpbiB0aGUgVS5TLiBWaXJnaW4gSXNsYW5kcyBpcyBhIHByb21pbmVudCBzYWlsaW5nIGV2ZW50IGRyYXdpbmcgcGFydGljaXBhbnRzIHdvcmxkd2lkZS4gSW4gdGhlIEJhaGFtYXMsIHRoZSBOYXRpb25hbCBGYW1pbHkgSXNsYW5kIFJlZ2F0dGEgc2hvd2Nhc2VzIHRyYWRpdGlvbmFsIEJhaGFtaWFuIHNsb29wIHNhaWxpbmcuIFRoZXNlIGV2ZW50cyBoaWdobGlnaHQgdGhlIHJlZ2lvbidzIHJpY2ggbWFyaXRpbWUgY3VsdHVyZSBhbmQgcHJvdmlkZSBvcHBvcnR1bml0aWVzIGZvciBib3RoIGNvbXBldGl0aW9uIGFuZCBjdWx0dXJhbCBleGNoYW5nZS4gXCIsICdJbiBBcHJpbCAyMDE0LCB0cmF2ZWxlcnMgaGlnaGxpZ2h0ZWQgdGhlIE9jZWFuIEFkdmVudHVyZXMgU2FpbGluZyBBZHZlbnR1cmUgaW4gUHVudGEgQ2FuYSwgRG9taW5pY2FuIFJlcHVibGljLCBhcyBhIHN0YW5kb3V0IGV4cGVyaWVuY2UuIE9uZSByZXZpZXdlciBkZXNjcmliZWQgaXQgYXMgdGhlIFwiaGlnaGxpZ2h0IG9mIG91ciB0cmlwLFwiIHByYWlzaW5nIHRoZSBjcmV3XFwncyBob3NwaXRhbGl0eSBhbmQgdGhlIHZhcmlldHkgb2YgYWN0aXZpdGllcywgaW5jbHVkaW5nIHNub3JrZWxpbmcsIHdhdGVyIHZvbGxleWJhbGwgaW4gYSBuYXR1cmFsIHBvb2wsIGRhbmNpbmcgZW50ZXJ0YWlubWVudCwgYW5kIGEgdGFzdHkgbHVuY2guIFRoZXkgYXBwcmVjaWF0ZWQgdGhhdCB0aGUgZXhjdXJzaW9uIG9mZmVyZWQgbW9yZSB0aGFuIGp1c3QgZHJpbmtpbmcgYW5kIHNpZ2h0c2VlaW5nLCBwcm92aWRpbmcgYSB3ZWxsLXJvdW5kZWQgYWR2ZW50dXJlLiAoW3RyaXBhZHZpc29yLmNvbV0oaHR0cHM6Ly93d3cudHJpcGFkdmlzb3IuY29tL1Nob3dVc2VyUmV2aWV3cy1nMTQ3MjkzLWQyNjI0Nzg5LXIyMDYxMTQxMDUtT2NlYW5fQWR2ZW50dXJlc19TYWlsaW5nX0FkdmVudHVyZS1QdW50YV9DYW5hX0xhX0FsdGFncmFjaWFfUHJvdmluY2VfRG9taW5pY2FuX1IuaHRtbD91dG1fc291cmNlPW9wZW5haSkpXFxuXFxuQW5vdGhlciB0cmF2ZWxlciwgd2hvIGhhZCBwcmV2aW91c2x5IGJlZW4gb24gYSBzaW1pbGFyIGNydWlzZSBpbiBQdW50YSBDYW5hLCBhbHNvIGhhZCBhIGdyZWF0IHRpbWUgb24gdGhpcyBzYWlsaW5nIGFkdmVudHVyZS4gVGhlc2UgcmV2aWV3cyBzdWdnZXN0IHRoYXQgdGhlIE9jZWFuIEFkdmVudHVyZXMgU2FpbGluZyBBZHZlbnR1cmUgb2ZmZXJzIGEgbWVtb3JhYmxlIGFuZCBlbmdhZ2luZyBleHBlcmllbmNlIGZvciB2aXNpdG9ycyBzZWVraW5nIGFjdGl2ZSB2YWNhdGlvbnMgaW4gdGhlIENhcmliYmVhbiBkdXJpbmcgQXByaWwuIChbdHJpcGFkdmlzb3IuY29tXShodHRwczovL3d3dy50cmlwYWR2aXNvci5jb20vU2hvd1VzZXJSZXZpZXdzLWcxNDcyOTMtZDI2MjQ3ODktcjIwNjExNDEwNS1PY2Vhbl9BZHZlbnR1cmVzX1NhaWxpbmdfQWR2ZW50dXJlLVB1bnRhX0NhbmFfTGFfQWx0YWdyYWNpYV9Qcm92aW5jZV9Eb21pbmljYW5fUi5odG1sP3V0bV9zb3VyY2U9b3BlbmFpKSkgJywgJ0FwcmlsIGlzIGFuIGV4Y2VsbGVudCBtb250aCBmb3Igc3VyZmluZyBpbiB0aGUgQ2FyaWJiZWFuLCB3aXRoIGNvbnNpc3RlbnQgc3dlbGxzIGFuZCBmYXZvcmFibGUgd2VhdGhlciBjb25kaXRpb25zLiBUb3AgZGVzdGluYXRpb25zIGluY2x1ZGUgUmluY29uIGluIFB1ZXJ0byBSaWNvLCBrbm93biBmb3IgaXRzIHZhcmlldHkgb2YgYnJlYWtzIHN1aXRhYmxlIGZvciBhbGwgbGV2ZWxzOyBCYXRoc2hlYmEgaW4gQmFyYmFkb3MsIG9mZmVyaW5nIHBvd2VyZnVsIHdhdmVzIGF0IHRoZSBTb3VwIEJvd2w7IGFuZCBQbGF5YSBFbmN1ZW50cm8gaW4gdGhlIERvbWluaWNhbiBSZXB1YmxpYywgZmVhdHVyaW5nIGNvbnNpc3RlbnQgd2F2ZXMgYW5kIGEgdmlicmFudCBzdXJmIGNvbW11bml0eS4gT3RoZXIgbm90YWJsZSBzcG90cyBhcmUgQWd1YWRpbGxhIGluIFB1ZXJ0byBSaWNvLCB3aXRoIGl0cyBkaXZlcnNlIHN1cmYgYnJlYWtzOyBhbmQgTG9uZyBCYXkgaW4gSmFtYWljYSwgcHJvdmlkaW5nIG1lbGxvdyB3YXZlcyBpZGVhbCBmb3IgYmVnaW5uZXJzLlxcblxcblRoZXNlIGxvY2F0aW9ucyBvZmZlciBhIG1peCBvZiByZWVmIGFuZCBiZWFjaCBicmVha3MsIGNhdGVyaW5nIHRvIHN1cmZlcnMgb2YgdmFyeWluZyBza2lsbCBsZXZlbHMuIFRoZSB3YXJtIENhcmliYmVhbiB3YXRlcnMgYW5kIGNvbnNpc3RlbnQgc3dlbGxzIGR1cmluZyBBcHJpbCBtYWtlIHRoZXNlIHNwb3RzIHBhcnRpY3VsYXJseSBhcHBlYWxpbmcuIEFkZGl0aW9uYWxseSwgdGhlIGxvY2FsIHN1cmYgY3VsdHVyZXMgYW5kIGFtZW5pdGllcyBlbmhhbmNlIHRoZSBvdmVyYWxsIGV4cGVyaWVuY2UgZm9yIHZpc2l0aW5nIHN1cmZlcnMuICcsICdJbiBBcHJpbCwgdGhlIENhcmliYmVhbiBvZmZlcnMgYSBwbGV0aG9yYSBvZiBhZHZlbnR1cmUgYWN0aXZpdGllcyBzdWl0YWJsZSBmb3Igb3V0ZG9vciBlbnRodXNpYXN0cy4gVGhlIHJlZ2lvblxcJ3Mgd2FybSB0ZW1wZXJhdHVyZXMgYW5kIHJlbGF0aXZlbHkgZHJ5IGNvbmRpdGlvbnMgbWFrZSBpdCBpZGVhbCBmb3IgdmFyaW91cyBwdXJzdWl0cy5cXG5cXG4qKkhpa2luZyBhbmQgVHJla2tpbmcqKjogRGVzdGluYXRpb25zIGxpa2UgRG9taW5pY2EsIGtub3duIGFzIHRoZSBcIk5hdHVyZSBJc2xhbmQsXCIgcHJvdmlkZSBleHRlbnNpdmUgaGlraW5nIHRyYWlscyB0aHJvdWdoIHJhaW5mb3Jlc3RzIGFuZCB0byB2b2xjYW5pYyBwZWFrcy4gSmFtYWljYVxcJ3MgQmx1ZSBNb3VudGFpbnMgb2ZmZXIgY2hhbGxlbmdpbmcgdHJla3Mgd2l0aCByZXdhcmRpbmcgcGFub3JhbWljIHZpZXdzLlxcblxcbioqV2F0ZXIgU3BvcnRzKio6IFRoZSBCYWhhbWFzIGFuZCB0aGUgQ2F5bWFuIElzbGFuZHMgYXJlIHJlbm93bmVkIGZvciBzY3ViYSBkaXZpbmcgYW5kIHNub3JrZWxpbmcsIGZlYXR1cmluZyB2aWJyYW50IGNvcmFsIHJlZWZzIGFuZCBkaXZlcnNlIG1hcmluZSBsaWZlLiBQdWVydG8gUmljb1xcJ3MgUmluY29uIGlzIGEgaG90c3BvdCBmb3Igc3VyZmluZywgd2l0aCBjb25zaXN0ZW50IHdhdmVzIGR1cmluZyB0aGlzIHBlcmlvZC5cXG5cXG4qKlNhaWxpbmcgYW5kIEtheWFraW5nKio6IFRoZSBCcml0aXNoIFZpcmdpbiBJc2xhbmRzIGFyZSBhIHNhaWxvclxcJ3MgcGFyYWRpc2UsIG9mZmVyaW5nIGNhbG0gd2F0ZXJzIGFuZCBudW1lcm91cyBpc2xhbmRzIHRvIGV4cGxvcmUuIEluIEJlbGl6ZSwga2F5YWtpbmcgdGhyb3VnaCB0aGUgR3JlYXQgQmx1ZSBIb2xlIHByb3ZpZGVzIGEgdW5pcXVlIGFkdmVudHVyZSBleHBlcmllbmNlLlxcblxcbioqV2lsZGxpZmUgRXhwbG9yYXRpb24qKjogQXByaWwgaXMgcHJpbWUgdGltZSBmb3IgYmlyZHdhdGNoaW5nIGluIFRyaW5pZGFkIGFuZCBUb2JhZ28sIGhvbWUgdG8gc3BlY2llcyBsaWtlIHRoZSBTY2FybGV0IEliaXMuIEluIFN0LiBMdWNpYSwgdmlzaXRvcnMgY2FuIGV4cGVyaWVuY2UgdHVydGxlIHdhdGNoaW5nIGFzIGxlYXRoZXJiYWNrIHR1cnRsZXMgY29tZSBhc2hvcmUgdG8gbmVzdC5cXG5cXG4qKkN1bHR1cmFsIEZlc3RpdmFscyoqOiBUaGUgU3QuIFRob21hcyBDYXJuaXZhbCBpbiB0aGUgVS5TLiBWaXJnaW4gSXNsYW5kcyBvY2N1cnMgaW4gQXByaWwsIGJsZW5kaW5nIGN1bHR1cmFsIGZlc3Rpdml0aWVzIHdpdGggYWR2ZW50dXJlIG9wcG9ydHVuaXRpZXMuXFxuXFxuVHJhdmVsZXJzIHNob3VsZCBjb25zdWx0IHNwZWNpZmljIHRyYXZlbCBndWlkZXMgZm9yIGRldGFpbGVkIGluZm9ybWF0aW9uIG9uIHRoZXNlIGFjdGl2aXRpZXMsIGluY2x1ZGluZyBzYWZldHkgdGlwcywgZXF1aXBtZW50IHJlbnRhbHMsIGFuZCBsb2NhbCByZWd1bGF0aW9ucy4gJywgXCJGb3IgYW4gYWR2ZW50dXJlLWZvY3VzZWQsIGFsbC1pbmNsdXNpdmUgQ2FyaWJiZWFuIHZhY2F0aW9uIGluIEFwcmlsIHRoYXQgY29tYmluZXMgc3VyZmluZyBhbmQgaGlraW5nLCBjb25zaWRlciB0aGUgZm9sbG93aW5nIG9wdGlvbnM6XFxuXFxuKioxLiBTdC4gTHVjaWE6IENvY29udXQgQmF5IEJlYWNoIFJlc29ydCAmIFNwYSoqXFxuXFxuTG9jYXRlZCBvbiBTdC4gTHVjaWEncyBzb3V0aGVybiBjb2FzdCwgQ29jb251dCBCYXkgb2ZmZXJzIGEgcmFuZ2Ugb2YgYWR2ZW50dXJlIGFjdGl2aXRpZXMuIEd1ZXN0cyBjYW4gZW5nYWdlIGluIGtpdGUgc3VyZmluZywgaGlraW5nIHRocm91Z2ggcmFpbmZvcmVzdHMsIHNub3JrZWxpbmcsIHppcC1saW5pbmcsIGFuZCBvZmYtcm9hZGluZyBleGN1cnNpb25zLiBUaGUgcmVzb3J0IHByb3ZpZGVzIG5hdHVyZSBndWlkZXMgdG8gbGVhZCByYWluZm9yZXN0IGhpa2VzIGFuZCB3YXRlcmZhbGwgdmlzaXRzLiAoW2NiYXlyZXNvcnQuY29tXShodHRwczovL2NiYXlyZXNvcnQuY29tL2FjdGl2aXRpZXMvYWR2ZW50dXJlLWJlZ2lucy1jb2NvbnV0LWJheT91dG1fc291cmNlPW9wZW5haSkpXFxuXFxuKioyLiBEb21pbmljYTogV2FuZGVybHVzdCBDYXJpYmJlYW4gQWR2ZW50dXJlIFRyYXZlbCBCb3V0aXF1ZSBIb3RlbCoqXFxuXFxuV2FuZGVybHVzdCBDYXJpYmJlYW4gc3BlY2lhbGl6ZXMgaW4gYWR2ZW50dXJlIHRyYXZlbCBwYWNrYWdlcyBvbiB0aGUgaXNsYW5kIG9mIERvbWluaWNhLiBBY3Rpdml0aWVzIGluY2x1ZGUgaGlraW5nIHRvIHdhdGVyZmFsbHMsIHNub3JrZWxpbmcgaW4gcHJpc3RpbmUgcmVlZnMsIHNjdWJhIGRpdmluZywga2F5YWtpbmcsIGNhbnlvbmluZywgd2hhbGUgd2F0Y2hpbmcsIGJpcmQgd2F0Y2hpbmcsIGJpa2luZywgYW5kIGhvcnNlYmFjayByaWRpbmcuIFRoZSBob3RlbCBvZmZlcnMgcGVyc29uYWxpemVkIHNtYWxsIGdyb3VwIHBhY2thZ2VzIHRhaWxvcmVkIHRvIGFjdGl2ZSB0cmF2ZWxlcnMuIChbd2FuZGVybHVzdGNhcmliYmVhbi5jb21dKGh0dHBzOi8vd3d3LndhbmRlcmx1c3RjYXJpYmJlYW4uY29tL2NvcHktb2YtcGFja2FnZXM/dXRtX3NvdXJjZT1vcGVuYWkpKVxcblxcbioqMy4gQmVsaXplOiBTbGlja3JvY2sgQmVsaXplIEFkdmVudHVyZXMqKlxcblxcblNsaWNrcm9jayBvZmZlcnMgYWxsLWluY2x1c2l2ZSBhZHZlbnR1cmUgdmFjYXRpb24gcGFja2FnZXMgb24gTG9uZyBDYXllLCBwYXJ0IG9mIEdsb3ZlcuKAmXMgUmVlZiBBdG9sbC4gR3Vlc3RzIHN0YXkgaW4gYmVhY2hmcm9udCBjYWJhbmFzIGFuZCBwYXJ0aWNpcGF0ZSBpbiBhY3Rpdml0aWVzIHN1Y2ggYXMgc2VhIGtheWFraW5nLCBzbm9ya2VsaW5nLCB3aW5kc3VyZmluZywgc3VyZiBrYXlha2luZywgYW5kIHN0YW5kLXVwIHBhZGRsZWJvYXJkaW5nLiBUaGUgOC1uaWdodCBwYWNrYWdlIGluY2x1ZGVzIGEgbmlnaHQgYXQgUGVsaWNhbiBCZWFjaCBSZXNvcnQgYW5kIHNldmVuIG5pZ2h0cyBvbiB0aGUgaXNsYW5kLiAoW3NsaWNrcm9jay5jb21dKGh0dHBzOi8vc2xpY2tyb2NrLmNvbS9wYWNrYWdlcy8/dXRtX3NvdXJjZT1vcGVuYWkpKVxcblxcbioqV2VhdGhlciBDb25zaWRlcmF0aW9ucyBmb3IgQXByaWw6KipcXG5cXG4tICoqTW9udGVnbyBCYXksIEphbWFpY2E6KiogQXZlcmFnZSB0ZW1wZXJhdHVyZXMgcmFuZ2UgZnJvbSAyM8KwQyB0byAzM8KwQyAoNzPCsEYgdG8gOTHCsEYpLlxcblxcbi0gKipQdWVydG8gVmllam8sIENvc3RhIFJpY2E6KiogQXZlcmFnZSB0ZW1wZXJhdHVyZXMgcmFuZ2UgZnJvbSAyMMKwQyB0byAzNsKwQyAoNjjCsEYgdG8gOTfCsEYpLlxcblxcbi0gKipDYWJhcmV0ZSwgRG9taW5pY2FuIFJlcHVibGljOioqIEF2ZXJhZ2UgdGVtcGVyYXR1cmVzIHJhbmdlIGZyb20gMTXCsEMgdG8gMzPCsEMgKDU5wrBGIHRvIDkxwrBGKS5cXG5cXG5UaGVzZSBkZXN0aW5hdGlvbnMgb2ZmZXIgYSBibGVuZCBvZiBzdXJmaW5nIGFuZCBoaWtpbmcgYWN0aXZpdGllcyB3aXRoaW4gYWxsLWluY2x1c2l2ZSBwYWNrYWdlcywgY2F0ZXJpbmcgdG8gYWR2ZW50dXJlIGVudGh1c2lhc3RzIHNlZWtpbmcgYW4gYWN0aXZlIENhcmliYmVhbiB2YWNhdGlvbiBpbiBBcHJpbC4gXCIsIFwiU2V2ZXJhbCBDYXJpYmJlYW4gaXNsYW5kcyBvZmZlciBleGNlbGxlbnQgb3Bwb3J0dW5pdGllcyBmb3IgYm90aCBzdXJmaW5nIGFuZCBoaWtpbmcgaW4gQXByaWwuIEphbWFpY2EsIHBhcnRpY3VsYXJseSB0aGUgTW9udGVnbyBCYXkgYXJlYSwgcHJvdmlkZXMgd2FybSB0ZW1wZXJhdHVyZXMgcmFuZ2luZyBmcm9tIDIyLjnCsEMgdG8gMzMuNMKwQywgaWRlYWwgZm9yIG91dGRvb3IgYWN0aXZpdGllcy4gVGhlIGlzbGFuZCBmZWF0dXJlcyBzdXJmIHNwb3RzIGxpa2UgQm9zdG9uIEJheSBhbmQgTG9uZyBCYXksIGFuZCBoaWtpbmcgdHJhaWxzIGluIHRoZSBCbHVlIE1vdW50YWlucy5cXG5cXG5CYXJiYWRvcywgd2l0aCB0ZW1wZXJhdHVyZXMgYmV0d2VlbiAyMi44wrBDIGFuZCAzMS4xwrBDLCBpcyBrbm93biBmb3IgY29uc2lzdGVudCBzdXJmIGJyZWFrcyBzdWNoIGFzIFNvdXAgQm93bCBhbmQgRnJlaWdodHMgQmF5LiBUaGUgaXNsYW5kIGFsc28gb2ZmZXJzIGhpa2luZyBvcHBvcnR1bml0aWVzIGluIGFyZWFzIGxpa2UgdGhlIFNjb3RsYW5kIERpc3RyaWN0LlxcblxcblRoZSBEb21pbmljYW4gUmVwdWJsaWMsIGVzcGVjaWFsbHkgYXJvdW5kIFB1bnRhIENhbmEsIGV4cGVyaWVuY2VzIHRlbXBlcmF0dXJlcyBmcm9tIDIwLjPCsEMgdG8gMzEuNcKwQy4gU3VyZmluZyBpcyBwb3B1bGFyIGluIENhYmFyZXRlLCB3aGlsZSB0aGUgQ29yZGlsbGVyYSBDZW50cmFsIG1vdW50YWlucyBwcm92aWRlIGV4dGVuc2l2ZSBoaWtpbmcgdHJhaWxzLlxcblxcbkNvc3RhIFJpY2EncyBQdWVydG8gVmllam8gcmVnaW9uLCB3aXRoIHRlbXBlcmF0dXJlcyBiZXR3ZWVuIDIwLjDCsEMgYW5kIDM2LjHCsEMsIG9mZmVycyBzdXJmIHNwb3RzIGxpa2UgU2Fsc2EgQnJhdmEgYW5kIFBsYXlhIENvY2xlcywgYWxvbmdzaWRlIGhpa2luZyBpbiB0aGUgbmVhcmJ5IEdhbmRvY2EtTWFuemFuaWxsbyBXaWxkbGlmZSBSZWZ1Z2UuXFxuXFxuUHVlcnRvIFJpY28sIHBhcnRpY3VsYXJseSBTYW4gSnVhbiwgaGFzIHRlbXBlcmF0dXJlcyByYW5naW5nIGZyb20gMjIuMMKwQyB0byAzMS41wrBDLiBUaGUgaXNsYW5kIGZlYXR1cmVzIHN1cmYgc3BvdHMgbGlrZSBSaW5jb24gYW5kIElzYWJlbGEsIGFuZCBoaWtpbmcgdHJhaWxzIGluIEVsIFl1bnF1ZSBOYXRpb25hbCBGb3Jlc3QuXFxuXFxuVGhlc2UgaXNsYW5kcyBwcm92aWRlIGEgY29tYmluYXRpb24gb2Ygc3VyZmluZyBhbmQgaGlraW5nIGV4cGVyaWVuY2VzIHN1aXRhYmxlIGZvciBBcHJpbCdzIGNsaW1hdGUuIFwiLCBcIkluIHRoZSBDYXJpYmJlYW4sIG51bWVyb3VzIHdhdGVyIHNwb3J0cyBnZWFyIHJlbnRhbCBzZXJ2aWNlcyBhcmUgYXZhaWxhYmxlIHRvIGVuaGFuY2UgeW91ciBhcXVhdGljIGFkdmVudHVyZXMuIEZvciBpbnN0YW5jZSwgKipSaW5jb24gQ2FwaXRhbCBXYXRlciBTcG9ydHMqKiBvZmZlcnMgYSB2YXJpZXR5IG9mIGVxdWlwbWVudCwgaW5jbHVkaW5nIGpldCBza2lzLCBrYXlha3MsIHN1cmZib2FyZHMsIHBhZGRsZWJvYXJkcywgc25vcmtlbCBhbmQgc2N1YmEgZ2VhciwgZmlzaGluZyBlcXVpcG1lbnQsIGJlYWNoIGNoYWlycywgdW1icmVsbGFzLCBhbmQgYmFuYW5hIGJvYXRzLiBUaGV5IHJlY29tbWVuZCBjb250YWN0aW5nIHRoZW0gZGlyZWN0bHkgdG8gbWFrZSBhIHJlc2VydmF0aW9uLiAoW2NhcGl0YWx3YXRlcnNwb3J0cy5jb21dKGh0dHBzOi8vd3d3LmNhcGl0YWx3YXRlcnNwb3J0cy5jb20vcmVudGFscz91dG1fc291cmNlPW9wZW5haSkpXFxuXFxuU2ltaWxhcmx5LCAqKlBsYWNlbmNpYSBBd2Vzb21lIFdhdGVyIFNwb3J0cyoqIGluIEJlbGl6ZSBwcm92aWRlcyByZW50YWxzIGZvciBqZXQgc2tpcywga2F5YWtzLCBwYWRkbGVib2FyZHMsIGFuZCBzbm9ya2VsIGdlYXIuIFRoZWlyIGpldCBza2lzIGNhbiBhY2NvbW1vZGF0ZSB0d28gYWR1bHRzIGFuZCBhcmUgYXZhaWxhYmxlIGZvciByZW50IHRocm91Z2hvdXQgdGhlIHllYXIuIFRoZXkgYWxzbyBvZmZlciBob3VybHksIGhhbGYtZGF5LCBmdWxsLWRheSwgYW5kIHdlZWtseSByZW50YWxzIGZvciBwYWRkbGVib2FyZHMgYW5kIGtheWFrcywgd2l0aCBkZWxpdmVyeSBvcHRpb25zIGZvciB3ZWVrbHkgcmVudGFscy4gKFtwbGFjZW5jaWFhd2Vzb21ld2F0ZXJzcG9ydHMuY29tXShodHRwczovL3d3dy5wbGFjZW5jaWFhd2Vzb21ld2F0ZXJzcG9ydHMuY29tLz91dG1fc291cmNlPW9wZW5haSkpXFxuXFxuSW4gU2FpbnQtTWFydGluLCAqKkNhcmliYmVhbiBQYWRkbGluZyoqIHNwZWNpYWxpemVzIGluIGVjby1mcmllbmRseSBhZHZlbnR1cmVzLCBvZmZlcmluZyBrYXlhayBhbmQgcGFkZGxlYm9hcmQgcmVudGFscywgYXMgd2VsbCBhcyBzbm9ya2VsaW5nIGV4cGVyaWVuY2VzLiBUaGV5IHByb3ZpZGUgdG9wLXF1YWxpdHkgZXF1aXBtZW50IGFuZCBwZXJzb25hbGl6ZWQgYWR2ZW50dXJlIHBhY2thZ2VzIHRhaWxvcmVkIHRvIHZhcmlvdXMgc2tpbGwgbGV2ZWxzLiBUaGVpciBwcmltZSBsb2NhdGlvbiBvZmZlcnMgZGlyZWN0IGFjY2VzcyB0byBwaWN0dXJlc3F1ZSBzcG90cyBsaWtlIFBpbmVsIElzbGFuZC4gKFtjYXJpYmJlYW5wYWRkbGluZy5jb21dKGh0dHBzOi8vd3d3LmNhcmliYmVhbnBhZGRsaW5nLmNvbS8/dXRtX3NvdXJjZT1vcGVuYWkpKVxcblxcbkZvciB0aG9zZSB2aXNpdGluZyBTdC4gVGhvbWFzLCAqKklzbGFuZCBBZHZlbnR1cmVzKiogb2ZmZXJzIHNub3JrZWwgZ2VhciwgY29vbGVycywgbm9vZGxlcywgZmxvYXQgbWF0cywgYW5kIGZvbGRpbmcgYmVhY2ggY2hhaXJzIGZvciByZW50LiBUaGV5IGhhdmUgcGljay11cCBsb2NhdGlvbnMgYXQgTWFyZ2FyaXRhdmlsbGUgUmVzb3J0IGFuZCBFbHlzaWFuIFJlc29ydC4gKFtmdW5ib2F0ZGF5LmNvbV0oaHR0cHM6Ly9mdW5ib2F0ZGF5LmNvbS93YXRlci1zcG9ydC1yZW50YWxzLz91dG1fc291cmNlPW9wZW5haSkpXFxuXFxuSW4gU3QuIEtpdHRzLCAqKlN0LiBLaXR0cyBXYXRlciBTcG9ydHMqKiBwcm92aWRlcyBhIHdpZGUgcmFuZ2Ugb2YgYWN0aXZpdGllcywgaW5jbHVkaW5nIGpldCBza2kgcmVudGFscywgZmx5Ym9hcmRpbmcsIHBhZGRsZWJvYXJkaW5nLCBrYXlha2luZywgYW5kIHNub3JrZWxpbmcuIFRoZXkgb2ZmZXIgdmFyaW91cyBwYWNrYWdlcywgc3VjaCBhcyB0aGUgQWxsIERheSBBY3Rpb24gUGFzcywgd2hpY2ggaW5jbHVkZXMgbXVsdGlwbGUgYWN0aXZpdGllcyBpbiBvbmUgZGF5LiAoW3N0a2l0dHN3YXRlcnNwb3J0cy5jb21dKGh0dHBzOi8vd3d3LnN0a2l0dHN3YXRlcnNwb3J0cy5jb20vP3V0bV9zb3VyY2U9b3BlbmFpKSlcXG5cXG5JbiBUb3J0b2xhLCAqKklzbGFuZCBTdXJmICYgU2FpbCoqIG9mZmVycyBhIHZhcmlldHkgb2Ygd2F0ZXIgdG95cyBhbmQgc3BvcnRzIGVxdWlwbWVudCBmb3IgcmVudCwgaW5jbHVkaW5nIGtheWFrcywgc3RhbmQtdXAgcGFkZGxlYm9hcmRzLCBzdXJmYm9hcmRzLCBmaXNoaW5nIGdlYXIsIHNub3JrZWxpbmcgZXF1aXBtZW50LCBhbmQgZmxvYXRzLiBUaGV5IHByb3ZpZGUgZnJlZSBkZWxpdmVyeSB0byBjaGFydGVyIGNvbXBhbmllcywgaG90ZWxzLCB2aWxsYXMsIGFuZCBiZWFjaGVzIGlzbGFuZC13aWRlLiAoW2J2aXRyYXZlbGxlci5jb21dKGh0dHBzOi8vd3d3LmJ2aXRyYXZlbGxlci5jb20vd2F0ZXJzcG9ydHMvaXNsYW5kLXN1cmYtc2FpbC8/dXRtX3NvdXJjZT1vcGVuYWkpKVxcblxcbkluIFB1ZXJ0byBSaWNvLCAqKkFxdWF0aWNhIFVuZGVyd2F0ZXIgQWR2ZW50dXJlcyoqIG9mZmVycyBzY3ViYSBhbmQgc25vcmtlbCBnZWFyIHJlbnRhbHMgYW5kIHNhbGVzLCBpbmNsdWRpbmcgbWFza3MsIHNub3JrZWxzLCBmaW5zLCBCQ0RzLCByZWd1bGF0b3JzLCBhbmQgdGFua3MuIFRoZXkgYXJlIGNvbnZlbmllbnRseSBsb2NhdGVkIG5lYXIgc2V2ZXJhbCBwb3B1bGFyIGJlYWNoZXMgYW5kIHByb3ZpZGUgaGlnaC1xdWFsaXR5LCBzYWZldHktdGVzdGVkIGVxdWlwbWVudC4gKFthcXVhdGljYXByLmNvbV0oaHR0cHM6Ly93d3cuYXF1YXRpY2Fwci5jb20vcmVudGFscy8/dXRtX3NvdXJjZT1vcGVuYWkpKVxcblxcblJlZ2FyZGluZyBoaWtpbmcgZ2VhciByZW50YWxzLCBzcGVjaWZpYyBpbmZvcm1hdGlvbiBpcyBsaW1pdGVkLiBJdCdzIGFkdmlzYWJsZSB0byBjb250YWN0IGxvY2FsIHRvdXIgb3BlcmF0b3JzIG9yIG91dGRvb3IgYWR2ZW50dXJlIGNvbXBhbmllcyBpbiB5b3VyIGNob3NlbiBkZXN0aW5hdGlvbiB0byBpbnF1aXJlIGFib3V0IHRoZSBhdmFpbGFiaWxpdHkgb2YgaGlraW5nIGVxdWlwbWVudCByZW50YWxzLiBcIiwgXCJBcHJpbCBpcyBhbiBleGNlbGxlbnQgdGltZSB0byBleHBsb3JlIHRoZSBDYXJpYmJlYW4ncyBkaXZlcnNlIGhpa2luZyBkZXN0aW5hdGlvbnMsIG9mZmVyaW5nIGZhdm9yYWJsZSB3ZWF0aGVyIGNvbmRpdGlvbnMgYW5kIHZpYnJhbnQgbmF0dXJhbCBsYW5kc2NhcGVzLiBIZXJlIGFyZSBzb21lIHRvcCBoaWtpbmcgc3BvdHMgdG8gY29uc2lkZXI6XFxuXFxuKioxLiBFbCBZdW5xdWUgTmF0aW9uYWwgRm9yZXN0LCBQdWVydG8gUmljbyoqXFxuXFxuQXMgdGhlIG9ubHkgdHJvcGljYWwgcmFpbmZvcmVzdCBpbiB0aGUgVS5TLiBOYXRpb25hbCBGb3Jlc3QgU3lzdGVtLCBFbCBZdW5xdWUgc3BhbnMgYXBwcm94aW1hdGVseSAyOCwwMDAgYWNyZXMgd2l0aCBhIG5ldHdvcmsgb2Ygd2VsbC1tYWludGFpbmVkIHRyYWlscy4gSGlrZXJzIGNhbiBleHBlcmllbmNlIGRpdmVyc2UgZmxvcmEgYW5kIGZhdW5hLCBpbmNsdWRpbmcgdGhlIFB1ZXJ0byBSaWNhbiBwYXJyb3QgYW5kIGNvcXXDrSBmcm9nLCBhbmQgdmlzaXQgc3R1bm5pbmcgd2F0ZXJmYWxscyBsaWtlIExhIE1pbmEgRmFsbHMuIE9ic2VydmF0aW9uIHRvd2VycyBzdWNoIGFzIFlva2FodSBUb3dlciBwcm92aWRlIHBhbm9yYW1pYyB2aWV3cyBvZiB0aGUgZm9yZXN0IGNhbm9weSBhbmQgY29hc3RsaW5lLiAoW2NhcmlidW5lLmNvbV0oaHR0cHM6Ly93d3cuY2FyaWJ1bmUuY29tL2NhcmliYmVhbi1oaWtpbmctYW5kLW5hdHVyZS10cmFpbHMvP3V0bV9zb3VyY2U9b3BlbmFpKSlcXG5cXG4qKjIuIEJsdWUgTW91bnRhaW4gUGVhaywgSmFtYWljYSoqXFxuXFxuU3RhbmRpbmcgYXQgNyw0MDIgZmVldCwgQmx1ZSBNb3VudGFpbiBQZWFrIGlzIEphbWFpY2EncyBoaWdoZXN0IHBvaW50LiBUaGUgaGlrZSB0byB0aGUgc3VtbWl0IGlzIHBvcHVsYXIgZm9yIGl0cyBicmVhdGh0YWtpbmcgc3VucmlzZSB2aWV3cywgd2hlcmUsIG9uIGNsZWFyIGRheXMsIHRoZSBkaXN0YW50IGNvYXN0bGluZSBvZiBDdWJhIGlzIHZpc2libGUuIFRoZSBhcmVhIGlzIGEgVU5FU0NPIFdvcmxkIEhlcml0YWdlIFNpdGUsIGhvbWUgdG8gZW5kZW1pYyBzcGVjaWVzIGxpa2UgdGhlIEphbWFpY2FuIHN3YWxsb3d0YWlsIGJ1dHRlcmZseSwgYW5kIGlzIHJlbm93bmVkIGZvciBpdHMgQmx1ZSBNb3VudGFpbiBDb2ZmZWUgcGxhbnRhdGlvbnMgb2ZmZXJpbmcgdG91cnMgYW5kIHRhc3RpbmdzLiAoW2NhcmlidW5lLmNvbV0oaHR0cHM6Ly93d3cuY2FyaWJ1bmUuY29tL2NhcmliYmVhbi1oaWtpbmctYW5kLW5hdHVyZS10cmFpbHMvP3V0bV9zb3VyY2U9b3BlbmFpKSlcXG5cXG4qKjMuIFdhaXR1a3VidWxpIE5hdGlvbmFsIFRyYWlsLCBEb21pbmljYSoqXFxuXFxuU3RyZXRjaGluZyAxMTUgbWlsZXMgYWNyb3NzIERvbWluaWNhLCB0aGUgV2FpdHVrdWJ1bGkgTmF0aW9uYWwgVHJhaWwgaXMgdGhlIENhcmliYmVhbidzIGZpcnN0IGxvbmctZGlzdGFuY2UgaGlraW5nIHRyYWlsLiBEaXZpZGVkIGludG8gMTQgc2VnbWVudHMsIGl0IHRyYXZlcnNlcyB2b2xjYW5pYyBjcmF0ZXJzLCBob3Qgc3ByaW5ncywgYW5kIGJvaWxpbmcgbGFrZXMsIHNob3djYXNpbmcgdGhlIGlzbGFuZCdzIGdlb3RoZXJtYWwgYWN0aXZpdHkuIEhpa2VycyBjYW4gdmlzaXQgaW5kaWdlbm91cyBLYWxpbmFnbyB2aWxsYWdlcywgaGlzdG9yaWMgZm9ydHMsIGFuZCBjb2xvbmlhbCBwbGFudGF0aW9ucywgZXhwZXJpZW5jaW5nIGRpdmVyc2UgZWNvc3lzdGVtcyBmcm9tIHJhaW5mb3Jlc3RzIHRvIGNvYXN0YWwgYXJlYXMuIChbbG9uZWx5cGxhbmV0LmNvbV0oaHR0cHM6Ly93d3cubG9uZWx5cGxhbmV0LmNvbS9hcnRpY2xlcy9iZXN0LWhpa2VzLWluLWNhcmliYmVhbj91dG1fc291cmNlPW9wZW5haSkpXFxuXFxuKio0LiBNb3VudCBTY2VuZXJ5LCBTYWJhKipcXG5cXG5Nb3VudCBTY2VuZXJ5LCB0aGUgaGlnaGVzdCBwb2ludCBpbiB0aGUgS2luZ2RvbSBvZiB0aGUgTmV0aGVybGFuZHMsIG9mZmVycyBhIHN0cmVudW91cyBoaWtlIHVwIDEsMDY0IHN0ZXBzIHRvIGl0cyBzdW1taXQuIFRoZSB0cmFpbCBwYXNzZXMgdGhyb3VnaCByYWluZm9yZXN0IGFuZCBjbG91ZCBmb3Jlc3QsIGZlYXR1cmluZyB0cmVlIGZlcm5zLCB3aWxkIHBsYW50YWlucywgYW5kIE1vdW50YWluIFBhbG1zLiBBdCB0aGUgc3VtbWl0LCBoaWtlcnMgYXJlIHJld2FyZGVkIHdpdGggcGFub3JhbWljIHZpZXdzIG9mIHRoZSBpc2xhbmQuIChbZW4ud2lraXBlZGlhLm9yZ10oaHR0cHM6Ly9lbi53aWtpcGVkaWEub3JnL3dpa2kvTW91bnRfU2NlbmVyeT91dG1fc291cmNlPW9wZW5haSkpXFxuXFxuKio1LiBTb3VmcmnDqHJlIFZvbGNhbm8gVHJhaWwsIFNhaW50IFZpbmNlbnQqKlxcblxcblRoZSBTb3VmcmnDqHJlIFZvbGNhbm8gVHJhaWwgbGVhZHMgdG8gdGhlIHN1bW1pdCBvZiBMYSBTb3VmcmnDqHJlLCBhbiBhY3RpdmUgc3RyYXRvdm9sY2Fuby4gVGhlIGNoYWxsZW5naW5nIGhpa2Ugb2ZmZXJzIHN0dW5uaW5nIHZpZXdzIG9mIHRoZSBpc2xhbmQncyBsdXNoIGxhbmRzY2FwZSBhbmQgdGhlIENhcmliYmVhbiBTZWEuIFRoZSB0cmFpbCBwYXNzZXMgdGhyb3VnaCBkZW5zZSByYWluZm9yZXN0LCBhbHBpbmUgbWVhZG93cywgYW5kIHZvbGNhbmljIHJvY2sgZm9ybWF0aW9ucywgd2l0aCBndWlkZWQgdG91cnMgcHJvdmlkaW5nIGluc2lnaHRzIGludG8gdGhlIHZvbGNhbm8ncyBoaXN0b3J5IGFuZCBnZW9sb2d5LiAoW2NhcmlidW5lLmNvbV0oaHR0cHM6Ly93d3cuY2FyaWJ1bmUuY29tL2NhcmliYmVhbi1oaWtpbmctYW5kLW5hdHVyZS10cmFpbHMvP3V0bV9zb3VyY2U9b3BlbmFpKSlcXG5cXG5UaGVzZSBkZXN0aW5hdGlvbnMgb2ZmZXIgYSBibGVuZCBvZiBuYXR1cmFsIGJlYXV0eSwgY3VsdHVyYWwgZXhwZXJpZW5jZXMsIGFuZCB2YXJ5aW5nIGxldmVscyBvZiBoaWtpbmcgY2hhbGxlbmdlcywgbWFraW5nIHRoZW0gaWRlYWwgZm9yIGFuIEFwcmlsIENhcmliYmVhbiBhZHZlbnR1cmUuIFwiLCBcIkFwcmlsIGluIHRoZSBDYXJpYmJlYW4gb2ZmZXJzIHdhcm0gdGVtcGVyYXR1cmVzLCB0eXBpY2FsbHkgcmFuZ2luZyBmcm9tIGRheXRpbWUgaGlnaHMgaW4gdGhlIG1pZC04MHPCsEYgKDI4LTMwwrBDKSB0byBuaWdodHRpbWUgbG93cyBpbiB0aGUgbWlkLTcwc8KwRiAoMjAtMjTCsEMpLiBUaGlzIHBlcmlvZCBtYXJrcyB0aGUgZW5kIG9mIHRoZSBkcnkgc2Vhc29uLCByZXN1bHRpbmcgaW4gcHJlZG9taW5hbnRseSBzdW5ueSBkYXlzIHdpdGggbWluaW1hbCByYWluZmFsbCwgbWFraW5nIGl0IGlkZWFsIGZvciBvdXRkb29yIGFjdGl2aXRpZXMuIFdhdGVyIHRlbXBlcmF0dXJlcyBhcmUgYWxzbyB3YXJtLCBhdmVyYWdpbmcgYXJvdW5kIDgwwrBGICgyN8KwQyksIHBlcmZlY3QgZm9yIHN3aW1taW5nIGFuZCB3YXRlciBzcG9ydHMuIChbdHJpcHNhdnZ5LmNvbV0oaHR0cHM6Ly93d3cudHJpcHNhdnZ5LmNvbS9hcHJpbC1pbi10aGUtY2FyaWJiZWFuLTE0ODc2NTk/dXRtX3NvdXJjZT1vcGVuYWkpKVxcblxcbkZvciBzdXJmaW5nIGVudGh1c2lhc3RzLCB0aGUgQ2FyaWJiZWFuIHByb3ZpZGVzIGZhdm9yYWJsZSBjb25kaXRpb25zIGluIEFwcmlsLiBXaGlsZSB0aGUgbGFyZ2VzdCBzd2VsbHMgb2NjdXIgZHVyaW5nIHRoZSBsYXRlIHJhaW55IHNlYXNvbiwgdGhlIHBlcmlvZCBmcm9tIE5vdmVtYmVyIHRvIEFwcmlsIG9mZmVycyBjb25zaXN0ZW50IHdhdmVzIHN1aXRhYmxlIGZvciBzdXJmZXJzIG9mIHZhcmlvdXMgc2tpbGwgbGV2ZWxzLiBOb3RhYmxlIHN1cmZpbmcgZGVzdGluYXRpb25zIGluY2x1ZGUgUHVlcnRvIFJpY28sIHRoZSBEb21pbmljYW4gUmVwdWJsaWMsIGFuZCBCYXJiYWRvcy4gKFtiYWNrcm9hZHMuY29tXShodHRwczovL3d3dy5iYWNrcm9hZHMuY29tL3Byby10aXBzL3RyYXZlbC1ndWlkZXMvd2hlbi10by12aXNpdC9jYXJpYmJlYW4/dXRtX3NvdXJjZT1vcGVuYWkpKVxcblxcbkhpa2luZyBvcHBvcnR1bml0aWVzIGFib3VuZCBpbiB0aGUgQ2FyaWJiZWFuJ3MgbW91bnRhaW5vdXMgcmVnaW9ucy4gRGVzdGluYXRpb25zIHN1Y2ggYXMgRWwgWXVucXVlIGluIFB1ZXJ0byBSaWNvLCB0aGUgQmx1ZSBNb3VudGFpbnMgaW4gSmFtYWljYSwgYW5kIHRoZSBhcmVhIGFyb3VuZCBKYXJhYmFjb2EgaW4gdGhlIERvbWluaWNhbiBSZXB1YmxpYyBvZmZlciBleGNlbGxlbnQgdHJhaWxzLiBBcHJpbCdzIHdhcm0gYW5kIGRyeSB3ZWF0aGVyIHByb3ZpZGVzIGNvbWZvcnRhYmxlIGNvbmRpdGlvbnMgZm9yIGV4cGxvcmluZyB0aGVzZSBsYW5kc2NhcGVzLiAoW2JhY2tyb2Fkcy5jb21dKGh0dHBzOi8vd3d3LmJhY2tyb2Fkcy5jb20vcHJvLXRpcHMvdHJhdmVsLWd1aWRlcy93aGVuLXRvLXZpc2l0L2NhcmliYmVhbj91dG1fc291cmNlPW9wZW5haSkpXFxuXFxuQWRkaXRpb25hbGx5LCBBcHJpbCBob3N0cyB2YXJpb3VzIGN1bHR1cmFsIGV2ZW50cyBhY3Jvc3MgdGhlIENhcmliYmVhbiwgaW5jbHVkaW5nIEVhc3RlciBjZWxlYnJhdGlvbnMgYW5kIHJlZ2F0dGFzIGxpa2UgQW50aWd1YSBTYWlsaW5nIFdlZWssIGVuaGFuY2luZyB0aGUgdHJhdmVsIGV4cGVyaWVuY2UuIChbdHJpcHNhdnZ5LmNvbV0oaHR0cHM6Ly93d3cudHJpcHNhdnZ5LmNvbS9hcHJpbC1pbi10aGUtY2FyaWJiZWFuLTE0ODc2NTk/dXRtX3NvdXJjZT1vcGVuYWkpKSBcIl0iLCJyb2xlIjoidXNlciJ9XSwibW9kZWxfc2V0dGluZ3MiOnsidGVtcGVyYXR1cmUiOm51bGwsInRvcF9wIjpudWxsLCJmcmVxdWVuY3lfcGVuYWx0eSI6bnVsbCwicHJlc2VuY2VfcGVuYWx0eSI6bnVsbCwidG9vbF9jaG9pY2UiOm51bGwsInBhcmFsbGVsX3Rvb2xfY2FsbHMiOm51bGwsInRydW5jYXRpb24iOm51bGwsIm1heF90b2tlbnMiOm51bGwsInJlYXNvbmluZyI6bnVsbCwibWV0YWRhdGEiOm51bGwsInN0b3JlIjpudWxsLCJpbmNsdWRlX3VzYWdlIjpudWxsLCJleHRyYV9xdWVyeSI6bnVsbCwiZXh0cmFfYm9keSI6bnVsbCwiZXh0cmFfaGVhZGVycyI6bnVsbCwiZXh0cmFfYXJncyI6bnVsbH0sInRvb2xzIjpbXSwib3V0cHV0X3NjaGVtYSI6eyJvdXRwdXRfdHlwZV9uYW1lIjoiUmVwb3J0RGF0YSIsImlzX3dyYXBwZWQiOmZhbHNlLCJvdXRwdXRfc2NoZW1hIjp7InByb3BlcnRpZXMiOnsic2hvcnRfc3VtbWFyeSI6eyJ0aXRsZSI6IlNob3J0IFN1bW1hcnkiLCJ0eXBlIjoic3RyaW5nIn0sIm1hcmtkb3duX3JlcG9ydCI6eyJ0aXRsZSI6Ik1hcmtkb3duIFJlcG9ydCIsInR5cGUiOiJzdHJpbmcifSwiZm9sbG93X3VwX3F1ZXN0aW9ucyI6eyJpdGVtcyI6eyJ0eXBlIjoic3RyaW5nIn0sInRpdGxlIjoiRm9sbG93IFVwIFF1ZXN0aW9ucyIsInR5cGUiOiJhcnJheSJ9fSwicmVxdWlyZWQiOlsic2hvcnRfc3VtbWFyeSIsIm1hcmtkb3duX3JlcG9ydCIsImZvbGxvd191cF9xdWVzdGlvbnMiXSwidGl0bGUiOiJSZXBvcnREYXRhIiwidHlwZSI6Im9iamVjdCIsImFkZGl0aW9uYWxQcm9wZXJ0aWVzIjpmYWxzZX0sInN0cmljdF9qc29uX3NjaGVtYSI6dHJ1ZX0sImhhbmRvZmZzIjpbXSwidHJhY2luZyI6MSwicHJldmlvdXNfcmVzcG9uc2VfaWQiOm51bGwsInByb21wdCI6bnVsbH0=" - } - ] - }, - "scheduleToCloseTimeout": "120s", - "scheduleToStartTimeout": "120s", - "startToCloseTimeout": "120s", - "heartbeatTimeout": "0s", - "workflowTaskCompletedEventId": "70", - "retryPolicy": { - "initialInterval": "1s", - "backoffCoefficient": 2.0, - "maximumInterval": "100s" - }, - "useWorkflowBuildId": true - }, - "userMetadata": { - "summary": { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "Ik9yaWdpbmFsIHF1ZXJ5OiBDYXJpYmJlYW4gdmFjYXRpb24gc3BvdHMgaW4gQXByaWwsIG9wdGltaXppbmcgZm9yIHN1cmZpbmcsIGhpa2luZyBhbmQgd2F0ZXIgc3BvcnRzXG5TIg==" - } - } - }, - { - "eventId": "72", - "eventTime": "2025-06-30T17:04:23.919583Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_STARTED", - "taskId": "1057227", - "activityTaskStartedEventAttributes": { - "scheduledEventId": "71", - "identity": "19167@Tims-MacBook-Pro.local", - "requestId": "eb8cb709-d324-46b5-a684-56f29f218ed6", - "attempt": 1, - "workerVersion": { - "buildId": "9f32b5d9789ece25bfb8515a7023ec78" - } - } - }, - { - "eventId": "73", - "eventTime": "2025-06-30T17:04:50.687490Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_COMPLETED", - "taskId": "1057228", - "activityTaskCompletedEventAttributes": { - "result": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJvdXRwdXQiOlt7ImlkIjoibXNnXzY4NjJjMzk4ZTcxODgxOWQ5Yzg1OTJjMWI3MGY2ZDM1MGRlNTlkZjk2NDhmZjBlMyIsImNvbnRlbnQiOlt7ImFubm90YXRpb25zIjpbXSwidGV4dCI6IntcInNob3J0X3N1bW1hcnlcIjpcIkFwcmlsIGlzIGFuIGV4Y2VsbGVudCB0aW1lIGZvciBhIENhcmliYmVhbiB2YWNhdGlvbiBmb2N1c2luZyBvbiBzdXJmaW5nLCBoaWtpbmcsIGFuZCB3YXRlciBzcG9ydHMuIFRvcCBzcG90cyBpbmNsdWRlIFJpbmNvbiBpbiBQdWVydG8gUmljbyBhbmQgQmF0aHNoZWJhIGluIEJhcmJhZG9zIGZvciBzdXJmaW5nLCB3aGlsZSBEb21pbmljYSdzIHRyYWlscyBhbmQgSmFtYWljYSdzIEJsdWUgTW91bnRhaW5zIG9mZmVyIHJlbWFya2FibGUgaGlraW5nLiBXYXRlciBhY3Rpdml0aWVzLCBzdWNoIGFzIGtpdGVzdXJmaW5nIGFuZCBzbm9ya2VsaW5nLCB0aHJpdmUgaW4gdGhpcyBtb250aCBkdWUgdG8gaWRlYWwgd2VhdGhlciBhbmQgbWFyaW5lIGNvbmRpdGlvbnMuIEN1bHR1cmFsIGZlc3RpdmFscyBhbmQgd2FybSB0ZW1wZXJhdHVyZXMgZnVydGhlciBlbnJpY2ggdGhlIGV4cGVyaWVuY2UuXCIsXCJtYXJrZG93bl9yZXBvcnRcIjpcIiMgQ2FyaWJiZWFuIFZhY2F0aW9uIFNwb3RzIGluIEFwcmlsOiBBIEd1aWRlIHRvIFN1cmZpbmcsIEhpa2luZywgYW5kIFdhdGVyIFNwb3J0cyBcXG5cXG4jIyBJbnRyb2R1Y3Rpb25cXG5cXG5BcHJpbCBpcyBhIHByaW1lIG1vbnRoIGZvciBhZHZlbnR1cmUgc2Vla2VycyBpbiB0aGUgQ2FyaWJiZWFuLCBwcm92aWRpbmcgYW4gb3B0aW1hbCBibGVuZCBvZiBjb25zaXN0ZW50IHN1cmZpbmcgd2F2ZXMsIHZpYnJhbnQgaGlraW5nIHRyYWlscywgYW5kIGRpdmVyc2Ugd2F0ZXIgc3BvcnRzLiBUaGUgcmVnaW9uJ3MgZmF2b3JhYmxlIGNsaW1hdGUsIGNoYXJhY3Rlcml6ZWQgYnkgd2FybSB0ZW1wZXJhdHVyZXMgYW5kIGRyeSBjb25kaXRpb25zLCBlbmhhbmNlcyB0aGVzZSBvdXRkb29yIGV4cGVyaWVuY2VzLiBUaGlzIHJlcG9ydCBleHBsb3JlcyB0aGUgdG9wIENhcmliYmVhbiBkZXN0aW5hdGlvbnMgaW4gQXByaWwgZm9yIHN1cmZpbmcsIGhpa2luZywgYW5kIGVuZ2FnaW5nIGluIHdhdGVyIHNwb3J0cywgZW5zdXJpbmcgdHJhdmVsZXJzIGNhbiBtYXhpbWl6ZSB0aGVpciB2YWNhdGlvbi5cXG5cXG4jIyBTdXJmaW5nIGluIHRoZSBDYXJpYmJlYW5cXG5cXG4jIyMgVG9wIFN1cmZpbmcgRGVzdGluYXRpb25zXFxuXFxuKioxLiBSaW5jb24sIFB1ZXJ0byBSaWNvKipcXG4gICAtIEtub3duIGZvciBhIHZhcmlldHkgb2YgYnJlYWtzIHN1aXRhYmxlIGZvciBhbGwgbGV2ZWxzLlxcbiAgIC0gT2ZmZXJzIGEgd2FybSBzdXJmIGNvbW11bml0eSBhbmQgY29uc2lzdGVudCBzd2VsbHMuXFxuXFxuKioyLiBCYXRoc2hlYmEsIEJhcmJhZG9zKipcXG4gICAtIEZhbW91cyBmb3IgdGhlIFNvdXAgQm93bCwgb2ZmZXJpbmcgcG93ZXJmdWwgd2F2ZXMuXFxuICAgLSBBdHRyYWN0cyBib3RoIGxvY2FsIGFuZCBpbnRlcm5hdGlvbmFsIHN1cmZlcnMuXFxuXFxuKiozLiBQbGF5YSBFbmN1ZW50cm8sIERvbWluaWNhbiBSZXB1YmxpYyoqXFxuICAgLSBGZWF0dXJlcyBjb25zaXN0ZW50IHdhdmVzIGFuZCBhIHZpYnJhbnQgc3VyZiBzY2VuZS5cXG5cXG4qKjQuIEFndWFkaWxsYSwgUHVlcnRvIFJpY28sIGFuZCBMb25nIEJheSwgSmFtYWljYSoqXFxuICAgLSBQcm92aWRlIGRpdmVyc2Ugc3VyZiBicmVha3MgYW5kIGJlZ2lubmVyLWZyaWVuZGx5IHdhdmVzLlxcblxcbiMjIyBTdXJmaW5nIEV2ZW50c1xcbiAgIC0gKipCYXJiYWRvcyBTdXJmIFBybyoqIGFuZCAqKk1hcnRpbmlxdWUgU3VyZiBQcm8qKiBhcmUgV1NMIGV2ZW50cyBvY2N1cnJpbmcgaW4gQXByaWwsIGRyYXdpbmcgaW50ZXJuYXRpb25hbCBjb21wZXRpdG9ycy5cXG5cXG4jIyBIaWtpbmcgQWR2ZW50dXJlc1xcblxcbiMjIyBOb3RhYmxlIEhpa2luZyBUcmFpbHNcXG5cXG4qKjEuIEVsIFl1bnF1ZSBOYXRpb25hbCBGb3Jlc3QsIFB1ZXJ0byBSaWNvKipcXG4gICAtIE9mZmVycyBhIG5ldHdvcmsgb2YgdHJhaWxzIHNob3djYXNpbmcgdW5pcXVlIGZsb3JhIGFuZCBmYXVuYS5cXG5cXG4qKjIuIEJsdWUgTW91bnRhaW4gUGVhaywgSmFtYWljYSoqXFxuICAgLSBQcm92aWRlcyBwYW5vcmFtaWMgc3VucmlzZSB2aWV3cyBhbmQgY3VsdHVyYWwgZW5jb3VudGVycy5cXG5cXG4qKjMuIFdhaXR1a3VidWxpIE5hdGlvbmFsIFRyYWlsLCBEb21pbmljYSoqXFxuICAgLSBTcGFucyAxMTUgbWlsZXMsIHRyYXZlcnNpbmcgdm9sY2FuaWMgYW5kIGN1bHR1cmFsIGxhbmRzY2FwZXMuXFxuXFxuIyMjIEhpa2luZyBUaXBzXFxuICAgLSBBcHJpbOKAmXMgd2VhdGhlciBwcm92aWRlcyBjb21mb3J0YWJsZSBjb25kaXRpb25zIGZvciBleHBsb3JpbmcgbW91bnRhaW5vdXMgYW5kIHJhaW5mb3Jlc3QgdHJhaWxzLiBcXG4gICAtIEVuZ2FnZSB3aXRoIGxvY2FsIGd1aWRlcyBmb3Igc2FmZSBhbmQgaW5mb3JtYXRpdmUgaGlrZXMuXFxuXFxuIyMgV2F0ZXIgU3BvcnRzIE9wcG9ydHVuaXRpZXNcXG5cXG4jIyMgUG9wdWxhciBBY3Rpdml0aWVzXFxuXFxuKioxLiBLaXRlc3VyZmluZyBhbmQgU25vcmtlbGluZyoqXFxuICAgLSBTdGVhZHkgd2luZHMgaW4gQXByaWwgcGVyZmVjdCBmb3Iga2l0ZXN1cmZpbmcsIGVzcGVjaWFsbHkgYXJvdW5kIENhYmFyZXRlIGluIHRoZSBEb21pbmljYW4gUmVwdWJsaWMuXFxuICAgLSBTbm9ya2VsaW5nIG9wcG9ydHVuaXRpZXMgYWJvdW5kLCB3aXRoIGNsZWFyIHdhdGVycyBob3N0aW5nIGRpdmVyc2UgbWFyaW5lIGxpZmUuXFxuXFxuKioyLiBTYWlsaW5nIGFuZCBLYXlha2luZyoqXFxuICAgLSBUaGUgQnJpdGlzaCBWaXJnaW4gSXNsYW5kcyBvZmZlciBleGNlcHRpb25hbCBzYWlsaW5nIGNvbmRpdGlvbnMuXFxuICAgLSBCZWxpemUncyBHcmVhdCBCbHVlIEhvbGUgaXMgYW4gaWNvbmljIGtheWFraW5nIGRlc3RpbmF0aW9uLlxcblxcbiMjIyBFcXVpcG1lbnQgUmVudGFsc1xcbiAgIC0gTnVtZXJvdXMgcmVudGFsIHNlcnZpY2VzLCBzdWNoIGFzIFJpbmNvbiBDYXBpdGFsIFdhdGVyIFNwb3J0cywgcHJvdmlkZSBhY2Nlc3MgdG8gamV0IHNraXMsIGtheWFrcywgYW5kIHNub3JrZWxpbmcgZ2Vhci5cXG5cXG4jIyBDdWx0dXJhbCBFdmVudHMgYW5kIExvY2FsIEluc2lnaHRzXFxuXFxuIyMjIEZlc3RpdmFscyBhbmQgQ2VsZWJyYXRpb25zXFxuXFxuKioxLiBTdC4gVGhvbWFzIEludGVybmF0aW9uYWwgUmVnYXR0YSwgVS5TLiBWaXJnaW4gSXNsYW5kcyoqXFxuICAgLSBBIGtleSBzYWlsaW5nIGV2ZW50IHByb21vdGluZyBjdWx0dXJhbCBleGNoYW5nZS5cXG5cXG4qKjIuIE5hdGlvbmFsIEZhbWlseSBJc2xhbmQgUmVnYXR0YSwgQmFoYW1hcyoqXFxuICAgLSBDZWxlYnJhdGVzIHRyYWRpdGlvbmFsIEJhaGFtaWFuIHNsb29wIHNhaWxpbmcuXFxuXFxuIyMjIExvY2FsIFJlY29tbWVuZGF0aW9uc1xcbiAgIC0gUGFydGljaXBhdGUgaW4gZ3VpZGVkIHRvdXJzIGFuZCBsb2NhbCBldmVudHMgdG8gZW5oYW5jZSBjdWx0dXJhbCB1bmRlcnN0YW5kaW5nIGFuZCBleHBlcmllbmNlLlxcblxcbiMjIFdlYXRoZXIgQ29uc2lkZXJhdGlvbnNcXG5cXG4tICoqVGVtcGVyYXR1cmUgT3ZlcnZpZXc6KipcXG4gIC0gQXZlcmFnZSBkYXkgdGVtcGVyYXR1cmVzIGluIHRoZSBtaWQtODBzwrBGICgyOC0zMMKwQykgYW5kIHdhcm0gd2F0ZXIgdGVtcGVyYXR1cmVzIGFyb3VuZCA4MMKwRiAoMjfCsEMpLlxcblxcbi0gKipSYWluZmFsbCBJbnNpZ2h0czoqKlxcbiAgLSBNaW5pbWFsIHJhaW5mYWxsIGFzIEFwcmlsIG1hcmtzIHRoZSBlbmQgb2YgdGhlIGRyeSBzZWFzb24sIHByb3ZpZGluZyBtb3N0bHkgc3VubnkgZGF5cy5cXG5cXG4jIyBDb25jbHVzaW9uXFxuXFxuQXByaWwgaXMgYW4gaWRlYWwgbW9udGggZm9yIGFkdmVudHVyZSB2YWNhdGlvbnMgaW4gdGhlIENhcmliYmVhbiwgb2ZmZXJpbmcgYSBzZWFtbGVzcyBjb21iaW5hdGlvbiBvZiBzdXJmaW5nLCBoaWtpbmcsIGFuZCB3YXRlciBzcG9ydHMgYWxvbmcgd2l0aCBlbnJpY2hpbmcgY3VsdHVyYWwgZXhwZXJpZW5jZXMuIFdpdGggZGl2ZXJzZSBkZXN0aW5hdGlvbnMgY2F0ZXJpbmcgdG8gYWxsIHNraWxsIGxldmVscyBhbmQgaW50ZXJlc3RzLCB0cmF2ZWxlcnMgYXJlIHN1cmUgdG8gZmluZCBhIGZ1bGZpbGxpbmcgYW5kIG1lbW9yYWJsZSB0cmF2ZWwgZXhwZXJpZW5jZS4gQXMgc3VjaCwgdGhpcyBtb250aCBpcyBwZXJmZWN0IGZvciB0aG9zZSBzZWVraW5nIGFuIGFjdGl2ZSBhbmQgdmlicmFudCBnZXRhd2F5LlxcblxcbiMjIFJlZmVyZW5jZXNcXG5cXG4tIEVsaXRlIElzbGFuZCBIb2xpZGF5cyBbQ2FyaWJiZWFuIFdhdGVyIFNwb3J0cyBHdWlkZV0oaHR0cHM6Ly9lbGl0ZWlzbGFuZGhvbGlkYXlzLmNvbS9jYXJpYmJlYW4td2F0ZXJzcG9ydHMtZ3VpZGUvP3V0bV9zb3VyY2U9b3BlbmFpKVxcbi0gVHJpcEFkdmlzb3IgW09jZWFuIEFkdmVudHVyZXMgU2FpbGluZyBBZHZlbnR1cmUgUmV2aWV3c10oaHR0cHM6Ly93d3cudHJpcGFkdmlzb3IuY29tL1Nob3dVc2VyUmV2aWV3cy1nMTQ3MjkzLWQyNjI0Nzg5LXIyMDYxMTQxMDUtT2NlYW5fQWR2ZW50dXJlc19TYWlsaW5nX0FkdmVudHVyZS1QdW50YV9DYW5hX0xhX0FsdGFncmFjaWFfUHJvdmluY2VfRG9taW5pY2FuX1IuaHRtbD91dG1fc291cmNlPW9wZW5haSlcXG4tIFNsaWNrcm9jayBCZWxpemUgQWR2ZW50dXJlcyBbUGFja2FnZXNdKGh0dHBzOi8vc2xpY2tyb2NrLmNvbS9wYWNrYWdlcy8/dXRtX3NvdXJjZT1vcGVuYWkpXFxuLSBPdGhlciByZWxldmFudCB0cmF2ZWwgYXJ0aWNsZXMgYW5kIGxvY2FsIGd1aWRlcy5cXG5cXG4tLS1cXG5cXG5Gb3IgbW9yZSBwZXJzb25hbGl6ZWQgdHJhdmVsIGd1aWRhbmNlLCBjb25zaWRlciByZWFjaGluZyBvdXQgdG8gbG9jYWwgdG91cmlzbSBib2FyZHMgb3IgdHJhdmVsIGFnZW5jaWVzIHNwZWNpYWxpemluZyBpbiBDYXJpYmJlYW4gYWR2ZW50dXJlcy5cIixcImZvbGxvd191cF9xdWVzdGlvbnNcIjpbXCJXaGF0IGFyZSB0aGUgc2FmZXR5IHRpcHMgZm9yIHN1cmZpbmcgaW4gdGhlIENhcmliYmVhbj9cIixcIkhvdyBjYW4gdHJhdmVsZXJzIGFycmFuZ2UgZXF1aXBtZW50IHJlbnRhbHMgaW4gYWR2YW5jZT9cIixcIldoYXQgYXJlIHRoZSBiZXN0IGN1bHR1cmFsIGV4cGVyaWVuY2VzIGluIHRoZSBDYXJpYmJlYW4gaW4gQXByaWw/XCIsXCJIb3cgZG8gd2VhdGhlciBjb25kaXRpb25zIHZhcnkgYW1vbmcgQ2FyaWJiZWFuIGlzbGFuZHMgaW4gQXByaWw/XCIsXCJXaGF0IGFyZSB0aGUgZmFtaWx5LWZyaWVuZGx5IGFkdmVudHVyZSBvcHRpb25zIGluIHRoZSBDYXJpYmJlYW4/XCJdfSIsInR5cGUiOiJvdXRwdXRfdGV4dCIsImxvZ3Byb2JzIjpbXX1dLCJyb2xlIjoiYXNzaXN0YW50Iiwic3RhdHVzIjoiY29tcGxldGVkIiwidHlwZSI6Im1lc3NhZ2UifV0sInVzYWdlIjp7InJlcXVlc3RzIjoxLCJpbnB1dF90b2tlbnMiOjM3ODEsImlucHV0X3Rva2Vuc19kZXRhaWxzIjp7ImNhY2hlZF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5zIjoxMTExLCJvdXRwdXRfdG9rZW5zX2RldGFpbHMiOnsicmVhc29uaW5nX3Rva2VucyI6MH0sInRvdGFsX3Rva2VucyI6NDg5Mn0sInJlc3BvbnNlX2lkIjoicmVzcF82ODYyYzM5ODE1NGM4MTlkYmVmOWU5ZjQ2NGE3YjcyNDBkZTU5ZGY5NjQ4ZmYwZTMifQ==" - } - ] - }, - "scheduledEventId": "71", - "startedEventId": "72", - "identity": "19167@Tims-MacBook-Pro.local" - } - }, - { - "eventId": "74", - "eventTime": "2025-06-30T17:04:50.687501Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", - "taskId": "1057229", - "workflowTaskScheduledEventAttributes": { - "taskQueue": { - "name": "19167@Tims-MacBook-Pro.local-f783ce7516bc48e59108b7b5cd31d0e5", - "kind": "TASK_QUEUE_KIND_STICKY", - "normalName": "fc97ba3c-6c32-4f4c-a986-8d7f4f5c7f80" - }, - "startToCloseTimeout": "10s", - "attempt": 1 - } - }, - { - "eventId": "75", - "eventTime": "2025-06-30T17:04:50.690917Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", - "taskId": "1057233", - "workflowTaskStartedEventAttributes": { - "scheduledEventId": "74", - "identity": "19167@Tims-MacBook-Pro.local", - "requestId": "88402abb-bcf0-4c2d-8fd3-823f221c29bd", - "historySizeBytes": "82270", - "workerVersion": { - "buildId": "9f32b5d9789ece25bfb8515a7023ec78" - } - } - }, - { - "eventId": "76", - "eventTime": "2025-06-30T17:04:50.710583Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", - "taskId": "1057237", - "workflowTaskCompletedEventAttributes": { - "scheduledEventId": "74", - "startedEventId": "75", - "identity": "19167@Tims-MacBook-Pro.local", - "workerVersion": { - "buildId": "9f32b5d9789ece25bfb8515a7023ec78" - }, - "sdkMetadata": {}, - "meteringMetadata": {} - } - }, - { - "eventId": "77", - "eventTime": "2025-06-30T17:04:50.710627Z", - "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_COMPLETED", - "taskId": "1057238", - "workflowExecutionCompletedEventAttributes": { - "result": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "IiMgQ2FyaWJiZWFuIFZhY2F0aW9uIFNwb3RzIGluIEFwcmlsOiBBIEd1aWRlIHRvIFN1cmZpbmcsIEhpa2luZywgYW5kIFdhdGVyIFNwb3J0cyBcblxuIyMgSW50cm9kdWN0aW9uXG5cbkFwcmlsIGlzIGEgcHJpbWUgbW9udGggZm9yIGFkdmVudHVyZSBzZWVrZXJzIGluIHRoZSBDYXJpYmJlYW4sIHByb3ZpZGluZyBhbiBvcHRpbWFsIGJsZW5kIG9mIGNvbnNpc3RlbnQgc3VyZmluZyB3YXZlcywgdmlicmFudCBoaWtpbmcgdHJhaWxzLCBhbmQgZGl2ZXJzZSB3YXRlciBzcG9ydHMuIFRoZSByZWdpb24ncyBmYXZvcmFibGUgY2xpbWF0ZSwgY2hhcmFjdGVyaXplZCBieSB3YXJtIHRlbXBlcmF0dXJlcyBhbmQgZHJ5IGNvbmRpdGlvbnMsIGVuaGFuY2VzIHRoZXNlIG91dGRvb3IgZXhwZXJpZW5jZXMuIFRoaXMgcmVwb3J0IGV4cGxvcmVzIHRoZSB0b3AgQ2FyaWJiZWFuIGRlc3RpbmF0aW9ucyBpbiBBcHJpbCBmb3Igc3VyZmluZywgaGlraW5nLCBhbmQgZW5nYWdpbmcgaW4gd2F0ZXIgc3BvcnRzLCBlbnN1cmluZyB0cmF2ZWxlcnMgY2FuIG1heGltaXplIHRoZWlyIHZhY2F0aW9uLlxuXG4jIyBTdXJmaW5nIGluIHRoZSBDYXJpYmJlYW5cblxuIyMjIFRvcCBTdXJmaW5nIERlc3RpbmF0aW9uc1xuXG4qKjEuIFJpbmNvbiwgUHVlcnRvIFJpY28qKlxuICAgLSBLbm93biBmb3IgYSB2YXJpZXR5IG9mIGJyZWFrcyBzdWl0YWJsZSBmb3IgYWxsIGxldmVscy5cbiAgIC0gT2ZmZXJzIGEgd2FybSBzdXJmIGNvbW11bml0eSBhbmQgY29uc2lzdGVudCBzd2VsbHMuXG5cbioqMi4gQmF0aHNoZWJhLCBCYXJiYWRvcyoqXG4gICAtIEZhbW91cyBmb3IgdGhlIFNvdXAgQm93bCwgb2ZmZXJpbmcgcG93ZXJmdWwgd2F2ZXMuXG4gICAtIEF0dHJhY3RzIGJvdGggbG9jYWwgYW5kIGludGVybmF0aW9uYWwgc3VyZmVycy5cblxuKiozLiBQbGF5YSBFbmN1ZW50cm8sIERvbWluaWNhbiBSZXB1YmxpYyoqXG4gICAtIEZlYXR1cmVzIGNvbnNpc3RlbnQgd2F2ZXMgYW5kIGEgdmlicmFudCBzdXJmIHNjZW5lLlxuXG4qKjQuIEFndWFkaWxsYSwgUHVlcnRvIFJpY28sIGFuZCBMb25nIEJheSwgSmFtYWljYSoqXG4gICAtIFByb3ZpZGUgZGl2ZXJzZSBzdXJmIGJyZWFrcyBhbmQgYmVnaW5uZXItZnJpZW5kbHkgd2F2ZXMuXG5cbiMjIyBTdXJmaW5nIEV2ZW50c1xuICAgLSAqKkJhcmJhZG9zIFN1cmYgUHJvKiogYW5kICoqTWFydGluaXF1ZSBTdXJmIFBybyoqIGFyZSBXU0wgZXZlbnRzIG9jY3VycmluZyBpbiBBcHJpbCwgZHJhd2luZyBpbnRlcm5hdGlvbmFsIGNvbXBldGl0b3JzLlxuXG4jIyBIaWtpbmcgQWR2ZW50dXJlc1xuXG4jIyMgTm90YWJsZSBIaWtpbmcgVHJhaWxzXG5cbioqMS4gRWwgWXVucXVlIE5hdGlvbmFsIEZvcmVzdCwgUHVlcnRvIFJpY28qKlxuICAgLSBPZmZlcnMgYSBuZXR3b3JrIG9mIHRyYWlscyBzaG93Y2FzaW5nIHVuaXF1ZSBmbG9yYSBhbmQgZmF1bmEuXG5cbioqMi4gQmx1ZSBNb3VudGFpbiBQZWFrLCBKYW1haWNhKipcbiAgIC0gUHJvdmlkZXMgcGFub3JhbWljIHN1bnJpc2Ugdmlld3MgYW5kIGN1bHR1cmFsIGVuY291bnRlcnMuXG5cbioqMy4gV2FpdHVrdWJ1bGkgTmF0aW9uYWwgVHJhaWwsIERvbWluaWNhKipcbiAgIC0gU3BhbnMgMTE1IG1pbGVzLCB0cmF2ZXJzaW5nIHZvbGNhbmljIGFuZCBjdWx0dXJhbCBsYW5kc2NhcGVzLlxuXG4jIyMgSGlraW5nIFRpcHNcbiAgIC0gQXByaWzigJlzIHdlYXRoZXIgcHJvdmlkZXMgY29tZm9ydGFibGUgY29uZGl0aW9ucyBmb3IgZXhwbG9yaW5nIG1vdW50YWlub3VzIGFuZCByYWluZm9yZXN0IHRyYWlscy4gXG4gICAtIEVuZ2FnZSB3aXRoIGxvY2FsIGd1aWRlcyBmb3Igc2FmZSBhbmQgaW5mb3JtYXRpdmUgaGlrZXMuXG5cbiMjIFdhdGVyIFNwb3J0cyBPcHBvcnR1bml0aWVzXG5cbiMjIyBQb3B1bGFyIEFjdGl2aXRpZXNcblxuKioxLiBLaXRlc3VyZmluZyBhbmQgU25vcmtlbGluZyoqXG4gICAtIFN0ZWFkeSB3aW5kcyBpbiBBcHJpbCBwZXJmZWN0IGZvciBraXRlc3VyZmluZywgZXNwZWNpYWxseSBhcm91bmQgQ2FiYXJldGUgaW4gdGhlIERvbWluaWNhbiBSZXB1YmxpYy5cbiAgIC0gU25vcmtlbGluZyBvcHBvcnR1bml0aWVzIGFib3VuZCwgd2l0aCBjbGVhciB3YXRlcnMgaG9zdGluZyBkaXZlcnNlIG1hcmluZSBsaWZlLlxuXG4qKjIuIFNhaWxpbmcgYW5kIEtheWFraW5nKipcbiAgIC0gVGhlIEJyaXRpc2ggVmlyZ2luIElzbGFuZHMgb2ZmZXIgZXhjZXB0aW9uYWwgc2FpbGluZyBjb25kaXRpb25zLlxuICAgLSBCZWxpemUncyBHcmVhdCBCbHVlIEhvbGUgaXMgYW4gaWNvbmljIGtheWFraW5nIGRlc3RpbmF0aW9uLlxuXG4jIyMgRXF1aXBtZW50IFJlbnRhbHNcbiAgIC0gTnVtZXJvdXMgcmVudGFsIHNlcnZpY2VzLCBzdWNoIGFzIFJpbmNvbiBDYXBpdGFsIFdhdGVyIFNwb3J0cywgcHJvdmlkZSBhY2Nlc3MgdG8gamV0IHNraXMsIGtheWFrcywgYW5kIHNub3JrZWxpbmcgZ2Vhci5cblxuIyMgQ3VsdHVyYWwgRXZlbnRzIGFuZCBMb2NhbCBJbnNpZ2h0c1xuXG4jIyMgRmVzdGl2YWxzIGFuZCBDZWxlYnJhdGlvbnNcblxuKioxLiBTdC4gVGhvbWFzIEludGVybmF0aW9uYWwgUmVnYXR0YSwgVS5TLiBWaXJnaW4gSXNsYW5kcyoqXG4gICAtIEEga2V5IHNhaWxpbmcgZXZlbnQgcHJvbW90aW5nIGN1bHR1cmFsIGV4Y2hhbmdlLlxuXG4qKjIuIE5hdGlvbmFsIEZhbWlseSBJc2xhbmQgUmVnYXR0YSwgQmFoYW1hcyoqXG4gICAtIENlbGVicmF0ZXMgdHJhZGl0aW9uYWwgQmFoYW1pYW4gc2xvb3Agc2FpbGluZy5cblxuIyMjIExvY2FsIFJlY29tbWVuZGF0aW9uc1xuICAgLSBQYXJ0aWNpcGF0ZSBpbiBndWlkZWQgdG91cnMgYW5kIGxvY2FsIGV2ZW50cyB0byBlbmhhbmNlIGN1bHR1cmFsIHVuZGVyc3RhbmRpbmcgYW5kIGV4cGVyaWVuY2UuXG5cbiMjIFdlYXRoZXIgQ29uc2lkZXJhdGlvbnNcblxuLSAqKlRlbXBlcmF0dXJlIE92ZXJ2aWV3OioqXG4gIC0gQXZlcmFnZSBkYXkgdGVtcGVyYXR1cmVzIGluIHRoZSBtaWQtODBzwrBGICgyOC0zMMKwQykgYW5kIHdhcm0gd2F0ZXIgdGVtcGVyYXR1cmVzIGFyb3VuZCA4MMKwRiAoMjfCsEMpLlxuXG4tICoqUmFpbmZhbGwgSW5zaWdodHM6KipcbiAgLSBNaW5pbWFsIHJhaW5mYWxsIGFzIEFwcmlsIG1hcmtzIHRoZSBlbmQgb2YgdGhlIGRyeSBzZWFzb24sIHByb3ZpZGluZyBtb3N0bHkgc3VubnkgZGF5cy5cblxuIyMgQ29uY2x1c2lvblxuXG5BcHJpbCBpcyBhbiBpZGVhbCBtb250aCBmb3IgYWR2ZW50dXJlIHZhY2F0aW9ucyBpbiB0aGUgQ2FyaWJiZWFuLCBvZmZlcmluZyBhIHNlYW1sZXNzIGNvbWJpbmF0aW9uIG9mIHN1cmZpbmcsIGhpa2luZywgYW5kIHdhdGVyIHNwb3J0cyBhbG9uZyB3aXRoIGVucmljaGluZyBjdWx0dXJhbCBleHBlcmllbmNlcy4gV2l0aCBkaXZlcnNlIGRlc3RpbmF0aW9ucyBjYXRlcmluZyB0byBhbGwgc2tpbGwgbGV2ZWxzIGFuZCBpbnRlcmVzdHMsIHRyYXZlbGVycyBhcmUgc3VyZSB0byBmaW5kIGEgZnVsZmlsbGluZyBhbmQgbWVtb3JhYmxlIHRyYXZlbCBleHBlcmllbmNlLiBBcyBzdWNoLCB0aGlzIG1vbnRoIGlzIHBlcmZlY3QgZm9yIHRob3NlIHNlZWtpbmcgYW4gYWN0aXZlIGFuZCB2aWJyYW50IGdldGF3YXkuXG5cbiMjIFJlZmVyZW5jZXNcblxuLSBFbGl0ZSBJc2xhbmQgSG9saWRheXMgW0NhcmliYmVhbiBXYXRlciBTcG9ydHMgR3VpZGVdKGh0dHBzOi8vZWxpdGVpc2xhbmRob2xpZGF5cy5jb20vY2FyaWJiZWFuLXdhdGVyc3BvcnRzLWd1aWRlLz91dG1fc291cmNlPW9wZW5haSlcbi0gVHJpcEFkdmlzb3IgW09jZWFuIEFkdmVudHVyZXMgU2FpbGluZyBBZHZlbnR1cmUgUmV2aWV3c10oaHR0cHM6Ly93d3cudHJpcGFkdmlzb3IuY29tL1Nob3dVc2VyUmV2aWV3cy1nMTQ3MjkzLWQyNjI0Nzg5LXIyMDYxMTQxMDUtT2NlYW5fQWR2ZW50dXJlc19TYWlsaW5nX0FkdmVudHVyZS1QdW50YV9DYW5hX0xhX0FsdGFncmFjaWFfUHJvdmluY2VfRG9taW5pY2FuX1IuaHRtbD91dG1fc291cmNlPW9wZW5haSlcbi0gU2xpY2tyb2NrIEJlbGl6ZSBBZHZlbnR1cmVzIFtQYWNrYWdlc10oaHR0cHM6Ly9zbGlja3JvY2suY29tL3BhY2thZ2VzLz91dG1fc291cmNlPW9wZW5haSlcbi0gT3RoZXIgcmVsZXZhbnQgdHJhdmVsIGFydGljbGVzIGFuZCBsb2NhbCBndWlkZXMuXG5cbi0tLVxuXG5Gb3IgbW9yZSBwZXJzb25hbGl6ZWQgdHJhdmVsIGd1aWRhbmNlLCBjb25zaWRlciByZWFjaGluZyBvdXQgdG8gbG9jYWwgdG91cmlzbSBib2FyZHMgb3IgdHJhdmVsIGFnZW5jaWVzIHNwZWNpYWxpemluZyBpbiBDYXJpYmJlYW4gYWR2ZW50dXJlcy4i" - } - ] - }, - "workflowTaskCompletedEventId": "76" - } - } - ] -} \ No newline at end of file diff --git a/tests/contrib/openai_agents/histories/tools-workflow-history.json b/tests/contrib/openai_agents/histories/tools-workflow-history.json deleted file mode 100644 index 66304d61e..000000000 --- a/tests/contrib/openai_agents/histories/tools-workflow-history.json +++ /dev/null @@ -1,506 +0,0 @@ -{ - "events": [ - { - "eventId": "1", - "eventTime": "2025-06-30T16:01:58.250193Z", - "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_STARTED", - "taskId": "1056366", - "workflowExecutionStartedEventAttributes": { - "workflowType": { - "name": "ToolsWorkflow" - }, - "taskQueue": { - "name": "98bba13c-e75a-446d-a2f8-077680fb23af", - "kind": "TASK_QUEUE_KIND_NORMAL" - }, - "input": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "IldoYXQgaXMgdGhlIHdlYXRoZXIgaW4gVG9raW8/Ig==" - } - ] - }, - "workflowExecutionTimeout": "5s", - "workflowRunTimeout": "5s", - "workflowTaskTimeout": "5s", - "originalExecutionRunId": "0197c192-e1ea-72ee-9435-c3625ba774b3", - "identity": "16733@Tims-MacBook-Pro.local", - "firstExecutionRunId": "0197c192-e1ea-72ee-9435-c3625ba774b3", - "attempt": 1, - "workflowExecutionExpirationTime": "2025-06-30T16:02:03.250Z", - "firstWorkflowTaskBackoff": "0s", - "workflowId": "tools-workflow-03ac713e-18d8-4075-be78-48ffaecbe8ee" - } - }, - { - "eventId": "2", - "eventTime": "2025-06-30T16:01:58.250225Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", - "taskId": "1056367", - "workflowTaskScheduledEventAttributes": { - "taskQueue": { - "name": "98bba13c-e75a-446d-a2f8-077680fb23af", - "kind": "TASK_QUEUE_KIND_NORMAL" - }, - "startToCloseTimeout": "5s", - "attempt": 1 - } - }, - { - "eventId": "3", - "eventTime": "2025-06-30T16:01:58.251382Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", - "taskId": "1056373", - "workflowTaskStartedEventAttributes": { - "scheduledEventId": "2", - "identity": "16733@Tims-MacBook-Pro.local", - "requestId": "5ba49c87-5a13-4c88-978b-a7fe5d7e6aa2", - "historySizeBytes": "413", - "workerVersion": { - "buildId": "01c9746b80ed5a14fc25fb053c5e410a" - } - } - }, - { - "eventId": "4", - "eventTime": "2025-06-30T16:01:58.297122Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", - "taskId": "1056377", - "workflowTaskCompletedEventAttributes": { - "scheduledEventId": "2", - "startedEventId": "3", - "identity": "16733@Tims-MacBook-Pro.local", - "workerVersion": { - "buildId": "01c9746b80ed5a14fc25fb053c5e410a" - }, - "sdkMetadata": { - "coreUsedFlags": [ - 3, - 1, - 2 - ], - "sdkName": "temporal-python", - "sdkVersion": "1.13.0" - }, - "meteringMetadata": {} - } - }, - { - "eventId": "5", - "eventTime": "2025-06-30T16:01:58.297147Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_SCHEDULED", - "taskId": "1056378", - "activityTaskScheduledEventAttributes": { - "activityId": "1", - "activityType": { - "name": "invoke_model_activity" - }, - "taskQueue": { - "name": "98bba13c-e75a-446d-a2f8-077680fb23af", - "kind": "TASK_QUEUE_KIND_NORMAL" - }, - "header": { - "fields": { - "__openai_span": { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJ0cmFjZU5hbWUiOiJBZ2VudCB3b3JrZmxvdyIsInNwYW5JZCI6InNwYW5fOWNmMTNjMDk5ZGQ0NGQ1N2JhOGYzMjg4IiwidHJhY2VJZCI6InRyYWNlXzM0ODdhZjk2ZGZmNzQ5MzJiNDNhNmRiMzkzMjMyYzc0In0=" - } - } - }, - "input": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJtb2RlbF9uYW1lIjpudWxsLCJzeXN0ZW1faW5zdHJ1Y3Rpb25zIjoiWW91IGFyZSBhIGhlbHBmdWwgYWdlbnQuIiwiaW5wdXQiOlt7ImNvbnRlbnQiOiJXaGF0IGlzIHRoZSB3ZWF0aGVyIGluIFRva2lvPyIsInJvbGUiOiJ1c2VyIn1dLCJtb2RlbF9zZXR0aW5ncyI6eyJ0ZW1wZXJhdHVyZSI6bnVsbCwidG9wX3AiOm51bGwsImZyZXF1ZW5jeV9wZW5hbHR5IjpudWxsLCJwcmVzZW5jZV9wZW5hbHR5IjpudWxsLCJ0b29sX2Nob2ljZSI6bnVsbCwicGFyYWxsZWxfdG9vbF9jYWxscyI6bnVsbCwidHJ1bmNhdGlvbiI6bnVsbCwibWF4X3Rva2VucyI6bnVsbCwicmVhc29uaW5nIjpudWxsLCJtZXRhZGF0YSI6bnVsbCwic3RvcmUiOm51bGwsImluY2x1ZGVfdXNhZ2UiOm51bGwsImV4dHJhX3F1ZXJ5IjpudWxsLCJleHRyYV9ib2R5IjpudWxsLCJleHRyYV9oZWFkZXJzIjpudWxsLCJleHRyYV9hcmdzIjpudWxsfSwidG9vbHMiOlt7Im5hbWUiOiJnZXRfd2VhdGhlciIsImRlc2NyaXB0aW9uIjoiR2V0IHRoZSB3ZWF0aGVyIGZvciBhIGdpdmVuIGNpdHkuIiwicGFyYW1zX2pzb25fc2NoZW1hIjp7InByb3BlcnRpZXMiOnsiY2l0eSI6eyJ0aXRsZSI6IkNpdHkiLCJ0eXBlIjoic3RyaW5nIn19LCJyZXF1aXJlZCI6WyJjaXR5Il0sInRpdGxlIjoiZ2V0X3dlYXRoZXJfYXJncyIsInR5cGUiOiJvYmplY3QiLCJhZGRpdGlvbmFsUHJvcGVydGllcyI6ZmFsc2V9LCJzdHJpY3RfanNvbl9zY2hlbWEiOnRydWV9XSwib3V0cHV0X3NjaGVtYSI6bnVsbCwiaGFuZG9mZnMiOltdLCJ0cmFjaW5nIjoxLCJwcmV2aW91c19yZXNwb25zZV9pZCI6bnVsbCwicHJvbXB0IjpudWxsfQ==" - } - ] - }, - "scheduleToCloseTimeout": "5s", - "scheduleToStartTimeout": "5s", - "startToCloseTimeout": "5s", - "heartbeatTimeout": "0s", - "workflowTaskCompletedEventId": "4", - "retryPolicy": { - "initialInterval": "1s", - "backoffCoefficient": 2.0, - "maximumInterval": "100s" - }, - "useWorkflowBuildId": true - }, - "userMetadata": { - "summary": { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "IldoYXQgaXMgdGhlIHdlYXRoZXIgaW4gVG9raW8/Ig==" - } - } - }, - { - "eventId": "6", - "eventTime": "2025-06-30T16:01:58.298046Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_STARTED", - "taskId": "1056383", - "activityTaskStartedEventAttributes": { - "scheduledEventId": "5", - "identity": "16733@Tims-MacBook-Pro.local", - "requestId": "ae5199c4-4759-4cf5-8a14-09bcf3232edb", - "attempt": 1, - "workerVersion": { - "buildId": "01c9746b80ed5a14fc25fb053c5e410a" - } - } - }, - { - "eventId": "7", - "eventTime": "2025-06-30T16:01:58.317880Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_COMPLETED", - "taskId": "1056384", - "activityTaskCompletedEventAttributes": { - "result": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJvdXRwdXQiOlt7ImFyZ3VtZW50cyI6IntcImNpdHlcIjpcIlRva3lvXCJ9IiwiY2FsbF9pZCI6ImNhbGwiLCJuYW1lIjoiZ2V0X3dlYXRoZXIiLCJ0eXBlIjoiZnVuY3Rpb25fY2FsbCIsImlkIjoiaWQiLCJzdGF0dXMiOiJjb21wbGV0ZWQifV0sInVzYWdlIjp7InJlcXVlc3RzIjowLCJpbnB1dF90b2tlbnMiOjAsImlucHV0X3Rva2Vuc19kZXRhaWxzIjp7ImNhY2hlZF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5zIjowLCJvdXRwdXRfdG9rZW5zX2RldGFpbHMiOnsicmVhc29uaW5nX3Rva2VucyI6MH0sInRvdGFsX3Rva2VucyI6MH0sInJlc3BvbnNlX2lkIjpudWxsfQ==" - } - ] - }, - "scheduledEventId": "5", - "startedEventId": "6", - "identity": "16733@Tims-MacBook-Pro.local" - } - }, - { - "eventId": "8", - "eventTime": "2025-06-30T16:01:58.317883Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", - "taskId": "1056385", - "workflowTaskScheduledEventAttributes": { - "taskQueue": { - "name": "16733@Tims-MacBook-Pro.local-d864298eeab644f88e86e84252c851f5", - "kind": "TASK_QUEUE_KIND_STICKY", - "normalName": "98bba13c-e75a-446d-a2f8-077680fb23af" - }, - "startToCloseTimeout": "5s", - "attempt": 1 - } - }, - { - "eventId": "9", - "eventTime": "2025-06-30T16:01:58.318575Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", - "taskId": "1056389", - "workflowTaskStartedEventAttributes": { - "scheduledEventId": "8", - "identity": "16733@Tims-MacBook-Pro.local", - "requestId": "bec333bb-94d5-4daa-9156-fca8ef1fd141", - "historySizeBytes": "2605", - "workerVersion": { - "buildId": "01c9746b80ed5a14fc25fb053c5e410a" - } - } - }, - { - "eventId": "10", - "eventTime": "2025-06-30T16:01:58.352463Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", - "taskId": "1056393", - "workflowTaskCompletedEventAttributes": { - "scheduledEventId": "8", - "startedEventId": "9", - "identity": "16733@Tims-MacBook-Pro.local", - "workerVersion": { - "buildId": "01c9746b80ed5a14fc25fb053c5e410a" - }, - "sdkMetadata": {}, - "meteringMetadata": {} - } - }, - { - "eventId": "11", - "eventTime": "2025-06-30T16:01:58.352485Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_SCHEDULED", - "taskId": "1056394", - "activityTaskScheduledEventAttributes": { - "activityId": "2", - "activityType": { - "name": "get_weather" - }, - "taskQueue": { - "name": "98bba13c-e75a-446d-a2f8-077680fb23af", - "kind": "TASK_QUEUE_KIND_NORMAL" - }, - "header": { - "fields": { - "__openai_span": { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJ0cmFjZU5hbWUiOiJBZ2VudCB3b3JrZmxvdyIsInNwYW5JZCI6InNwYW5fZDhlMzdkYjVjNDZjNGZmZGEwOTk1MTg5IiwidHJhY2VJZCI6InRyYWNlXzM0ODdhZjk2ZGZmNzQ5MzJiNDNhNmRiMzkzMjMyYzc0In0=" - } - } - }, - "input": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "IntcImNpdHlcIjpcIlRva3lvXCJ9Ig==" - } - ] - }, - "scheduleToCloseTimeout": "5s", - "scheduleToStartTimeout": "5s", - "startToCloseTimeout": "5s", - "heartbeatTimeout": "0s", - "workflowTaskCompletedEventId": "10", - "retryPolicy": { - "initialInterval": "1s", - "backoffCoefficient": 2.0, - "maximumInterval": "100s" - }, - "useWorkflowBuildId": true - } - }, - { - "eventId": "12", - "eventTime": "2025-06-30T16:01:58.353293Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_STARTED", - "taskId": "1056398", - "activityTaskStartedEventAttributes": { - "scheduledEventId": "11", - "identity": "16733@Tims-MacBook-Pro.local", - "requestId": "612e8fe0-d67f-48fa-8b3b-692b86898abe", - "attempt": 1, - "workerVersion": { - "buildId": "01c9746b80ed5a14fc25fb053c5e410a" - } - } - }, - { - "eventId": "13", - "eventTime": "2025-06-30T16:01:58.357887Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_COMPLETED", - "taskId": "1056399", - "activityTaskCompletedEventAttributes": { - "result": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJjaXR5Ijoie1wiY2l0eVwiOlwiVG9reW9cIn0iLCJ0ZW1wZXJhdHVyZV9yYW5nZSI6IjE0LTIwQyIsImNvbmRpdGlvbnMiOiJTdW5ueSB3aXRoIHdpbmQuIn0=" - } - ] - }, - "scheduledEventId": "11", - "startedEventId": "12", - "identity": "16733@Tims-MacBook-Pro.local" - } - }, - { - "eventId": "14", - "eventTime": "2025-06-30T16:01:58.357890Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", - "taskId": "1056400", - "workflowTaskScheduledEventAttributes": { - "taskQueue": { - "name": "16733@Tims-MacBook-Pro.local-d864298eeab644f88e86e84252c851f5", - "kind": "TASK_QUEUE_KIND_STICKY", - "normalName": "98bba13c-e75a-446d-a2f8-077680fb23af" - }, - "startToCloseTimeout": "5s", - "attempt": 1 - } - }, - { - "eventId": "15", - "eventTime": "2025-06-30T16:01:58.358535Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", - "taskId": "1056404", - "workflowTaskStartedEventAttributes": { - "scheduledEventId": "14", - "identity": "16733@Tims-MacBook-Pro.local", - "requestId": "5860a55b-57b1-4439-88a1-fc6612cd0a1f", - "historySizeBytes": "3649", - "workerVersion": { - "buildId": "01c9746b80ed5a14fc25fb053c5e410a" - } - } - }, - { - "eventId": "16", - "eventTime": "2025-06-30T16:01:58.363611Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", - "taskId": "1056408", - "workflowTaskCompletedEventAttributes": { - "scheduledEventId": "14", - "startedEventId": "15", - "identity": "16733@Tims-MacBook-Pro.local", - "workerVersion": { - "buildId": "01c9746b80ed5a14fc25fb053c5e410a" - }, - "sdkMetadata": {}, - "meteringMetadata": {} - } - }, - { - "eventId": "17", - "eventTime": "2025-06-30T16:01:58.363630Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_SCHEDULED", - "taskId": "1056409", - "activityTaskScheduledEventAttributes": { - "activityId": "3", - "activityType": { - "name": "invoke_model_activity" - }, - "taskQueue": { - "name": "98bba13c-e75a-446d-a2f8-077680fb23af", - "kind": "TASK_QUEUE_KIND_NORMAL" - }, - "header": { - "fields": { - "__openai_span": { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJ0cmFjZU5hbWUiOiJBZ2VudCB3b3JrZmxvdyIsInNwYW5JZCI6InNwYW5fNjNhN2VkYWE0MzU3NDU5OWIzOTFkNzAzIiwidHJhY2VJZCI6InRyYWNlXzM0ODdhZjk2ZGZmNzQ5MzJiNDNhNmRiMzkzMjMyYzc0In0=" - } - } - }, - "input": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJtb2RlbF9uYW1lIjpudWxsLCJzeXN0ZW1faW5zdHJ1Y3Rpb25zIjoiWW91IGFyZSBhIGhlbHBmdWwgYWdlbnQuIiwiaW5wdXQiOlt7ImNvbnRlbnQiOiJXaGF0IGlzIHRoZSB3ZWF0aGVyIGluIFRva2lvPyIsInJvbGUiOiJ1c2VyIn0seyJhcmd1bWVudHMiOiJ7XCJjaXR5XCI6XCJUb2t5b1wifSIsImNhbGxfaWQiOiJjYWxsIiwibmFtZSI6ImdldF93ZWF0aGVyIiwidHlwZSI6ImZ1bmN0aW9uX2NhbGwiLCJpZCI6ImlkIiwic3RhdHVzIjoiY29tcGxldGVkIn0seyJjYWxsX2lkIjoiY2FsbCIsIm91dHB1dCI6IldlYXRoZXIoY2l0eT0ne1wiY2l0eVwiOlwiVG9reW9cIn0nLCB0ZW1wZXJhdHVyZV9yYW5nZT0nMTQtMjBDJywgY29uZGl0aW9ucz0nU3Vubnkgd2l0aCB3aW5kLicpIiwidHlwZSI6ImZ1bmN0aW9uX2NhbGxfb3V0cHV0In1dLCJtb2RlbF9zZXR0aW5ncyI6eyJ0ZW1wZXJhdHVyZSI6bnVsbCwidG9wX3AiOm51bGwsImZyZXF1ZW5jeV9wZW5hbHR5IjpudWxsLCJwcmVzZW5jZV9wZW5hbHR5IjpudWxsLCJ0b29sX2Nob2ljZSI6bnVsbCwicGFyYWxsZWxfdG9vbF9jYWxscyI6bnVsbCwidHJ1bmNhdGlvbiI6bnVsbCwibWF4X3Rva2VucyI6bnVsbCwicmVhc29uaW5nIjpudWxsLCJtZXRhZGF0YSI6bnVsbCwic3RvcmUiOm51bGwsImluY2x1ZGVfdXNhZ2UiOm51bGwsImV4dHJhX3F1ZXJ5IjpudWxsLCJleHRyYV9ib2R5IjpudWxsLCJleHRyYV9oZWFkZXJzIjpudWxsLCJleHRyYV9hcmdzIjpudWxsfSwidG9vbHMiOlt7Im5hbWUiOiJnZXRfd2VhdGhlciIsImRlc2NyaXB0aW9uIjoiR2V0IHRoZSB3ZWF0aGVyIGZvciBhIGdpdmVuIGNpdHkuIiwicGFyYW1zX2pzb25fc2NoZW1hIjp7InByb3BlcnRpZXMiOnsiY2l0eSI6eyJ0aXRsZSI6IkNpdHkiLCJ0eXBlIjoic3RyaW5nIn19LCJyZXF1aXJlZCI6WyJjaXR5Il0sInRpdGxlIjoiZ2V0X3dlYXRoZXJfYXJncyIsInR5cGUiOiJvYmplY3QiLCJhZGRpdGlvbmFsUHJvcGVydGllcyI6ZmFsc2V9LCJzdHJpY3RfanNvbl9zY2hlbWEiOnRydWV9XSwib3V0cHV0X3NjaGVtYSI6bnVsbCwiaGFuZG9mZnMiOltdLCJ0cmFjaW5nIjoxLCJwcmV2aW91c19yZXNwb25zZV9pZCI6bnVsbCwicHJvbXB0IjpudWxsfQ==" - } - ] - }, - "scheduleToCloseTimeout": "5s", - "scheduleToStartTimeout": "5s", - "startToCloseTimeout": "5s", - "heartbeatTimeout": "0s", - "workflowTaskCompletedEventId": "16", - "retryPolicy": { - "initialInterval": "1s", - "backoffCoefficient": 2.0, - "maximumInterval": "100s" - }, - "useWorkflowBuildId": true - } - }, - { - "eventId": "18", - "eventTime": "2025-06-30T16:01:58.364372Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_STARTED", - "taskId": "1056413", - "activityTaskStartedEventAttributes": { - "scheduledEventId": "17", - "identity": "16733@Tims-MacBook-Pro.local", - "requestId": "0b072618-f02f-483e-8a13-260fa095eb59", - "attempt": 1, - "workerVersion": { - "buildId": "01c9746b80ed5a14fc25fb053c5e410a" - } - } - }, - { - "eventId": "19", - "eventTime": "2025-06-30T16:01:58.378926Z", - "eventType": "EVENT_TYPE_ACTIVITY_TASK_COMPLETED", - "taskId": "1056414", - "activityTaskCompletedEventAttributes": { - "result": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "eyJvdXRwdXQiOlt7ImlkIjoiIiwiY29udGVudCI6W3siYW5ub3RhdGlvbnMiOltdLCJ0ZXh0IjoiVGVzdCB3ZWF0aGVyIHJlc3VsdCIsInR5cGUiOiJvdXRwdXRfdGV4dCJ9XSwicm9sZSI6ImFzc2lzdGFudCIsInN0YXR1cyI6ImNvbXBsZXRlZCIsInR5cGUiOiJtZXNzYWdlIn1dLCJ1c2FnZSI6eyJyZXF1ZXN0cyI6MCwiaW5wdXRfdG9rZW5zIjowLCJpbnB1dF90b2tlbnNfZGV0YWlscyI6eyJjYWNoZWRfdG9rZW5zIjowfSwib3V0cHV0X3Rva2VucyI6MCwib3V0cHV0X3Rva2Vuc19kZXRhaWxzIjp7InJlYXNvbmluZ190b2tlbnMiOjB9LCJ0b3RhbF90b2tlbnMiOjB9LCJyZXNwb25zZV9pZCI6bnVsbH0=" - } - ] - }, - "scheduledEventId": "17", - "startedEventId": "18", - "identity": "16733@Tims-MacBook-Pro.local" - } - }, - { - "eventId": "20", - "eventTime": "2025-06-30T16:01:58.378930Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED", - "taskId": "1056415", - "workflowTaskScheduledEventAttributes": { - "taskQueue": { - "name": "16733@Tims-MacBook-Pro.local-d864298eeab644f88e86e84252c851f5", - "kind": "TASK_QUEUE_KIND_STICKY", - "normalName": "98bba13c-e75a-446d-a2f8-077680fb23af" - }, - "startToCloseTimeout": "5s", - "attempt": 1 - } - }, - { - "eventId": "21", - "eventTime": "2025-06-30T16:01:58.379545Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED", - "taskId": "1056419", - "workflowTaskStartedEventAttributes": { - "scheduledEventId": "20", - "identity": "16733@Tims-MacBook-Pro.local", - "requestId": "3180f9ec-370c-47f8-a020-653eaf69367a", - "historySizeBytes": "6053", - "workerVersion": { - "buildId": "01c9746b80ed5a14fc25fb053c5e410a" - } - } - }, - { - "eventId": "22", - "eventTime": "2025-06-30T16:01:58.385250Z", - "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED", - "taskId": "1056423", - "workflowTaskCompletedEventAttributes": { - "scheduledEventId": "20", - "startedEventId": "21", - "identity": "16733@Tims-MacBook-Pro.local", - "workerVersion": { - "buildId": "01c9746b80ed5a14fc25fb053c5e410a" - }, - "sdkMetadata": {}, - "meteringMetadata": {} - } - }, - { - "eventId": "23", - "eventTime": "2025-06-30T16:01:58.385268Z", - "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_COMPLETED", - "taskId": "1056424", - "workflowExecutionCompletedEventAttributes": { - "result": { - "payloads": [ - { - "metadata": { - "encoding": "anNvbi9wbGFpbg==" - }, - "data": "IlRlc3Qgd2VhdGhlciByZXN1bHQi" - } - ] - }, - "workflowTaskCompletedEventId": "22" - } - } - ] -} \ No newline at end of file diff --git a/tests/contrib/openai_agents/research_agents/planner_agent.py b/tests/contrib/openai_agents/research_agents/planner_agent.py deleted file mode 100644 index 8289d8636..000000000 --- a/tests/contrib/openai_agents/research_agents/planner_agent.py +++ /dev/null @@ -1,29 +0,0 @@ -from agents import Agent -from pydantic import BaseModel - -PROMPT = ( - "You are a helpful research assistant. Given a query, come up with a set of web searches " - "to perform to best answer the query. Output between 2 and 3 terms to query for." -) - - -class WebSearchItem(BaseModel): - reason: str - "Your reasoning for why this search is important to the query." - - query: str - "The search term to use for the web search." - - -class WebSearchPlan(BaseModel): - searches: list[WebSearchItem] - """A list of web searches to perform to best answer the query.""" - - -def new_planner_agent(): - return Agent( - name="PlannerAgent", - instructions=PROMPT, - model="gpt-4o", - output_type=WebSearchPlan, - ) diff --git a/tests/contrib/openai_agents/research_agents/printer.py b/tests/contrib/openai_agents/research_agents/printer.py deleted file mode 100644 index fa448e7bf..000000000 --- a/tests/contrib/openai_agents/research_agents/printer.py +++ /dev/null @@ -1,45 +0,0 @@ -from typing import Any - -from rich.console import Console, Group -from rich.live import Live -from rich.spinner import Spinner - - -class Printer: - def __init__(self, console: Console): - self.live = Live(console=console) - self.items: dict[str, tuple[str, bool]] = {} - self.hide_done_ids: set[str] = set() - self.live.start() - - def end(self) -> None: - self.live.stop() - - def hide_done_checkmark(self, item_id: str) -> None: - self.hide_done_ids.add(item_id) - - def update_item( - self, - item_id: str, - content: str, - is_done: bool = False, - hide_checkmark: bool = False, - ) -> None: - self.items[item_id] = (content, is_done) - if hide_checkmark: - self.hide_done_ids.add(item_id) - self.flush() - - def mark_item_done(self, item_id: str) -> None: - self.items[item_id] = (self.items[item_id][0], True) - self.flush() - - def flush(self) -> None: - renderables: list[Any] = [] - for item_id, (content, is_done) in self.items.items(): - if is_done: - prefix = "✅ " if item_id not in self.hide_done_ids else "" - renderables.append(prefix + content) - else: - renderables.append(Spinner("dots", text=content)) - self.live.update(Group(*renderables)) diff --git a/tests/contrib/openai_agents/research_agents/research_manager.py b/tests/contrib/openai_agents/research_agents/research_manager.py deleted file mode 100644 index 98ab550f5..000000000 --- a/tests/contrib/openai_agents/research_agents/research_manager.py +++ /dev/null @@ -1,73 +0,0 @@ -from __future__ import annotations - -import asyncio - -from agents import Runner, custom_span - -import temporalio.workflow -from tests.contrib.openai_agents.research_agents.planner_agent import ( - WebSearchItem, - WebSearchPlan, - new_planner_agent, -) -from tests.contrib.openai_agents.research_agents.search_agent import new_search_agent -from tests.contrib.openai_agents.research_agents.writer_agent import ( - ReportData, - new_writer_agent, -) - - -class ResearchManager: - def __init__(self): - self.search_agent = new_search_agent() - self.planner_agent = new_planner_agent() - self.writer_agent = new_writer_agent() - - async def run(self, query: str) -> str: - with custom_span("Research manager"): - search_plan = await self._plan_searches(query) - search_results = await self._perform_searches(search_plan) - report = await self._write_report(query, search_results) - - return report.markdown_report - - async def _plan_searches(self, query: str) -> WebSearchPlan: - result = await Runner.run( - self.planner_agent, - f"Query: {query}", - ) - return result.final_output_as(WebSearchPlan) - - async def _perform_searches(self, search_plan: WebSearchPlan) -> list[str]: - with custom_span("Search the web"): - num_completed = 0 - tasks = [ - asyncio.create_task(self._search(item)) for item in search_plan.searches - ] - results = [] - for task in temporalio.workflow.as_completed(tasks): - result = await task - if result is not None: - results.append(result) - num_completed += 1 - return results - - async def _search(self, item: WebSearchItem) -> str | None: - input = f"Search term: {item.query}\nReason for searching: {item.reason}" - try: - result = await Runner.run( - self.search_agent, - input, - ) - return str(result.final_output) - except Exception: - raise - - async def _write_report(self, query: str, search_results: list[str]) -> ReportData: - input = f"Original query: {query}\nSummarized search results: {search_results}" - result = await Runner.run( - self.writer_agent, - input, - ) - - return result.final_output_as(ReportData) diff --git a/tests/contrib/openai_agents/research_agents/search_agent.py b/tests/contrib/openai_agents/research_agents/search_agent.py deleted file mode 100644 index 43c30ed88..000000000 --- a/tests/contrib/openai_agents/research_agents/search_agent.py +++ /dev/null @@ -1,20 +0,0 @@ -from agents import Agent, WebSearchTool -from agents.model_settings import ModelSettings - -INSTRUCTIONS = ( - "You are a research assistant. Given a search term, you search the web for that term and " - "produce a concise summary of the results. The summary must 2-3 paragraphs and less than 300 " - "words. Capture the main points. Write succinctly, no need to have complete sentences or good " - "grammar. This will be consumed by someone synthesizing a report, so its vital you capture the " - "essence and ignore any fluff. Do not include any additional commentary other than the summary " - "itself." -) - - -def new_search_agent(): - return Agent( - name="Search agent", - instructions=INSTRUCTIONS, - tools=[WebSearchTool()], - model_settings=ModelSettings(tool_choice="required"), - ) diff --git a/tests/contrib/openai_agents/research_agents/writer_agent.py b/tests/contrib/openai_agents/research_agents/writer_agent.py deleted file mode 100644 index 9d8a34b0b..000000000 --- a/tests/contrib/openai_agents/research_agents/writer_agent.py +++ /dev/null @@ -1,33 +0,0 @@ -# Agent used to synthesize a final report from the individual summaries. -from agents import Agent -from pydantic import BaseModel - -PROMPT = ( - "You are a senior researcher tasked with writing a cohesive report for a research query. " - "You will be provided with the original query, and some initial research done by a research " - "assistant.\n" - "You should first come up with an outline for the report that describes the structure and " - "flow of the report. Then, generate the report and return that as your final output.\n" - "The final output should be in markdown format, and it should be lengthy and detailed. Aim " - "for 5-10 pages of content, at least 1000 words." -) - - -class ReportData(BaseModel): - short_summary: str - """A short 2-3 sentence summary of the findings.""" - - markdown_report: str - """The final report""" - - follow_up_questions: list[str] - """Suggested topics to research further""" - - -def new_writer_agent(): - return Agent( - name="WriterAgent", - instructions=PROMPT, - model="o3-mini", - output_type=ReportData, - ) diff --git a/tests/contrib/openai_agents/test_openai.py b/tests/contrib/openai_agents/test_openai.py deleted file mode 100644 index 28f285f1a..000000000 --- a/tests/contrib/openai_agents/test_openai.py +++ /dev/null @@ -1,2999 +0,0 @@ -import asyncio -import json -import os -import sys -import uuid -from collections.abc import AsyncIterator, Callable, Sequence -from dataclasses import dataclass -from datetime import timedelta -from typing import ( - Any, - cast, -) - -import nexusrpc -import pydantic -import pytest -from agents import ( - Agent, - AgentBase, - AgentOutputSchemaBase, - CodeInterpreterTool, - FileSearchTool, - GuardrailFunctionOutput, - Handoff, - HostedMCPTool, - ImageGenerationTool, - InputGuardrailTripwireTriggered, - ItemHelpers, - MCPToolApprovalFunctionResult, - MCPToolApprovalRequest, - MessageOutputItem, - Model, - ModelProvider, - ModelResponse, - ModelSettings, - ModelTracing, - OpenAIChatCompletionsModel, - OpenAIResponsesModel, - OutputGuardrailTripwireTriggered, - RunConfig, - RunContextWrapper, - Runner, - SQLiteSession, - Tool, - TResponseInputItem, - Usage, - function_tool, - handoff, - input_guardrail, - output_guardrail, - trace, -) -from agents.extensions.handoff_prompt import RECOMMENDED_PROMPT_PREFIX -from agents.items import ( - HandoffOutputItem, - ToolCallItem, - ToolCallOutputItem, - TResponseStreamEvent, -) -from agents.mcp import MCPServer, MCPServerStdio -from agents.sandbox.capabilities.tools import SandboxApplyPatchTool -from agents.tool import CustomTool -from agents.tool_context import ToolContext -from openai import APIStatusError, AsyncOpenAI, BaseModel, RateLimitError -from openai.types.responses import ( - ResponseCodeInterpreterToolCall, - ResponseCustomToolCall, - ResponseFileSearchToolCall, - ResponseFunctionWebSearch, -) -from openai.types.responses.response_file_search_tool_call import Result -from openai.types.responses.response_function_web_search import ActionSearch -from openai.types.responses.response_output_item import ( - ImageGenerationCall, - McpApprovalRequest, - McpCall, -) -from openai.types.responses.response_prompt_param import ResponsePromptParam -from pydantic import ConfigDict, Field, TypeAdapter - -from temporalio import activity, workflow -from temporalio.client import Client, WorkflowFailureError, WorkflowHandle -from temporalio.common import RetryPolicy -from temporalio.contrib import openai_agents -from temporalio.contrib.openai_agents import ( - ModelActivityParameters, - StatefulMCPServerProvider, - StatelessMCPServerProvider, -) -from temporalio.contrib.openai_agents._invoke_model_activity import ( - _build_tool, - _raise_for_openai_status, -) -from temporalio.contrib.openai_agents._model_parameters import ModelSummaryProvider -from temporalio.contrib.openai_agents._openai_runner import ( - _coerce_run_config, - _convert_agent, -) -from temporalio.contrib.openai_agents._temporal_model_stub import ( - _TemporalModelStub, -) -from temporalio.contrib.openai_agents._temporal_worker_env_ref import ( - _WorkerEnvRefResolver, -) -from temporalio.contrib.openai_agents.testing import ( - AgentEnvironment, - ResponseBuilders, - TestModel, - TestModelProvider, -) -from temporalio.contrib.pydantic import pydantic_data_converter -from temporalio.exceptions import ( - ActivityError, - ApplicationError, - CancelledError, - TemporalError, -) -from temporalio.testing import WorkflowEnvironment -from temporalio.workflow import ActivityConfig -from tests.contrib.openai_agents.research_agents.research_manager import ( - ResearchManager, -) -from tests.helpers import assert_eventually, new_worker -from tests.helpers.nexus import make_nexus_endpoint_name - - -def hello_mock_model(): - return TestModel.returning_responses([ResponseBuilders.output_message("test")]) - - -@workflow.defn -class HelloWorldAgent: - @workflow.run - async def run(self, prompt: str) -> str: - agent = Agent[None]( - name="Assistant", - instructions="You only respond in haikus.", - ) - result = await Runner.run(starting_agent=agent, input=prompt) - return result.final_output - - -@pytest.mark.parametrize("use_local_model", [True, False]) -async def test_hello_world_agent(client: Client, use_local_model: bool): - if not use_local_model and not os.environ.get("OPENAI_API_KEY"): - pytest.skip("No openai API key") - - model = hello_mock_model() if use_local_model else None - async with AgentEnvironment( - model=model, - model_params=ModelActivityParameters( - start_to_close_timeout=timedelta(seconds=30), - ), - ) as env: - client = env.applied_on_client(client) - - async with new_worker(client, HelloWorldAgent) as worker: - result = await client.execute_workflow( - HelloWorldAgent.run, - "Tell me about recursion in programming.", - id=f"hello-workflow-{uuid.uuid4()}", - task_queue=worker.task_queue, - execution_timeout=timedelta(seconds=60), - ) - if use_local_model: - assert result == "test" - - -@dataclass -class Weather: - city: str - temperature_range: str - conditions: str - - -@activity.defn -async def get_weather(city: str) -> Weather: - """ - Get the weather for a given city. - """ - return Weather(city=city, temperature_range="14-20C", conditions="Sunny with wind.") - - -@activity.defn -async def get_weather_country(city: str, country: str) -> Weather: # type: ignore[reportUnusedParameter] - """ - Get the weather for a given city in a country. - """ - return Weather(city=city, temperature_range="14-20C", conditions="Sunny with wind.") - - -@dataclass -class WeatherInput: - city: str - - -@activity.defn -async def get_weather_object(input: WeatherInput) -> Weather: - """ - Get the weather for a given city. - """ - return Weather( - city=input.city, temperature_range="14-20C", conditions="Sunny with wind." - ) - - -@activity.defn -async def get_weather_context(ctx: RunContextWrapper[str], city: str) -> Weather: - """ - Get the weather for a given city. - """ - return Weather(city=city, temperature_range="14-20C", conditions=ctx.context) - - -class ActivityWeatherService: - @activity.defn - async def get_weather_method(self, city: str) -> Weather: - """ - Get the weather for a given city. - """ - return Weather( - city=city, temperature_range="14-20C", conditions="Sunny with wind." - ) - - -@nexusrpc.service -class WeatherService: - get_weather_nexus_operation: nexusrpc.Operation[WeatherInput, Weather] - - -@nexusrpc.handler.service_handler(service=WeatherService) -class WeatherServiceHandler: - @nexusrpc.handler.sync_operation - async def get_weather_nexus_operation( - self, - ctx: nexusrpc.handler.StartOperationContext, # type: ignore[reportUnusedParameter] - input: WeatherInput, # type: ignore[reportUnusedParameter] - ) -> Weather: - return Weather( - city=input.city, temperature_range="14-20C", conditions="Sunny with wind." - ) - - -def weather_mock_model(): - return TestModel.returning_responses( - [ - ResponseBuilders.tool_call('{"city":"Tokyo"}', "get_weather"), - ResponseBuilders.tool_call( - '{"input":{"city":"Tokyo"}}', "get_weather_object" - ), - ResponseBuilders.tool_call( - '{"city":"Tokyo","country":"Japan"}', "get_weather_country" - ), - ResponseBuilders.tool_call('{"city":"Tokyo"}', "get_weather_context"), - ResponseBuilders.tool_call('{"city":"Tokyo"}', "get_weather_method"), - ResponseBuilders.output_message("Test weather result"), - ] - ) - - -def nexus_weather_mock_model(): - return TestModel.returning_responses( - [ - ResponseBuilders.tool_call( - '{"input":{"city":"Tokyo"}}', "get_weather_nexus_operation" - ), - ResponseBuilders.output_message("Test nexus weather result"), - ] - ) - - -@workflow.defn -class ToolsWorkflow: - @workflow.run - async def run(self, question: str) -> str: - agent = Agent[str]( - name="Tools Workflow", - instructions="You are a helpful agent.", - tools=[ - openai_agents.workflow.activity_as_tool( - get_weather, start_to_close_timeout=timedelta(seconds=10) - ), - openai_agents.workflow.activity_as_tool( - get_weather_object, start_to_close_timeout=timedelta(seconds=10) - ), - openai_agents.workflow.activity_as_tool( - get_weather_country, start_to_close_timeout=timedelta(seconds=10) - ), - openai_agents.workflow.activity_as_tool( - get_weather_context, start_to_close_timeout=timedelta(seconds=10) - ), - openai_agents.workflow.activity_as_tool( - ActivityWeatherService.get_weather_method, - start_to_close_timeout=timedelta(seconds=10), - ), - openai_agents.workflow.activity_as_tool( - get_weather_failure, - start_to_close_timeout=timedelta(seconds=10), - ), - ], - ) - result = await Runner.run( - starting_agent=agent, input=question, context="Stormy" - ) - return result.final_output - - -@workflow.defn -class NexusToolsWorkflow: - @workflow.run - async def run(self, question: str) -> str: - agent = Agent[str]( - name="Nexus Tools Workflow", - instructions="You are a helpful agent.", - tools=[ - openai_agents.workflow.nexus_operation_as_tool( - WeatherService.get_weather_nexus_operation, - service=WeatherService, - endpoint=make_nexus_endpoint_name(workflow.info().task_queue), - schedule_to_close_timeout=timedelta(seconds=10), - ), - ], - ) - result = await Runner.run( - starting_agent=agent, input=question, context="Stormy" - ) - return result.final_output - - -@pytest.mark.parametrize("use_local_model", [True, False]) -async def test_tool_workflow(client: Client, use_local_model: bool): - if not use_local_model and not os.environ.get("OPENAI_API_KEY"): - pytest.skip("No openai API key") - - model = weather_mock_model() if use_local_model else None - async with AgentEnvironment( - model=model, - model_params=ModelActivityParameters( - start_to_close_timeout=timedelta(seconds=30), - ), - ) as env: - client = env.applied_on_client(client) - - async with new_worker( - client, - ToolsWorkflow, - activities=[ - get_weather, - get_weather_object, - get_weather_country, - get_weather_context, - ActivityWeatherService().get_weather_method, - ], - ) as worker: - workflow_handle = await client.start_workflow( - ToolsWorkflow.run, - "What is the weather in Tokio?", - id=f"tools-workflow-{uuid.uuid4()}", - task_queue=worker.task_queue, - execution_timeout=timedelta(seconds=30), - ) - result = await workflow_handle.result() - - if use_local_model: - assert result == "Test weather result" - - events = [] - async for e in workflow_handle.fetch_history_events(): - if e.HasField("activity_task_completed_event_attributes"): - events.append(e) - - assert len(events) == 11 - assert ( - "function_call" - in events[0] - .activity_task_completed_event_attributes.result.payloads[0] - .data.decode() - ) - assert ( - "Sunny with wind" - in events[1] - .activity_task_completed_event_attributes.result.payloads[0] - .data.decode() - ) - assert ( - "function_call" - in events[2] - .activity_task_completed_event_attributes.result.payloads[0] - .data.decode() - ) - assert ( - "Sunny with wind" - in events[3] - .activity_task_completed_event_attributes.result.payloads[0] - .data.decode() - ) - assert ( - "function_call" - in events[4] - .activity_task_completed_event_attributes.result.payloads[0] - .data.decode() - ) - assert ( - "Sunny with wind" - in events[5] - .activity_task_completed_event_attributes.result.payloads[0] - .data.decode() - ) - assert ( - "function_call" - in events[6] - .activity_task_completed_event_attributes.result.payloads[0] - .data.decode() - ) - assert ( - "Stormy" - in events[7] - .activity_task_completed_event_attributes.result.payloads[0] - .data.decode() - ) - assert ( - "function_call" - in events[8] - .activity_task_completed_event_attributes.result.payloads[0] - .data.decode() - ) - assert ( - "Sunny with wind" - in events[9] - .activity_task_completed_event_attributes.result.payloads[0] - .data.decode() - ) - assert ( - "Test weather result" - in events[10] - .activity_task_completed_event_attributes.result.payloads[0] - .data.decode() - ) - - -@activity.defn -async def get_weather_failure(city: str) -> Weather: # type: ignore[reportUnusedParameter] - """ - Get the weather for a given city. - """ - raise ApplicationError("No weather", non_retryable=True) - - -def weather_failure_mock_model(): - return TestModel.returning_responses( - [ - ResponseBuilders.tool_call('{"city":"Tokyo"}', "get_weather_failure"), - ] - ) - - -async def test_tool_failure_workflow(client: Client): - async with AgentEnvironment( - model=weather_failure_mock_model(), - model_params=ModelActivityParameters( - start_to_close_timeout=timedelta(seconds=30), - ), - ) as env: - client = env.applied_on_client(client) - - async with new_worker( - client, - ToolsWorkflow, - activities=[ - get_weather_failure, - ], - ) as worker: - workflow_handle = await client.start_workflow( - ToolsWorkflow.run, - "What is the weather in Tokio?", - id=f"tools-failure-workflow-{uuid.uuid4()}", - task_queue=worker.task_queue, - execution_timeout=timedelta(seconds=30), - ) - with pytest.raises(WorkflowFailureError) as e: - await workflow_handle.result() - cause = e.value.cause - assert isinstance(cause, ApplicationError) - assert "Workflow failure exception in Agents Framework" in cause.message - - -@pytest.mark.parametrize("use_local_model", [True, False]) -@pytest.mark.requires_local_server -async def test_nexus_tool_workflow( - client: Client, env: WorkflowEnvironment, use_local_model: bool -): - if not use_local_model and not os.environ.get("OPENAI_API_KEY"): - pytest.skip("No openai API key") - - if env.supports_time_skipping: - pytest.skip("Nexus tests don't work with time-skipping server") - - model = nexus_weather_mock_model() if use_local_model else None - async with AgentEnvironment( - model=model, - model_params=ModelActivityParameters( - start_to_close_timeout=timedelta(seconds=30), - ), - ) as agent_env: - client = agent_env.applied_on_client(client) - - async with new_worker( - client, - NexusToolsWorkflow, - nexus_service_handlers=[WeatherServiceHandler()], - ) as worker: - await env.create_nexus_endpoint( - make_nexus_endpoint_name(worker.task_queue), worker.task_queue - ) - - workflow_handle = await client.start_workflow( - NexusToolsWorkflow.run, - "What is the weather in Tokio?", - id=f"nexus-tools-workflow-{uuid.uuid4()}", - task_queue=worker.task_queue, - execution_timeout=timedelta(seconds=30), - ) - result = await workflow_handle.result() - - if use_local_model: - assert result == "Test nexus weather result" - - events = [] - async for e in workflow_handle.fetch_history_events(): - if e.HasField( - "activity_task_completed_event_attributes" - ) or e.HasField("nexus_operation_completed_event_attributes"): - events.append(e) - - assert len(events) == 3 - assert ( - "function_call" - in events[0] - .activity_task_completed_event_attributes.result.payloads[0] - .data.decode() - ) - assert ( - "Sunny with wind" - in events[ - 1 - ].nexus_operation_completed_event_attributes.result.data.decode() - ) - assert ( - "Test nexus weather result" - in events[2] - .activity_task_completed_event_attributes.result.payloads[0] - .data.decode() - ) - - -def research_mock_model(): - responses = [ - ResponseBuilders.output_message( - '{"searches":[{"query":"best Caribbean surfing spots April","reason":"Identify locations with optimal surfing conditions in the Caribbean during April."},{"query":"top Caribbean islands for hiking April","reason":"Find Caribbean islands with excellent hiking opportunities that are ideal in April."},{"query":"Caribbean water sports destinations April","reason":"Locate Caribbean destinations offering a variety of water sports activities in April."},{"query":"surfing conditions Caribbean April","reason":"Understand the surfing conditions and which islands are suitable for surfing in April."},{"query":"Caribbean adventure travel hiking surfing","reason":"Explore adventure travel options that combine hiking and surfing in the Caribbean."},{"query":"best beaches for surfing Caribbean April","reason":"Identify which Caribbean beaches are renowned for surfing in April."},{"query":"Caribbean islands with national parks hiking","reason":"Find islands with national parks or reserves that offer hiking trails."},{"query":"Caribbean weather April surfing conditions","reason":"Research the weather conditions in April affecting surfing in the Caribbean."},{"query":"Caribbean water sports rentals April","reason":"Look for places where water sports equipment can be rented in the Caribbean during April."},{"query":"Caribbean multi-activity vacation packages","reason":"Look for vacation packages that offer a combination of surfing, hiking, and water sports."}]}' - ) - ] - for _ in range(10): - responses.append( - ModelResponse( - output=[ - ResponseFunctionWebSearch( - id="", - status="completed", - type="web_search_call", - action=ActionSearch.model_construct( - type="search", queries=[""] - ), - ), - ResponseBuilders.response_output_message("Granada"), - ], - usage=Usage(), - response_id=None, - ) - ) - responses.append( - ResponseBuilders.output_message( - '{"follow_up_questions":[], "markdown_report":"report", "short_summary":"rep"}' - ) - ) - return TestModel.returning_responses(responses) - - -@workflow.defn -class ResearchWorkflow: - @workflow.run - async def run(self, query: str): - return await ResearchManager().run(query) - - -@pytest.mark.parametrize("use_local_model", [True, False]) -@pytest.mark.timeout(120) -async def test_research_workflow(client: Client, use_local_model: bool): - if not use_local_model and not os.environ.get("OPENAI_API_KEY"): - pytest.skip("No openai API key") - - model = research_mock_model() if use_local_model else None - async with AgentEnvironment( - model=model, - model_params=ModelActivityParameters( - start_to_close_timeout=timedelta(seconds=120), - schedule_to_close_timeout=timedelta(seconds=120), - ), - ) as env: - client = env.applied_on_client(client) - - async with new_worker( - client, - ResearchWorkflow, - ) as worker: - workflow_handle = await client.start_workflow( - ResearchWorkflow.run, - "Caribbean vacation spots in April, optimizing for surfing, hiking and water sports", - id=f"research-workflow-{uuid.uuid4()}", - task_queue=worker.task_queue, - execution_timeout=timedelta(seconds=120), - ) - result = await workflow_handle.result() - - if use_local_model: - assert result == "report" - - events = [] - async for e in workflow_handle.fetch_history_events(): - if e.HasField("activity_task_completed_event_attributes"): - events.append(e) - - assert len(events) == 12 - assert ( - '"type":"output_text"' - in events[0] - .activity_task_completed_event_attributes.result.payloads[0] - .data.decode() - ) - for i in range(1, 11): - assert ( - "web_search_call" - in events[i] - .activity_task_completed_event_attributes.result.payloads[0] - .data.decode() - ) - - assert ( - '"type":"output_text"' - in events[11] - .activity_task_completed_event_attributes.result.payloads[0] - .data.decode() - ) - - -def orchestrator_agent() -> Agent: - spanish_agent = Agent[None]( - name="spanish_agent", - instructions="You translate the user's message to Spanish", - handoff_description="An english to spanish translator", - ) - - french_agent = Agent[None]( - name="french_agent", - instructions="You translate the user's message to French", - handoff_description="An english to french translator", - ) - - italian_agent = Agent[None]( - name="italian_agent", - instructions="You translate the user's message to Italian", - handoff_description="An english to italian translator", - ) - - orchestrator_agent = Agent[None]( - name="orchestrator_agent", - instructions=( - "You are a translation agent. You use the tools given to you to translate." - "If asked for multiple translations, you call the relevant tools in order." - "You never translate on your own, you always use the provided tools." - ), - tools=[ - spanish_agent.as_tool( - tool_name="translate_to_spanish", - tool_description="Translate the user's message to Spanish", - ), - french_agent.as_tool( - tool_name="translate_to_french", - tool_description="Translate the user's message to French", - ), - italian_agent.as_tool( - tool_name="translate_to_italian", - tool_description="Translate the user's message to Italian", - ), - ], - ) - return orchestrator_agent - - -def synthesizer_agent() -> Agent: - return Agent( - name="synthesizer_agent", - instructions="You inspect translations, correct them if needed, and produce a final concatenated response.", - ) - - -@workflow.defn -class AgentsAsToolsWorkflow: - @workflow.run - async def run(self, msg: str) -> str: - # Run the entire orchestration in a single trace - with trace("Orchestrator evaluator"): - orchestrator = orchestrator_agent() - synthesizer = synthesizer_agent() - - orchestrator_result = await Runner.run( - starting_agent=orchestrator, input=msg - ) - - for item in orchestrator_result.new_items: - if isinstance(item, MessageOutputItem): - text = ItemHelpers.text_message_output(item) - if text: - print(f" - Translation step: {text}") - - synthesizer_result = await Runner.run( - starting_agent=synthesizer, input=orchestrator_result.to_input_list() - ) - - return synthesizer_result.final_output - - -def agent_as_tools_mock_model(): - return TestModel.returning_responses( - [ - ResponseBuilders.tool_call('{"input":"I am full"}', "translate_to_spanish"), - ResponseBuilders.output_message("Estoy lleno."), - ResponseBuilders.output_message( - 'The translation to Spanish is: "Estoy lleno."' - ), - ResponseBuilders.output_message( - 'The translation to Spanish is: "Estoy lleno."' - ), - ] - ) - - -@pytest.mark.parametrize("use_local_model", [True, False]) -async def test_agents_as_tools_workflow(client: Client, use_local_model: bool): - if not use_local_model and not os.environ.get("OPENAI_API_KEY"): - pytest.skip("No openai API key") - - model = agent_as_tools_mock_model() if use_local_model else None - async with AgentEnvironment( - model=model, - model_params=ModelActivityParameters( - start_to_close_timeout=timedelta(seconds=30), - ), - ) as env: - client = env.applied_on_client(client) - - async with new_worker( - client, - AgentsAsToolsWorkflow, - ) as worker: - workflow_handle = await client.start_workflow( - AgentsAsToolsWorkflow.run, - "Translate to Spanish: 'I am full'", - id=f"agents-as-tools-workflow-{uuid.uuid4()}", - task_queue=worker.task_queue, - execution_timeout=timedelta(seconds=30), - ) - result = await workflow_handle.result() - - if use_local_model: - assert result == 'The translation to Spanish is: "Estoy lleno."' - - events = [] - async for e in workflow_handle.fetch_history_events(): - if e.HasField("activity_task_completed_event_attributes"): - events.append(e) - - assert len(events) == 4 - assert ( - "function_call" - in events[0] - .activity_task_completed_event_attributes.result.payloads[0] - .data.decode() - ) - assert ( - "Estoy lleno" - in events[1] - .activity_task_completed_event_attributes.result.payloads[0] - .data.decode() - ) - assert ( - "The translation to Spanish is:" - in events[2] - .activity_task_completed_event_attributes.result.payloads[0] - .data.decode() - ) - assert ( - "The translation to Spanish is:" - in events[3] - .activity_task_completed_event_attributes.result.payloads[0] - .data.decode() - ) - - -class AirlineAgentContext(BaseModel): - passenger_name: str | None = None - confirmation_number: str | None = None - seat_number: str | None = None - flight_number: str | None = None - - -@function_tool( - name_override="faq_lookup_tool", - description_override="Lookup frequently asked questions.", -) -async def faq_lookup_tool(question: str) -> str: - if "bag" in question or "baggage" in question: - return ( - "You are allowed to bring one bag on the plane. " - "It must be under 50 pounds and 22 inches x 14 inches x 9 inches." - ) - elif "seats" in question or "plane" in question: - return ( - "There are 120 seats on the plane. " - "There are 22 business class seats and 98 economy seats. " - "Exit rows are rows 4 and 16. " - "Rows 5-8 are Economy Plus, with extra legroom. " - ) - elif "wifi" in question: - return "We have free wifi on the plane, join Airline-Wifi" - return "I'm sorry, I don't know the answer to that question." - - -@function_tool -async def update_seat( - context: RunContextWrapper[AirlineAgentContext], - confirmation_number: str, - new_seat: str, -) -> str: - # Update the context based on the customer's input - context.context.confirmation_number = confirmation_number - context.context.seat_number = new_seat - # Ensure that the flight number has been set by the incoming handoff - assert context.context.flight_number is not None, "Flight number is required" - return f"Updated seat to {new_seat} for confirmation number {confirmation_number}" - - -### HOOKS - - -async def on_seat_booking_handoff( - context: RunContextWrapper[AirlineAgentContext], -) -> None: - try: - flight_number = f"FLT-{workflow.random().randint(100, 999)}" - except TemporalError: - flight_number = "FLT-100" - context.context.flight_number = flight_number - - -### AGENTS - - -def init_agents() -> Agent[AirlineAgentContext]: - """ - Initialize the agents for the airline customer service workflow. - :return: triage agent - """ - faq_agent = Agent[AirlineAgentContext]( - name="FAQ Agent", - handoff_description="A helpful agent that can answer questions about the airline.", - instructions=f"""{RECOMMENDED_PROMPT_PREFIX} - You are an FAQ agent. If you are speaking to a customer, you probably were transferred to from the triage agent. - Use the following routine to support the customer. - # Routine - 1. Identify the last question asked by the customer. - 2. Use the faq lookup tool to answer the question. Do not rely on your own knowledge. - 3. If you cannot answer the question, transfer back to the triage agent.""", - tools=[faq_lookup_tool], - ) - - seat_booking_agent = Agent[AirlineAgentContext]( - name="Seat Booking Agent", - handoff_description="A helpful agent that can update a seat on a flight.", - instructions=f"""{RECOMMENDED_PROMPT_PREFIX} - You are a seat booking agent. If you are speaking to a customer, you probably were transferred to from the triage agent. - Use the following routine to support the customer. - # Routine - 1. Ask for their confirmation number. - 2. Ask the customer what their desired seat number is. - 3. Use the update seat tool to update the seat on the flight. - If the customer asks a question that is not related to the routine, transfer back to the triage agent. """, - tools=[update_seat], - ) - - triage_agent = Agent[AirlineAgentContext]( - name="Triage Agent", - handoff_description="A triage agent that can delegate a customer's request to the appropriate agent.", - instructions=( - f"{RECOMMENDED_PROMPT_PREFIX} " - "You are a helpful triaging agent. You can use your tools to delegate questions to other appropriate agents." - ), - handoffs=[ - faq_agent, - handoff(agent=seat_booking_agent, on_handoff=on_seat_booking_handoff), - ], - ) - - faq_agent.handoffs.append(triage_agent) - seat_booking_agent.handoffs.append(triage_agent) - return triage_agent - - -class ProcessUserMessageInput(BaseModel): - user_input: str - chat_length: int - - -def customer_service_mock_model(): - return TestModel.returning_responses( - [ - ResponseBuilders.output_message("Hi there! How can I assist you today?"), - ResponseBuilders.tool_call("{}", "transfer_to_seat_booking_agent"), - ResponseBuilders.output_message( - "Could you please provide your confirmation number?" - ), - ResponseBuilders.output_message( - "Thanks! What seat number would you like to change to?" - ), - ResponseBuilders.tool_call( - '{"confirmation_number":"11111","new_seat":"window seat"}', - "update_seat", - ), - ResponseBuilders.output_message( - "Your seat has been updated to a window seat. If there's anything else you need, feel free to let me know!" - ), - ResponseBuilders.tool_call("{}", "transfer_to_triage_agent"), - ResponseBuilders.output_message("You're welcome!"), - ] - ) - - -@workflow.defn -class CustomerServiceWorkflow: - def __init__(self, input_items: list[TResponseInputItem] = []): - self.chat_history: list[str] = [] - self.current_agent: Agent[AirlineAgentContext] = init_agents() - self.context = AirlineAgentContext() - self.input_items = input_items - - @workflow.run - async def run(self, _input_items: list[TResponseInputItem] = []): - await workflow.wait_condition(lambda: False) - workflow.continue_as_new(self.input_items) - - @workflow.query - def get_chat_history(self) -> list[str]: - return self.chat_history - - @workflow.update - async def process_user_message(self, input: ProcessUserMessageInput) -> list[str]: - length = len(self.chat_history) - self.chat_history.append(f"User: {input.user_input}") - with trace("Customer service", group_id=workflow.info().workflow_id): - self.input_items.append({"content": input.user_input, "role": "user"}) - result = await Runner.run( - starting_agent=self.current_agent, - input=self.input_items, - context=self.context, - ) - - for new_item in result.new_items: - agent_name = new_item.agent.name - if isinstance(new_item, MessageOutputItem): - self.chat_history.append( - f"{agent_name}: {ItemHelpers.text_message_output(new_item)}" - ) - elif isinstance(new_item, HandoffOutputItem): - self.chat_history.append( - f"Handed off from {new_item.source_agent.name} to {new_item.target_agent.name}" - ) - elif isinstance(new_item, ToolCallItem): - self.chat_history.append(f"{agent_name}: Calling a tool") - elif isinstance(new_item, ToolCallOutputItem): - self.chat_history.append( - f"{agent_name}: Tool call output: {new_item.output}" - ) - else: - self.chat_history.append( - f"{agent_name}: Skipping item: {new_item.__class__.__name__}" - ) - self.input_items = result.to_input_list() - self.current_agent = result.last_agent - workflow.set_current_details("\n\n".join(self.chat_history)) - return self.chat_history[length:] - - @process_user_message.validator - def validate_process_user_message(self, input: ProcessUserMessageInput) -> None: - if not input.user_input: - raise ValueError("User input cannot be empty.") - if len(input.user_input) > 1000: - raise ValueError("User input is too long. Please limit to 1000 characters.") - if input.chat_length != len(self.chat_history): - raise ValueError("Stale chat history. Please refresh the chat.") - - -@pytest.mark.parametrize("use_local_model", [True, False]) -async def test_customer_service_workflow(client: Client, use_local_model: bool): - if not use_local_model and not os.environ.get("OPENAI_API_KEY"): - pytest.skip("No openai API key") - - model = customer_service_mock_model() if use_local_model else None - async with AgentEnvironment( - model=model, - model_params=ModelActivityParameters( - start_to_close_timeout=timedelta(seconds=30), - ), - ) as env: - client = env.applied_on_client(client) - - questions = [ - "Hello", - "Book me a flight to PDX", - "11111", - "Any window seat", - "Take me back to the triage agent to say goodbye", - ] - - async with new_worker( - client, - CustomerServiceWorkflow, - ) as worker: - workflow_handle = await client.start_workflow( - CustomerServiceWorkflow.run, - id=f"customer-service-{uuid.uuid4()}", - task_queue=worker.task_queue, - execution_timeout=timedelta(seconds=60), - ) - history: list[Any] = [] - for q in questions: - message_input = ProcessUserMessageInput( - user_input=q, chat_length=len(history) - ) - new_history = await workflow_handle.execute_update( - CustomerServiceWorkflow.process_user_message, message_input - ) - history.extend(new_history) - print(*new_history, sep="\n") - - await workflow_handle.cancel() - - with pytest.raises(WorkflowFailureError) as err: - await workflow_handle.result() - assert isinstance(err.value.cause, CancelledError) - - if use_local_model: - events = [] - async for e in WorkflowHandle( - client, - workflow_handle.id, - run_id=workflow_handle._first_execution_run_id, - ).fetch_history_events(): - if e.HasField("activity_task_completed_event_attributes"): - events.append(e) - - assert len(events) == 8 - assert ( - "Hi there! How can I assist you today?" - in events[0] - .activity_task_completed_event_attributes.result.payloads[0] - .data.decode() - ) - assert ( - "transfer_to_seat_booking_agent" - in events[1] - .activity_task_completed_event_attributes.result.payloads[0] - .data.decode() - ) - assert ( - "Could you please provide your confirmation number?" - in events[2] - .activity_task_completed_event_attributes.result.payloads[0] - .data.decode() - ) - assert ( - "Thanks! What seat number would you like to change to?" - in events[3] - .activity_task_completed_event_attributes.result.payloads[0] - .data.decode() - ) - assert ( - "update_seat" - in events[4] - .activity_task_completed_event_attributes.result.payloads[0] - .data.decode() - ) - assert ( - "Your seat has been updated to a window seat. If there's anything else you need, feel free to let me know!" - in events[5] - .activity_task_completed_event_attributes.result.payloads[0] - .data.decode() - ) - assert ( - "transfer_to_triage_agent" - in events[6] - .activity_task_completed_event_attributes.result.payloads[0] - .data.decode() - ) - assert ( - "You're welcome!" - in events[7] - .activity_task_completed_event_attributes.result.payloads[0] - .data.decode() - ) - - -class InputGuardrailModel(OpenAIResponsesModel): - __test__ = False - responses: list[ModelResponse] = [ - ResponseBuilders.output_message("The capital of California is Sacramento."), - ResponseBuilders.output_message("x=3"), - ] - guardrail_responses = [ - ResponseBuilders.output_message( - '{"is_math_homework":false,"reasoning":"The question asked is about the capital of California, which is a geography-related query, not math."}' - ), - ResponseBuilders.output_message( - '{"is_math_homework":true,"reasoning":"The question involves solving an equation for a variable, which is a typical math homework problem."}' - ), - ] - - def __init__( - self, - model: str, - openai_client: AsyncOpenAI, - ) -> None: - super().__init__(model, openai_client) - self._responses = iter(self.responses) - self._guardrail_responses = iter(self.guardrail_responses) - - async def get_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, - previous_response_id: str | None = None, - conversation_id: str | None = None, - prompt: ResponsePromptParam | None = None, - ) -> ModelResponse: - if ( - system_instructions - == "Check if the user is asking you to do their math homework." - ): - return next(self._guardrail_responses) - else: - return next(self._responses) - - -### 1. An agent-based guardrail that is triggered if the user is asking to do math homework -class MathHomeworkOutput(BaseModel): - reasoning: str - is_math_homework: bool - model_config = ConfigDict(extra="forbid") - - -guardrail_agent: Agent = Agent( - name="Guardrail check", - instructions="Check if the user is asking you to do their math homework.", - output_type=MathHomeworkOutput, -) - - -@input_guardrail -async def math_guardrail( - context: RunContextWrapper[None], - _agent: Agent, - input: str | list[TResponseInputItem], -) -> GuardrailFunctionOutput: - """This is an input guardrail function, which happens to call an agent to check if the input - is a math homework question. - """ - result = await Runner.run(guardrail_agent, input, context=context.context) - final_output = result.final_output_as(MathHomeworkOutput) - - return GuardrailFunctionOutput( - output_info=final_output, - tripwire_triggered=final_output.is_math_homework, - ) - - -@workflow.defn -class InputGuardrailWorkflow: - @workflow.run - async def run(self, messages: list[str]) -> list[str]: - agent = Agent( - name="Customer support agent", - instructions="You are a customer support agent. You help customers with their questions.", - input_guardrails=[math_guardrail], - ) - - input_data: list[TResponseInputItem] = [] - results: list[str] = [] - - for user_input in messages: - input_data.append( - { - "role": "user", - "content": user_input, - } - ) - - try: - result = await Runner.run(agent, input_data) - results.append(result.final_output) - # If the guardrail didn't trigger, we use the result as the input for the next run - input_data = result.to_input_list() - except InputGuardrailTripwireTriggered: - # If the guardrail triggered, we instead add a refusal message to the input - message = "Sorry, I can't help you with your math homework." - results.append(message) - input_data.append( - { - "role": "assistant", - "content": message, - } - ) - return results - - -@pytest.mark.parametrize("use_local_model", [True, False]) -async def test_input_guardrail(client: Client, use_local_model: bool): - if not use_local_model and not os.environ.get("OPENAI_API_KEY"): - pytest.skip("No openai API key") - - model = ( - InputGuardrailModel("", openai_client=AsyncOpenAI(api_key="Fake key")) - if use_local_model - else None - ) - async with AgentEnvironment( - model=model, - model_params=ModelActivityParameters( - start_to_close_timeout=timedelta(seconds=30), - ), - ) as env: - client = env.applied_on_client(client) - - async with new_worker( - client, - InputGuardrailWorkflow, - ) as worker: - workflow_handle = await client.start_workflow( - InputGuardrailWorkflow.run, - [ - "What's the capital of California?", - "Can you help me solve for x: 2x + 5 = 11", - ], - id=f"input-guardrail-{uuid.uuid4()}", - task_queue=worker.task_queue, - execution_timeout=timedelta(seconds=60), - ) - result = await workflow_handle.result() - - if use_local_model: - assert len(result) == 2 - assert result[0] == "The capital of California is Sacramento." - assert result[1] == "Sorry, I can't help you with your math homework." - - -def output_guardrail_mock_model(): - return TestModel.returning_responses( - [ - ResponseBuilders.output_message( - '{"reasoning":"The phone number\'s area code (650) is associated with a region. However, the exact location is not definitive, but it\'s commonly linked to the San Francisco Peninsula in California, including cities like San Mateo, Palo Alto, and parts of Silicon Valley. It\'s important to note that area codes don\'t always guarantee a specific location due to mobile number portability.","response":"The area code 650 is typically associated with California, particularly the San Francisco Peninsula, including cities like Palo Alto and San Mateo.","user_name":null}' - ) - ] - ) - - -# The agent's output type -class MessageOutput(BaseModel): - reasoning: str = Field( - description="Thoughts on how to respond to the user's message" - ) - response: str = Field(description="The response to the user's message") - user_name: str | None = Field( - description="The name of the user who sent the message, if known" - ) - model_config = ConfigDict(extra="forbid") - - -@output_guardrail -async def sensitive_data_check( - _context: RunContextWrapper, _agent: Agent, output: MessageOutput -) -> GuardrailFunctionOutput: - phone_number_in_response = "650" in output.response - phone_number_in_reasoning = "650" in output.reasoning - - return GuardrailFunctionOutput( - output_info={ - "phone_number_in_response": phone_number_in_response, - "phone_number_in_reasoning": phone_number_in_reasoning, - }, - tripwire_triggered=phone_number_in_response or phone_number_in_reasoning, - ) - - -output_guardrail_agent = Agent( - name="Assistant", - instructions="You are a helpful assistant.", - output_type=MessageOutput, - output_guardrails=[sensitive_data_check], -) - - -@workflow.defn -class OutputGuardrailWorkflow: - @workflow.run - async def run(self) -> bool: - try: - await Runner.run( - output_guardrail_agent, - "My phone number is 650-123-4567. Where do you think I live?", - ) - return True - except OutputGuardrailTripwireTriggered: - return False - - -@pytest.mark.parametrize("use_local_model", [True, False]) -async def test_output_guardrail(client: Client, use_local_model: bool): - if not use_local_model and not os.environ.get("OPENAI_API_KEY"): - pytest.skip("No openai API key") - - model = output_guardrail_mock_model() if use_local_model else None - async with AgentEnvironment( - model=model, - model_params=ModelActivityParameters( - start_to_close_timeout=timedelta(seconds=30), - ), - ) as env: - client = env.applied_on_client(client) - - async with new_worker( - client, - OutputGuardrailWorkflow, - ) as worker: - workflow_handle = await client.start_workflow( - OutputGuardrailWorkflow.run, - id=f"output-guardrail-{uuid.uuid4()}", - task_queue=worker.task_queue, - ) - result = await workflow_handle.result() - - if use_local_model: - assert not result - - -def workflow_tool_mock_model(): - return TestModel.returning_responses( - [ - ResponseBuilders.tool_call("{}", "run_tool"), - ResponseBuilders.output_message("Workflow tool was used"), - ] - ) - - -@workflow.defn -class WorkflowToolWorkflow: - @workflow.run - async def run(self) -> None: - agent: Agent = Agent( - name="Assistant", - instructions="You are a helpful assistant.", - model="gpt-4o", - tools=[function_tool(self.run_tool)], - ) - await Runner.run( - agent, - "My phone number is 650-123-4567. Where do you think I live?", - ) - - async def run_tool(self): - print("Tool ran with self:", self) - workflow.logger.info("Tool ran with self: %s", self) - return None - - -async def test_workflow_method_tools(client: Client): - async with AgentEnvironment( - model=workflow_tool_mock_model(), - model_params=ModelActivityParameters( - start_to_close_timeout=timedelta(seconds=30), - ), - ) as env: - client = env.applied_on_client(client) - - async with new_worker( - client, - WorkflowToolWorkflow, - ) as worker: - workflow_handle = await client.start_workflow( - WorkflowToolWorkflow.run, - id=f"workflow-tool-{uuid.uuid4()}", - task_queue=worker.task_queue, - ) - await workflow_handle.result() - - -async def test_response_serialization(): - # This should not be used in another test, or this test needs to change to use another unloaded type - from openai.types.responses.response_output_item import LocalShellCall - - data = json.loads( - b'{"id":"", "action":{"command": [],"env": {},"type": "exec"},"call_id":"","status":"completed","type":"local_shell_call"}' - ) - call = TypeAdapter(LocalShellCall).validate_python(data) - model_response = ModelResponse( - output=[ - call, - ], - usage=Usage(), - response_id="", - ) - await pydantic_data_converter.encode([model_response]) - - -async def assert_status_retry_behavior( - status: int, - client: Client, - should_retry: bool, - *, - retry_policy: RetryPolicy | None = None, -) -> None: - def status_error(status: int) -> ModelResponse: - with workflow.unsafe.imports_passed_through(): - with workflow.unsafe.sandbox_unrestricted(): - import httpx - error_type: type[APIStatusError] = ( - RateLimitError if status == 429 else APIStatusError - ) - raise error_type( - message="Something went wrong.", - response=httpx.Response( - status_code=status, request=httpx.Request("GET", url="") - ), - body=None, - ) - - async with AgentEnvironment( - model=TestModel(lambda: status_error(status)), - model_params=ModelActivityParameters( - retry_policy=retry_policy or RetryPolicy(maximum_attempts=2), - ), - ) as env: - client = env.applied_on_client(client) - - async with new_worker( - client, - HelloWorldAgent, - ) as worker: - workflow_handle = await client.start_workflow( - HelloWorldAgent.run, - "Input", - id=f"workflow-tool-{uuid.uuid4()}", - task_queue=worker.task_queue, - ) - with pytest.raises(WorkflowFailureError) as err: - await workflow_handle.result() - assert isinstance(err.value.cause, ActivityError) - assert isinstance(err.value.cause.cause, ApplicationError) - assert err.value.cause.cause.type == "APIStatusError" - - found = False - async for event in workflow_handle.fetch_history_events(): - if event.HasField("activity_task_started_event_attributes"): - found = True - if should_retry: - assert event.activity_task_started_event_attributes.attempt == 2 - else: - assert event.activity_task_started_event_attributes.attempt == 1 - assert found - - -async def test_exception_handling(client: Client): - await assert_status_retry_behavior(408, client, should_retry=True) - await assert_status_retry_behavior(409, client, should_retry=True) - await assert_status_retry_behavior(429, client, should_retry=True) - await assert_status_retry_behavior(500, client, should_retry=True) - - await assert_status_retry_behavior(400, client, should_retry=False) - await assert_status_retry_behavior(403, client, should_retry=False) - await assert_status_retry_behavior(404, client, should_retry=False) - await assert_status_retry_behavior( - 429, - client, - should_retry=False, - retry_policy=RetryPolicy( - maximum_attempts=2, - non_retryable_error_types=["APIStatusError"], - ), - ) - - -def _openai_status_error(status: int, headers: dict[str, str]) -> APIStatusError: - import httpx - - return APIStatusError( - message="Something went wrong.", - response=httpx.Response( - status_code=status, - request=httpx.Request("GET", url=""), - headers=headers, - ), - body=None, - ) - - -def test_retry_after_ms_propagated_when_server_requests_retry(): - with pytest.raises(ApplicationError) as err: - _raise_for_openai_status( - _openai_status_error( - 429, {"x-should-retry": "true", "retry-after-ms": "5000"} - ) - ) - assert not err.value.non_retryable - assert err.value.type == "APIStatusError" - assert err.value.next_retry_delay == timedelta(milliseconds=5000) - - -def test_retry_after_seconds_propagated_when_server_requests_retry(): - with pytest.raises(ApplicationError) as err: - _raise_for_openai_status( - _openai_status_error(429, {"x-should-retry": "true", "retry-after": "5"}) - ) - assert not err.value.non_retryable - assert err.value.next_retry_delay == timedelta(seconds=5) - - -def test_should_retry_true_overrides_non_retryable_status(): - with pytest.raises(ApplicationError) as err: - _raise_for_openai_status(_openai_status_error(400, {"x-should-retry": "true"})) - assert not err.value.non_retryable - - -def test_should_retry_false_stays_non_retryable(): - with pytest.raises(ApplicationError) as err: - _raise_for_openai_status( - _openai_status_error( - 429, {"x-should-retry": "false", "retry-after-ms": "5000"} - ) - ) - assert err.value.non_retryable - assert err.value.next_retry_delay == timedelta(milliseconds=5000) - - -def test_retry_after_ms_takes_precedence_over_retry_after(): - with pytest.raises(ApplicationError) as err: - _raise_for_openai_status( - _openai_status_error(429, {"retry-after-ms": "1500", "retry-after": "60"}) - ) - assert err.value.next_retry_delay == timedelta(milliseconds=1500) - - -class CustomModelProvider(ModelProvider): - def get_model(self, model_name: str | None) -> Model: - client = AsyncOpenAI(base_url="https://api.openai.com/v1") - return OpenAIChatCompletionsModel(model="gpt-4o", openai_client=client) - - -async def test_chat_completions_model(client: Client): - if not os.environ.get("OPENAI_API_KEY"): - pytest.skip("No openai API key") - - async with AgentEnvironment( - model_provider=CustomModelProvider(), - model_params=ModelActivityParameters( - start_to_close_timeout=timedelta(seconds=30), - ), - ) as env: - client = env.applied_on_client(client) - - async with new_worker( - client, - WorkflowToolWorkflow, - ) as worker: - workflow_handle = await client.start_workflow( - WorkflowToolWorkflow.run, - id=f"workflow-tool-{uuid.uuid4()}", - task_queue=worker.task_queue, - ) - await workflow_handle.result() - - -class WaitModel(Model): - async def get_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, # type:ignore[reportMissingParameterType] - ) -> ModelResponse: - activity.logger.info("Waiting") - await asyncio.sleep(1.0) - activity.logger.info("Returning") - return ResponseBuilders.output_message("test") - - 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, # type:ignore[reportMissingParameterType] - ) -> AsyncIterator[TResponseStreamEvent]: - raise NotImplementedError() - - -@workflow.defn -class AlternateModelAgent: - @workflow.run - async def run(self, prompt: str) -> str: - agent = Agent[None]( - name="Assistant", - instructions="You only respond in haikus.", - model="test_model", - ) - result = await Runner.run(starting_agent=agent, input=prompt) - return result.final_output - - -class CheckModelNameProvider(ModelProvider): - def get_model(self, model_name: str | None) -> Model: - assert model_name == "test_model" - return hello_mock_model() - - -async def test_alternative_model(client: Client): - async with AgentEnvironment( - model_provider=CheckModelNameProvider(), - model_params=ModelActivityParameters( - start_to_close_timeout=timedelta(seconds=30), - ), - ) as env: - client = env.applied_on_client(client) - - async with new_worker( - client, - AlternateModelAgent, - ) as worker: - workflow_handle = await client.start_workflow( - AlternateModelAgent.run, - "Hello", - id=f"alternative-model-{uuid.uuid4()}", - task_queue=worker.task_queue, - ) - await workflow_handle.result() - - -async def test_heartbeat(client: Client, env: WorkflowEnvironment): - if env.supports_time_skipping: - pytest.skip("Relies on real timing, skip.") - - async with AgentEnvironment( - model=WaitModel(), - model_params=ModelActivityParameters( - heartbeat_timeout=timedelta(seconds=0.5), - ), - ) as agent_env: - client = agent_env.applied_on_client(client) - - async with new_worker( - client, - HelloWorldAgent, - ) as worker: - workflow_handle = await client.start_workflow( - HelloWorldAgent.run, - "Tell me about recursion in programming.", - id=f"workflow-tool-{uuid.uuid4()}", - task_queue=worker.task_queue, - ) - await workflow_handle.result() - - -@workflow.defn -class SessionWorkflow: - @workflow.run - async def run(self) -> None: - agent: Agent = Agent( - name="Assistant", - instructions="You are a helpful assistant.", - ) - await Runner.run( - agent, - "My phone number is 650-123-4567. Where do you think I live?", - session=SQLiteSession(session_id="id"), - ) - - -async def test_session(client: Client): - async with AgentEnvironment(model=hello_mock_model()) as env: - client = env.applied_on_client(client) - - async with new_worker( - client, - SessionWorkflow, - ) as worker: - workflow_handle = await client.start_workflow( - SessionWorkflow.run, - id=f"session-{uuid.uuid4()}", - task_queue=worker.task_queue, - retry_policy=RetryPolicy(maximum_attempts=1), - ) - - async def check(): - async for evt in workflow_handle.fetch_history_events(): - # Sometimes just creating the sqlite session takes too long for a workflow in CI, so check both - if evt.HasField("workflow_task_failed_event_attributes") and ( - "Temporal workflows don't support SQLite sessions" - in evt.workflow_task_failed_event_attributes.failure.message - or "Potential deadlock detected" - in evt.workflow_task_failed_event_attributes.failure.message - ): - return - - await assert_eventually(check) - - -async def test_lite_llm(client: Client): - if not os.environ.get("OPENAI_API_KEY"): - pytest.skip("No openai API key") - if sys.version_info >= (3, 14): - pytest.skip("Lite LLM does not yet support Python 3.14") # type:ignore[reportUnreachable] - - from agents.extensions.models.litellm_provider import ( - LitellmProvider, # type:ignore[reportUnreachable] - ) - - async with AgentEnvironment( - model_provider=LitellmProvider(), - model_params=ModelActivityParameters( - start_to_close_timeout=timedelta(seconds=30), - ), - ) as agent_env: - client = agent_env.applied_on_client(client) # type:ignore[reportUnreachable] - - async with new_worker( - client, - HelloWorldAgent, - ) as worker: - workflow_handle = await client.start_workflow( # type:ignore[reportUnreachable] - HelloWorldAgent.run, - "Tell me about recursion in programming", - id=f"lite-llm-{uuid.uuid4()}", - task_queue=worker.task_queue, - ) - await workflow_handle.result() - - -def file_search_tool_mock_model(): - return TestModel.returning_responses( - [ - ModelResponse( - output=[ - ResponseFileSearchToolCall( - queries=["side character in the Iliad"], - type="file_search_call", - id="id", - status="completed", - results=[ - Result(text="Some scene"), - Result(text="Other scene"), - ], - ), - ResponseBuilders.response_output_message("Patroclus"), - ], - usage=Usage(), - response_id=None, - ), - ] - ) - - -@workflow.defn -class FileSearchToolWorkflow: - @workflow.run - async def run(self, question: str) -> str: - agent = Agent[str]( - name="File Search Workflow", - instructions="You are a librarian. You should use your tools to source all your information.", - tools=[ - FileSearchTool( - max_num_results=3, - vector_store_ids=["vs_687fd7f5e69c8191a2740f06bc9a159d"], - include_search_results=True, - ) - ], - ) - result = await Runner.run(starting_agent=agent, input=question) - - # A file search was performed - assert any( - isinstance(item, ToolCallItem) - and isinstance(item.raw_item, ResponseFileSearchToolCall) - for item in result.new_items - ) - return result.final_output - - -@pytest.mark.parametrize("use_local_model", [True, False]) -async def test_file_search_tool(client: Client, use_local_model: bool): - if not use_local_model and not os.environ.get("OPENAI_API_KEY"): - pytest.skip("No openai API key") - - model = file_search_tool_mock_model() if use_local_model else None - async with AgentEnvironment( - model_params=ModelActivityParameters( - start_to_close_timeout=timedelta(seconds=30) - ), - model=model, - ) as env: - client = env.applied_on_client(client) - - async with new_worker( - client, - FileSearchToolWorkflow, - ) as worker: - workflow_handle = await client.start_workflow( - FileSearchToolWorkflow.run, - "Tell me about a side character in the Iliad.", - id=f"file-search-tool-{uuid.uuid4()}", - task_queue=worker.task_queue, - execution_timeout=timedelta(seconds=30), - ) - result = await workflow_handle.result() - if use_local_model: - assert result == "Patroclus" - - -def image_generation_mock_model(): - return TestModel.returning_responses( - [ - ModelResponse( - output=[ - ImageGenerationCall( - type="image_generation_call", - id="id", - status="completed", - ), - ResponseBuilders.response_output_message("Patroclus"), - ], - usage=Usage(), - response_id=None, - ), - ] - ) - - -@workflow.defn -class ImageGenerationWorkflow: - @workflow.run - async def run(self, question: str) -> str: - agent = Agent[str]( - name="Image Generation Workflow", - instructions="You are a helpful agent.", - tools=[ - ImageGenerationTool( - tool_config={"type": "image_generation", "quality": "low"}, - ) - ], - ) - result = await Runner.run(starting_agent=agent, input=question) - - # An image generation was performed - assert any( - isinstance(item, ToolCallItem) - and isinstance(item.raw_item, ImageGenerationCall) - for item in result.new_items - ) - return result.final_output - - -# Can't currently validate against real server, we aren't verified for image generation -@pytest.mark.parametrize("use_local_model", [True]) -async def test_image_generation_tool(client: Client, use_local_model: bool): - if not use_local_model and not os.environ.get("OPENAI_API_KEY"): - pytest.skip("No openai API key") - - model = image_generation_mock_model() if use_local_model else None - async with AgentEnvironment( - model_params=ModelActivityParameters( - start_to_close_timeout=timedelta(seconds=30) - ), - model=model, - ) as env: - client = env.applied_on_client(client) - - async with new_worker( - client, - ImageGenerationWorkflow, - ) as worker: - workflow_handle = await client.start_workflow( - ImageGenerationWorkflow.run, - "Create an image of a frog eating a pizza, comic book style.", - id=f"image-generation-tool-{uuid.uuid4()}", - task_queue=worker.task_queue, - execution_timeout=timedelta(seconds=30), - ) - await workflow_handle.result() - - -def code_interpreter_mock_model(): - return TestModel.returning_responses( - [ - ModelResponse( - output=[ - ResponseCodeInterpreterToolCall( - container_id="", - code="some code", - type="code_interpreter_call", - id="id", - status="completed", - ), - ResponseBuilders.response_output_message("Over 9000"), - ], - usage=Usage(), - response_id=None, - ), - ] - ) - - -@workflow.defn -class CodeInterpreterWorkflow: - @workflow.run - async def run(self, question: str) -> str: - agent = Agent[str]( - name="Code Interpreter Workflow", - instructions="You are a helpful agent.", - tools=[ - CodeInterpreterTool( - tool_config={ - "type": "code_interpreter", - "container": {"type": "auto"}, - }, - ) - ], - ) - result = await Runner.run(starting_agent=agent, input=question) - - assert any( - isinstance(item, ToolCallItem) - and isinstance(item.raw_item, ResponseCodeInterpreterToolCall) - for item in result.new_items - ) - return result.final_output - - -async def test_code_interpreter_tool(client: Client): - async with AgentEnvironment( - model=code_interpreter_mock_model(), - model_params=ModelActivityParameters( - start_to_close_timeout=timedelta(seconds=60), - ), - ) as env: - client = env.applied_on_client(client) - - async with new_worker( - client, - CodeInterpreterWorkflow, - ) as worker: - workflow_handle = await client.start_workflow( - CodeInterpreterWorkflow.run, - "What is the square root of273 * 312821 plus 1782?", - id=f"code-interpreter-tool-{uuid.uuid4()}", - task_queue=worker.task_queue, - execution_timeout=timedelta(seconds=60), - ) - result = await workflow_handle.result() - assert result == "Over 9000" - - -def hosted_mcp_mock_model(): - return TestModel.returning_responses( - [ - ModelResponse( - output=[ - McpApprovalRequest( - arguments="", - name="", - server_label="gitmcp", - type="mcp_approval_request", - id="id", - ) - ], - usage=Usage(), - response_id=None, - ), - ModelResponse( - output=[ - McpCall( - arguments="", - name="", - server_label="", - type="mcp_call", - id="id", - output="Mcp output", - ), - ResponseBuilders.response_output_message("Some language"), - ], - usage=Usage(), - response_id=None, - ), - ] - ) - - -@workflow.defn -class HostedMCPWorkflow: - @workflow.run - async def run(self, question: str) -> str: - requested_approval = False - - def approve(_: MCPToolApprovalRequest) -> MCPToolApprovalFunctionResult: - nonlocal requested_approval - requested_approval = True - return MCPToolApprovalFunctionResult(approve=True) - - agent = Agent[str]( - name="Hosted MCP Workflow", - instructions="You are a helpful agent.", - tools=[ - HostedMCPTool( - tool_config={ - "type": "mcp", - "server_label": "gitmcp", - "server_url": "https://gitmcp.io/openai/codex", - "require_approval": "always", - }, - on_approval_request=approve, - ) - ], - ) - result = await Runner.run(starting_agent=agent, input=question) - assert requested_approval - assert any( - isinstance(item, ToolCallItem) and isinstance(item.raw_item, McpCall) - for item in result.new_items - ) - return result.final_output - - -async def test_hosted_mcp_tool(client: Client): - async with AgentEnvironment( - model_params=ModelActivityParameters( - start_to_close_timeout=timedelta(seconds=120) - ), - model=hosted_mcp_mock_model(), - ) as env: - client = env.applied_on_client(client) - - async with new_worker( - client, - HostedMCPWorkflow, - ) as worker: - workflow_handle = await client.start_workflow( - HostedMCPWorkflow.run, - "Which language is this repo written in?", - id=f"hosted-mcp-tool-{uuid.uuid4()}", - task_queue=worker.task_queue, - execution_timeout=timedelta(seconds=120), - ) - result = await workflow_handle.result() - assert result == "Some language" - - -def custom_tool_mock_model(): - return TestModel.returning_responses( - [ - ModelResponse( - output=[ - ResponseCustomToolCall( - call_id="c1", - input="ping", - name="echo", - type="custom_tool_call", - ) - ], - usage=Usage(), - response_id=None, - ), - ResponseBuilders.output_message("done"), - ] - ) - - -@workflow.defn -class CustomToolWorkflow: - @workflow.run - async def run(self) -> str: - captured: list[str] = [] - - async def echo(ctx: ToolContext[Any], input: str) -> str: # type: ignore[reportUnusedParameter] - captured.append(input) - return input - - agent = Agent[str]( - name="custom-tool-agent", - instructions="Use the echo tool.", - tools=[ - CustomTool( - name="echo", - description="Echo the input string back.", - on_invoke_tool=echo, - ) - ], - ) - result = await Runner.run(starting_agent=agent, input="say something") - return f"{result.final_output}:{captured[0]}" - - -async def test_custom_tool_workflow(client: Client): - async with AgentEnvironment(model=custom_tool_mock_model()) as env: - client = env.applied_on_client(client) - - async with new_worker(client, CustomToolWorkflow) as worker: - workflow_handle = await client.start_workflow( - CustomToolWorkflow.run, - id=f"custom-tool-workflow-{uuid.uuid4()}", - task_queue=worker.task_queue, - execution_timeout=timedelta(seconds=30), - ) - result = await workflow_handle.result() - assert result == "done:ping" - - -class AssertDifferentModelProvider(ModelProvider): - model_names: set[str | None] - - def __init__(self, model: Model): - self._model = model - self.model_names = set() - - def get_model(self, model_name: str | None) -> Model: - self.model_names.add(model_name) - return self._model - - -MULTIPLE_MODELS_FINAL_RESPONSE = "I'm here to help! Was there a specific task you needed assistance with regarding the storeroom?" - - -def multiple_models_mock_model(): - return TestModel.returning_responses( - [ - ResponseBuilders.tool_call("{}", "transfer_to_underling"), - ResponseBuilders.output_message(MULTIPLE_MODELS_FINAL_RESPONSE), - ] - ) - - -@workflow.defn -class MultipleModelWorkflow: - @workflow.run - async def run(self, use_run_config: bool): - underling = Agent[None]( - name="Underling", - instructions="You do all the work you are told.", - ) - - starting_agent = Agent[None]( - name="Lazy Assistant", - model="gpt-4o-mini", - instructions="You delegate all your work to another agent.", - handoffs=[underling], - ) - result = await Runner.run( - starting_agent=starting_agent, - input="Have you cleaned the store room yet?", - run_config=RunConfig(model="gpt-4o") if use_run_config else None, - ) - return result.final_output - - -async def test_multiple_models(client: Client): - provider = AssertDifferentModelProvider(multiple_models_mock_model()) - async with AgentEnvironment( - model_params=ModelActivityParameters( - start_to_close_timeout=timedelta(seconds=120) - ), - model_provider=provider, - ) as env: - client = env.applied_on_client(client) - - async with new_worker( - client, - MultipleModelWorkflow, - ) as worker: - workflow_handle = await client.start_workflow( - MultipleModelWorkflow.run, - False, - id=f"multiple-model-{uuid.uuid4()}", - task_queue=worker.task_queue, - ) - await workflow_handle.result() - assert provider.model_names == {None, "gpt-4o-mini"} - - -async def test_run_config_models(client: Client): - provider = AssertDifferentModelProvider(multiple_models_mock_model()) - async with AgentEnvironment( - model_params=ModelActivityParameters( - start_to_close_timeout=timedelta(seconds=120) - ), - model_provider=provider, - ) as env: - client = env.applied_on_client(client) - - async with new_worker( - client, - MultipleModelWorkflow, - ) as worker: - workflow_handle = await client.start_workflow( - MultipleModelWorkflow.run, - True, - id=f"run-config-model-{uuid.uuid4()}", - task_queue=worker.task_queue, - ) - await workflow_handle.result() - - # Only the model from the runconfig override is used - assert provider.model_names == {"gpt-4o"} - - -@workflow.defn -class DictRunConfigWorkflow: - """Same agents as MultipleModelWorkflow, but passes run_config as a plain - dict, which openai-agents >= 0.19 accepts at its public runner boundaries.""" - - @workflow.run - async def run(self) -> str: - underling = Agent[None]( - name="Underling", - instructions="You do all the work you are told.", - ) - - starting_agent = Agent[None]( - name="Lazy Assistant", - model="gpt-4o-mini", - instructions="You delegate all your work to another agent.", - handoffs=[underling], - ) - # Typed as Any so this also type-checks against openai-agents - # versions whose run_config annotation does not include dict. - dict_run_config: Any = {"model": "gpt-4o"} - result = await Runner.run( - starting_agent=starting_agent, - input="Have you cleaned the store room yet?", - run_config=dict_run_config, - ) - return result.final_output - - -async def test_dict_run_config_models(client: Client): - # A dict run_config must behave identically to the equivalent - # RunConfig(model="gpt-4o") in test_run_config_models above. - provider = AssertDifferentModelProvider(multiple_models_mock_model()) - async with AgentEnvironment( - model_params=ModelActivityParameters( - start_to_close_timeout=timedelta(seconds=120) - ), - model_provider=provider, - ) as env: - client = env.applied_on_client(client) - - async with new_worker( - client, - DictRunConfigWorkflow, - ) as worker: - workflow_handle = await client.start_workflow( - DictRunConfigWorkflow.run, - id=f"dict-run-config-model-{uuid.uuid4()}", - task_queue=worker.task_queue, - ) - result = await workflow_handle.result() - - # Only the model from the runconfig override is used - assert provider.model_names == {"gpt-4o"} - assert result == MULTIPLE_MODELS_FINAL_RESPONSE - - -def test_coerce_run_config_validation(): - # Mirrors upstream agents' normalization: equivalent RunConfig out of a - # dict, and the same TypeErrors for invalid input. - coerced = _coerce_run_config({"model": "gpt-4o", "workflow_name": "wf"}) - assert isinstance(coerced, RunConfig) - assert coerced.model == "gpt-4o" - assert coerced.workflow_name == "wf" - - run_config = RunConfig(model="gpt-4o") - assert _coerce_run_config(run_config) is run_config - - with pytest.raises(TypeError, match="Unknown run_config settings: bogus_setting"): - _coerce_run_config({"model": "gpt-4o", "bogus_setting": True}) - - with pytest.raises( - TypeError, match="run_config must be a RunConfig instance or a dict, got int" - ): - _coerce_run_config(42) - - -async def test_summary_provider(client: Client): - class SummaryProvider(ModelSummaryProvider): - def provide( - self, - agent: Agent[Any] | None, - instructions: str | None, - input: str | list[TResponseInputItem], - ) -> str: - return "My summary" - - async with AgentEnvironment( - model_params=ModelActivityParameters( - start_to_close_timeout=timedelta(seconds=120), - summary_override=SummaryProvider(), - ), - model=hello_mock_model(), - ) as env: - client = env.applied_on_client(client) - - async with new_worker( - client, - HelloWorldAgent, - ) as worker: - workflow_handle = await client.start_workflow( - HelloWorldAgent.run, - "Prompt", - id=f"summary-provider-model-{uuid.uuid4()}", - task_queue=worker.task_queue, - ) - await workflow_handle.result() - async for e in workflow_handle.fetch_history_events(): - if e.HasField("activity_task_scheduled_event_attributes"): - assert e.user_metadata.summary.data == b'"My summary"' - - -class OutputType(pydantic.BaseModel): - answer: str - model_config = ConfigDict(extra="forbid") # Forbid additional properties - - -@workflow.defn -class OutputTypeWorkflow: - @workflow.run - async def run(self) -> OutputType: - agent: Agent = Agent( - name="Assistant", - instructions="You are a helpful assistant, adhere to the json schema output", - output_type=OutputType, - ) - result = await Runner.run( - starting_agent=agent, - input="Hello!", - ) - return result.final_output - - -def output_type_mock_model(): - return TestModel.returning_responses( - [ - ResponseBuilders.output_message( - '{"answer": "My answer"}', - ), - ] - ) - - -async def test_output_type(client: Client): - async with AgentEnvironment( - model=output_type_mock_model(), - model_params=ModelActivityParameters( - start_to_close_timeout=timedelta(seconds=120), - ), - ) as env: - client = env.applied_on_client(client) - - async with new_worker( - client, - OutputTypeWorkflow, - ) as worker: - workflow_handle = await client.start_workflow( - OutputTypeWorkflow.run, - id=f"output-type-{uuid.uuid4()}", - task_queue=worker.task_queue, - ) - result = await workflow_handle.result() - assert isinstance(result, OutputType) - assert result.answer == "My answer" - - -@workflow.defn -class McpServerWorkflow: - @workflow.run - async def run(self, caching: bool, factory_argument: Any | None) -> str: - from agents.mcp import MCPServer # type: ignore[reportUnusedImport] - - server: MCPServer = openai_agents.workflow.stateless_mcp_server( - "HelloServer", cache_tools_list=caching, factory_argument=factory_argument - ) - agent = Agent[str]( - name="MCP ServerWorkflow", - instructions="Use the tools to assist the customer.", - mcp_servers=[server], - ) - result = await Runner.run( - starting_agent=agent, input="Say hello to Tom and Tim." - ) - return result.final_output - - -@workflow.defn -class McpServerStatefulWorkflow: - @workflow.run - async def run(self, timeout: timedelta, factory_argument: Any | None) -> str: - async with openai_agents.workflow.stateful_mcp_server( - "HelloServer", - config=ActivityConfig( - schedule_to_start_timeout=timeout, - start_to_close_timeout=timedelta(seconds=30), - ), - factory_argument=factory_argument, - ) as server: - agent = Agent[str]( - name="MCP ServerWorkflow", - instructions="Use the tools to assist the customer.", - mcp_servers=[server], - ) - result = await Runner.run( - starting_agent=agent, input="Say hello to Tom and Tim." - ) - return result.final_output - - -def tracking_mcp_mock_model(): - return TestModel.returning_responses( - [ - ResponseBuilders.tool_call( - arguments='{"name":"Tom"}', - name="Say-Hello", - ), - ResponseBuilders.tool_call( - arguments='{"name":"Tim"}', - name="Say-Hello", - ), - ResponseBuilders.output_message("Hi Tom and Tim!"), - ] - ) - - -def get_tracking_server(name: str): - from agents.mcp import MCPServer # type: ignore - from mcp import GetPromptResult, ListPromptsResult # type: ignore - from mcp import Tool as MCPTool # type: ignore - from mcp.types import CallToolResult, TextContent # type: ignore - - class TrackingMCPServer(MCPServer): - calls: list[str] - - def __init__(self, name: str): - self._name = name - self.calls = [] - super().__init__() - - async def connect(self): - self.calls.append("connect") - - @property - def name(self) -> str: - return self._name - - async def cleanup(self): - self.calls.append("cleanup") - - async def list_tools( - self, - run_context: RunContextWrapper[Any] | None = None, - agent: AgentBase | None = None, - ) -> list[MCPTool]: - self.calls.append("list_tools") - return [ - MCPTool( - name="Say-Hello", - inputSchema={ - "type": "object", - "properties": { - "name": {"type": "string"}, - }, - "required": ["name"], - "$schema": "http://json-schema.org/draft-07/schema#", - }, - ) - ] - - async def call_tool( - self, - tool_name: str, - arguments: dict[str, Any] | None, - meta: dict[str, Any] | None = None, - ) -> CallToolResult: - self.calls.append("call_tool") - name = (arguments or {}).get("name") or "John Doe" - return CallToolResult( - content=[TextContent(type="text", text=f"Hello {name}")] - ) - - async def list_prompts(self) -> ListPromptsResult: - raise NotImplementedError() - - async def get_prompt( - self, name: str, arguments: dict[str, Any] | None = None - ) -> GetPromptResult: - raise NotImplementedError() - - return TrackingMCPServer(name) - - -@pytest.mark.parametrize("use_local_model", [True, False]) -@pytest.mark.parametrize("stateful", [True, False]) -@pytest.mark.parametrize("caching", [True, False]) -async def test_mcp_server( - client: Client, use_local_model: bool, stateful: bool, caching: bool -): - if not use_local_model and not os.environ.get("OPENAI_API_KEY"): - pytest.skip("No openai API key") - - if stateful and caching: - pytest.skip("Caching is only supported for stateless MCP servers") - - from agents.mcp import MCPServer # type: ignore - - from temporalio.contrib.openai_agents import ( - StatefulMCPServerProvider, - StatelessMCPServerProvider, - ) - - tracking_server = get_tracking_server(name="HelloServer") - server: StatefulMCPServerProvider | StatelessMCPServerProvider = ( - StatefulMCPServerProvider("HelloServer", lambda _: tracking_server) - if stateful - else StatelessMCPServerProvider("HelloServer", lambda _: tracking_server) - ) - - model = tracking_mcp_mock_model() if use_local_model else None - async with AgentEnvironment( - model_params=ModelActivityParameters( - start_to_close_timeout=timedelta(seconds=120) - ), - model=model, - mcp_server_providers=[server], - ) as env: - client = env.applied_on_client(client) - - async with new_worker( - client, McpServerStatefulWorkflow, McpServerWorkflow - ) as worker: - if stateful: - result = await client.execute_workflow( - McpServerStatefulWorkflow.run, - args=[timedelta(seconds=30), None], - id=f"mcp-server-{uuid.uuid4()}", - task_queue=worker.task_queue, - execution_timeout=timedelta(seconds=30), - ) - else: - result = await client.execute_workflow( - McpServerWorkflow.run, - args=[caching, None], - id=f"mcp-server-{uuid.uuid4()}", - task_queue=worker.task_queue, - execution_timeout=timedelta(seconds=30), - ) - if use_local_model: - assert result == "Hi Tom and Tim!" - if use_local_model: - print(tracking_server.calls) - if stateful: - assert tracking_server.calls == [ - "connect", - "list_tools", - "call_tool", - "list_tools", - "call_tool", - "list_tools", - "cleanup", - ] - assert len(cast(StatefulMCPServerProvider, server)._servers) == 0 - else: - if caching: - assert tracking_server.calls == [ - "connect", - "list_tools", - "cleanup", - "connect", - "call_tool", - "cleanup", - "connect", - "call_tool", - "cleanup", - ] - else: - assert tracking_server.calls == [ - "connect", - "list_tools", - "cleanup", - "connect", - "call_tool", - "cleanup", - "connect", - "list_tools", - "cleanup", - "connect", - "call_tool", - "cleanup", - "connect", - "list_tools", - "cleanup", - ] - - -@pytest.mark.parametrize("stateful", [True, False]) -async def test_mcp_server_factory_argument(client: Client, stateful: bool): - def factory(args: Any | None) -> MCPServer: - print("Invoking factory: ", args) - if args is not None: - assert args is not None - assert cast(dict[str, str], args).get("user") == "blah" - - return get_tracking_server("HelloServer") - - server: StatefulMCPServerProvider | StatelessMCPServerProvider = ( - StatefulMCPServerProvider("HelloServer", factory) - if stateful - else StatelessMCPServerProvider("HelloServer", factory) - ) - - async with AgentEnvironment( - model_params=ModelActivityParameters( - start_to_close_timeout=timedelta(seconds=120) - ), - model=tracking_mcp_mock_model(), - mcp_server_providers=[server], - ) as env: - client = env.applied_on_client(client) - - headers = {"user": "blah"} - async with new_worker( - client, McpServerStatefulWorkflow, McpServerWorkflow - ) as worker: - if stateful: - await client.execute_workflow( - McpServerStatefulWorkflow.run, - args=[timedelta(seconds=30), headers], - id=f"mcp-server-{uuid.uuid4()}", - task_queue=worker.task_queue, - execution_timeout=timedelta(seconds=30), - ) - else: - await client.execute_workflow( - McpServerWorkflow.run, - args=[False, headers], - id=f"mcp-server-{uuid.uuid4()}", - task_queue=worker.task_queue, - execution_timeout=timedelta(seconds=30), - ) - - -async def test_stateful_mcp_server_no_worker(client: Client): - server = StatefulMCPServerProvider( - "Filesystem-Server", - lambda _: MCPServerStdio( - name="Filesystem-Server", - params={ - "command": "npx", - "args": [ - "-y", - "@modelcontextprotocol/server-filesystem", - os.path.dirname(os.path.abspath(__file__)), - ], - }, - ), - ) - - # Override the connect activity to not actually start a worker - @activity.defn(name="Filesystem-Server-stateful-connect") - async def connect() -> None: - await asyncio.sleep(30) - - def override_get_activities() -> Sequence[Callable]: - return (connect,) - - server.get_activities = override_get_activities # type:ignore - - async with AgentEnvironment( - model_params=ModelActivityParameters( - start_to_close_timeout=timedelta(seconds=120) - ), - model=tracking_mcp_mock_model(), - mcp_server_providers=[server], - ) as env: - client = env.applied_on_client(client) - - async with new_worker( - client, - McpServerStatefulWorkflow, - ) as worker: - workflow_handle = await client.start_workflow( - McpServerStatefulWorkflow.run, - args=[timedelta(seconds=1), None], - id=f"mcp-server-{uuid.uuid4()}", - task_queue=worker.task_queue, - execution_timeout=timedelta(seconds=30), - ) - with pytest.raises(WorkflowFailureError) as err: - await workflow_handle.result() - assert isinstance(err.value.cause, ApplicationError) - assert ( - err.value.cause.message - == "MCP Stateful Server Worker failed to schedule activity." - ) - - -async def test_model_conversion_loops(): - agent = init_agents() - converted = _convert_agent(ModelActivityParameters(), agent, None) - seat_booking_handoff = converted.handoffs[1] - assert isinstance(seat_booking_handoff, Handoff) - context: RunContextWrapper[AirlineAgentContext] = RunContextWrapper( - context=AirlineAgentContext() # type: ignore - ) - seat_booking_agent = await seat_booking_handoff.on_invoke_handoff(context, "") - triage_agent = seat_booking_agent.handoffs[0] - assert isinstance(triage_agent, Agent) - assert isinstance(triage_agent.model, _TemporalModelStub) - seat_booking_agent = await seat_booking_handoff.on_invoke_handoff(context, "") - triage_agent = seat_booking_agent.handoffs[0] - assert isinstance(triage_agent, Agent) - assert isinstance(triage_agent.model, _TemporalModelStub) - seat_booking_agent = await seat_booking_handoff.on_invoke_handoff(context, "") - triage_agent = seat_booking_agent.handoffs[0] - assert isinstance(triage_agent, Agent) - assert isinstance(triage_agent.model, _TemporalModelStub) - - -def test_sandbox_apply_patch_tool_round_trips_through_activity_input(): - class FakeSandboxSession: - pass - - tool = SandboxApplyPatchTool(session=FakeSandboxSession()) # type: ignore[arg-type] - - stub = _TemporalModelStub( - model_name="gpt-5", - model_params=ModelActivityParameters(), - agent=None, - ) - - activity_input, _summary = stub._build_activity_input( - system_instructions=None, - input="hi", - model_settings=ModelSettings(), - tools=[tool], - output_schema=None, - handoffs=[], - tracing=ModelTracing.DISABLED, - previous_response_id=None, - conversation_id=None, - prompt=None, - ) - - tool_inputs = activity_input.get("tools") or [] - assert len(tool_inputs) == 1 - rebuilt = _build_tool(tool_inputs[0], _WorkerEnvRefResolver(())) - assert isinstance(rebuilt, CustomTool) - assert rebuilt.name == tool.name - assert rebuilt.description == tool.description - assert rebuilt.format == tool.format - assert rebuilt.tool_config == tool.tool_config - - -def test_custom_tool_with_defer_loading_round_trips_through_activity_input(): - async def stub(_ctx: Any, _payload: str) -> str: - return "" - - tool = CustomTool( - name="deferred_tool", - description="A custom tool with defer_loading enabled", - on_invoke_tool=stub, - defer_loading=True, - ) - - stub_model = _TemporalModelStub( - model_name="gpt-5", - model_params=ModelActivityParameters(), - agent=None, - ) - - activity_input, _summary = stub_model._build_activity_input( - system_instructions=None, - input="hi", - model_settings=ModelSettings(), - tools=[tool], - output_schema=None, - handoffs=[], - tracing=ModelTracing.DISABLED, - previous_response_id=None, - conversation_id=None, - prompt=None, - ) - - tool_inputs = activity_input.get("tools") or [] - assert len(tool_inputs) == 1 - rebuilt = _build_tool(tool_inputs[0], _WorkerEnvRefResolver(())) - assert isinstance(rebuilt, CustomTool) - assert rebuilt.tool_config == tool.tool_config - assert rebuilt.defer_loading is True - - -async def test_local_hello_world_agent(client: Client): - async with AgentEnvironment( - model=hello_mock_model(), - model_params=ModelActivityParameters( - start_to_close_timeout=timedelta(seconds=30), - use_local_activity=True, - ), - ) as env: - client = env.applied_on_client(client) - - async with new_worker(client, HelloWorldAgent) as worker: - handle = await client.start_workflow( - HelloWorldAgent.run, - "Tell me about recursion in programming.", - id=f"hello-workflow-{uuid.uuid4()}", - task_queue=worker.task_queue, - ) - result = await handle.result() - assert result == "test" - local_activity_found = False - async for e in handle.fetch_history_events(): - if e.HasField("marker_recorded_event_attributes"): - local_activity_found = True - assert local_activity_found - - -async def test_split_workers(client: Client): - new_config = client.config() - - workflow_plugin = openai_agents.OpenAIAgentsPlugin( - model_params=ModelActivityParameters( - start_to_close_timeout=timedelta(seconds=30) - ), - model_provider=TestModelProvider(hello_mock_model()), - register_activities=False, - ) - new_config["plugins"] = [workflow_plugin] - workflow_client = Client(**new_config) - - # Workflow worker - async with new_worker( - workflow_client, HelloWorldAgent, no_remote_activities=True - ) as worker: - activity_plugin = openai_agents.OpenAIAgentsPlugin( - model_params=ModelActivityParameters( - start_to_close_timeout=timedelta(seconds=30) - ), - model_provider=TestModelProvider(hello_mock_model()), - ) - new_config["plugins"] = [activity_plugin] - activity_client = Client(**new_config) - # Activity Worker - async with new_worker(activity_client, task_queue=worker.task_queue): - result = await activity_client.execute_workflow( - HelloWorldAgent.run, - "Tell me about recursion in programming.", - id=f"hello-workflow-{uuid.uuid4()}", - task_queue=worker.task_queue, - execution_timeout=timedelta(seconds=120), - ) - assert result == "test" - - -def multiple_handoffs_mock_model(): - return TestModel.returning_responses( - [ - ResponseBuilders.tool_call("{}", "transfer_to_planner"), - ResponseBuilders.output_message( - "I'll analyze the requirements and create a plan." - ), - ] - ) - - -@workflow.defn -class MultipleHandoffsWorkflow: - @workflow.run - async def run(self, task: str) -> str: - planner = Agent[None]( - name="Planner", - instructions="You analyze requirements and create detailed plans.", - handoff_description="An agent that creates detailed plans and strategies", - ) - - writer = Agent[None]( - name="Writer", - instructions="You write documents and reports based on provided information.", - handoff_description="An agent that writes professional documents and reports", - ) - - specialists = [planner, writer] - handoffs_list: list[Agent[Any] | Handoff[None, Any]] = [ - handoff(agent=a) for a in specialists - ] - - triage = Agent[None]( - name="Triage", - instructions="Hand off to Planner when requested.", - handoffs=handoffs_list, - ) - - result = await Runner.run(starting_agent=triage, input=task) - return result.final_output - - -async def test_multiple_handoffs_workflow(client: Client): - model = multiple_handoffs_mock_model() - async with AgentEnvironment( - model=model, - model_params=ModelActivityParameters( - start_to_close_timeout=timedelta(seconds=30), - ), - ) as env: - client = env.applied_on_client(client) - - async with new_worker( - client, - MultipleHandoffsWorkflow, - ) as worker: - workflow_handle = await client.start_workflow( - MultipleHandoffsWorkflow.run, - "Create a project plan for building a web application", - id=f"multiple-handoffs-workflow-{uuid.uuid4()}", - task_queue=worker.task_queue, - execution_timeout=timedelta(seconds=30), - ) - result = await workflow_handle.result() - - assert result == "I'll analyze the requirements and create a plan." - - # Verify the correct handoff occurred - events = [] - async for e in workflow_handle.fetch_history_events(): - if e.HasField("activity_task_completed_event_attributes"): - events.append(e) - - # Should have 2 activity completions: - # 1. Triage agent makes handoff call to planner - # 2. Planner agent responds - assert len(events) == 2 - - # Verify handoff to planner was requested - first_event_data = ( - events[0] - .activity_task_completed_event_attributes.result.payloads[0] - .data.decode() - ) - assert "transfer_to_planner" in first_event_data - - # Verify that the planner agent was actually invoked (this would fail before the fix) - planner_response_data = ( - events[1] - .activity_task_completed_event_attributes.result.payloads[0] - .data.decode() - ) - assert ( - "I'll analyze the requirements and create a plan." - in planner_response_data - ) diff --git a/tests/contrib/openai_agents/test_openai_replay.py b/tests/contrib/openai_agents/test_openai_replay.py deleted file mode 100644 index 7c9a586b2..000000000 --- a/tests/contrib/openai_agents/test_openai_replay.py +++ /dev/null @@ -1,58 +0,0 @@ -import warnings -from pathlib import Path - -import pytest - -from temporalio.client import WorkflowHistory -from temporalio.contrib.openai_agents import OpenAIAgentsPlugin -from temporalio.worker import Replayer -from tests.contrib.openai_agents.test_openai import ( - AgentsAsToolsWorkflow, - CustomerServiceWorkflow, - HelloWorldAgent, - InputGuardrailWorkflow, - OutputGuardrailWorkflow, - ResearchWorkflow, - ToolsWorkflow, -) - - -@pytest.mark.parametrize( - "file_name", - [ - "agents-as-tools-workflow-history.json", - "customer-service-workflow-history.json", - "hello-workflow-history.json", - "input-guardrail-workflow-history.json", - "output-guardrail-workflow-history.json", - "research-workflow-history.json", - "tools-workflow-history.json", - ], -) -async def test_replay(file_name: str) -> None: - with (Path(__file__).with_name("histories") / file_name).open("r") as f: - history_json = f.read() - - with warnings.catch_warnings(record=True) as recorder: - warnings.filterwarnings( - "always", message=r"Module .* was imported after initial workflow load" - ) - await Replayer( - workflows=[ - ResearchWorkflow, - ToolsWorkflow, - CustomerServiceWorkflow, - AgentsAsToolsWorkflow, - HelloWorldAgent, - InputGuardrailWorkflow, - OutputGuardrailWorkflow, - ], - plugins=[OpenAIAgentsPlugin()], - ).replay_workflow(WorkflowHistory.from_json("fake", history_json)) - - # Sandbox imports during an activation count toward the deadlock timeout - assert not [ - str(w.message) - for w in recorder - if "was imported after initial workflow load" in str(w.message) - ] diff --git a/tests/contrib/openai_agents/test_openai_sandbox.py b/tests/contrib/openai_agents/test_openai_sandbox.py deleted file mode 100644 index 4b9bf2f2b..000000000 --- a/tests/contrib/openai_agents/test_openai_sandbox.py +++ /dev/null @@ -1,1575 +0,0 @@ -"""Tests for sandbox validation in TemporalOpenAIRunner.""" - -import io -import uuid -from collections.abc import Collection -from datetime import timedelta -from pathlib import Path -from typing import Any, Literal - -import pytest -from agents import Agent, FunctionTool, RunConfig, Runner, Tool -from agents.sandbox import Capability, Manifest, SandboxAgent, SandboxRunConfig -from agents.sandbox.errors import ( - ExecTransportError, - SandboxError, - WorkspaceArchiveReadError, -) -from agents.sandbox.manifest import Environment -from agents.sandbox.session.base_sandbox_session import BaseSandboxSession -from agents.sandbox.session.pty_types import PtyExecUpdate -from agents.sandbox.session.sandbox_client import ( - BaseSandboxClient, - BaseSandboxClientOptions, -) -from agents.sandbox.session.sandbox_session import SandboxSession -from agents.sandbox.session.sandbox_session_state import SandboxSessionState -from agents.sandbox.snapshot import NoopSnapshot -from agents.sandbox.types import ExecResult -from agents.sandbox.workspace_paths import SandboxPathGrant -from pydantic import BaseModel, TypeAdapter -from pydantic_core import to_json - -from temporalio import workflow -from temporalio.client import Client, WorkflowFailureError -from temporalio.contrib.openai_agents import ( - AgentsWorkflowError, - ModelActivityParameters, - OpenAIAgentsPlugin, - OpenAIPayloadConverter, - SandboxClientProvider, - TemporalWorkerEnvValue, -) -from temporalio.contrib.openai_agents._openai_runner import _has_sandbox_agent -from temporalio.contrib.openai_agents._temporal_worker_env_ref import ( - AllowAllWorkerEnvVars, -) -from temporalio.contrib.openai_agents.sandbox._temporal_activity_models import ( - CreateSessionArgs, - ExecArgs, - HydrateWorkspaceArgs, - PersistWorkspaceArgs, - PersistWorkspaceResult, - PtyExecStartArgs, - PtyExecUpdateResult, - PtyWriteStdinArgs, - ReadArgs, - ReadResult, - ResumeSessionArgs, - RunningArgs, - StartArgs, - StopArgs, - WriteArgs, -) -from temporalio.contrib.openai_agents.sandbox._temporal_activity_models import ( - ExecResult as ExecResultModel, -) -from temporalio.contrib.openai_agents.sandbox._temporal_sandbox_client import ( - TemporalSandboxClient, -) -from temporalio.contrib.openai_agents.sandbox._temporal_worker_env_value import ( - _resolvable_worker_env_vars, -) -from temporalio.contrib.openai_agents.testing import ( - AgentEnvironment, - ResponseBuilders, - TestModel, - TestModelProvider, -) -from temporalio.contrib.openai_agents.workflow import temporal_sandbox_client -from temporalio.exceptions import ActivityError, ApplicationError -from temporalio.workflow import ActivityConfig -from tests.helpers import new_worker - -# ── _has_sandbox_agent unit tests ── - - -def test_has_sandbox_agent_regular_agent(): - assert _has_sandbox_agent(Agent[None](name="regular")) is False - - -def test_has_sandbox_agent_sandbox_starting(): - assert _has_sandbox_agent(SandboxAgent[None](name="sandbox")) is True - - -def test_has_sandbox_agent_sandbox_direct_handoff(): - sandbox = SandboxAgent[None](name="sandbox") - regular = Agent[None](name="regular", handoffs=[sandbox]) - assert _has_sandbox_agent(regular) is True - - -def test_has_sandbox_agent_sandbox_deep_handoff(): - sandbox = SandboxAgent[None](name="sandbox") - middle = Agent[None](name="middle", handoffs=[sandbox]) - top = Agent[None](name="top", handoffs=[middle]) - assert _has_sandbox_agent(top) is True - - -def test_has_sandbox_agent_no_sandbox_in_chain(): - c = Agent[None](name="c") - b = Agent[None](name="b", handoffs=[c]) - a = Agent[None](name="a", handoffs=[b]) - assert _has_sandbox_agent(a) is False - - -def test_has_sandbox_agent_circular_no_sandbox(): - a: Agent[Any] = Agent[None](name="a") - b: Agent[Any] = Agent[None](name="b", handoffs=[a]) - a.handoffs = [b] - assert _has_sandbox_agent(a) is False - - -def test_has_sandbox_agent_circular_with_sandbox(): - sandbox = SandboxAgent[None](name="sandbox") - a: Agent[Any] = Agent[None](name="a", handoffs=[sandbox]) - b: Agent[Any] = Agent[None](name="b", handoffs=[a]) - a.handoffs = [b, sandbox] - assert _has_sandbox_agent(b) is True - - -# ── temporal_sandbox_client helper tests ── - - -def test_temporal_sandbox_client_returns_temporal_client(): - client = temporal_sandbox_client("my-backend") - assert isinstance(client, TemporalSandboxClient) - assert client._name == "my-backend" - assert client.backend_id == "my-backend" - - -def test_temporal_sandbox_client_with_config(): - config = ActivityConfig(start_to_close_timeout=timedelta(minutes=10)) - client = temporal_sandbox_client("my-backend", config=config) - assert isinstance(client, TemporalSandboxClient) - assert client._config == config - - -# ── Workflow validation tests ── - - -def _mock_model(): - return TestModel.returning_responses([ResponseBuilders.output_message("test")]) - - -@workflow.defn -class SandboxValidationWorkflow: - """Single workflow that validates all sandbox configuration error cases.""" - - @workflow.run - async def run(self) -> str: - # Case 1: SandboxAgent without run_config.sandbox - try: - agent = SandboxAgent[None](name="sandbox") - await Runner.run(starting_agent=agent, input="hello") - return "FAIL: no-config should have raised" - except ValueError as e: - assert "run_config.sandbox is not configured" in str(e) - - # Case 2: SandboxAgent reachable via handoff without run_config.sandbox - try: - sandbox = SandboxAgent[None](name="sandbox_target") - router = Agent[None](name="router", handoffs=[sandbox]) - await Runner.run(starting_agent=router, input="hello") - return "FAIL: handoff-no-config should have raised" - except ValueError as e: - assert "run_config.sandbox is not configured" in str(e) - - # Case 3: SandboxRunConfig with client=None - try: - agent = SandboxAgent[None](name="sandbox") - await Runner.run( - starting_agent=agent, - input="hello", - run_config=RunConfig( - sandbox=SandboxRunConfig(client=None), # type: ignore[arg-type] - ), - ) - return "FAIL: null-client should have raised" - except ValueError as e: - assert "run_config.sandbox.client must be set" in str(e) - - # Case 4: Non-TemporalSandboxClient in run_config.sandbox.client - try: - agent = SandboxAgent[None](name="sandbox") - await Runner.run( - starting_agent=agent, - input="hello", - run_config=RunConfig( - sandbox=SandboxRunConfig(client=object()), # type: ignore[arg-type] - ), - ) - return "FAIL: wrong-client should have raised" - except ValueError as e: - assert "temporal_sandbox_client(name)" in str(e) - - return "OK" - - -async def test_sandbox_validation_errors(client: Client): - """All sandbox configuration errors should be caught immediately in the workflow.""" - async with AgentEnvironment(model=_mock_model()) as env: - client = env.applied_on_client(client) - async with new_worker( - client, - SandboxValidationWorkflow, - workflow_failure_exception_types=[ValueError, AssertionError], - ) as worker: - result = await client.execute_workflow( - SandboxValidationWorkflow.run, - id=f"sandbox-validation-{uuid.uuid4()}", - task_queue=worker.task_queue, - execution_timeout=timedelta(seconds=10), - ) - assert result == "OK" - - -# ── Mock sandbox infrastructure for delegation tests ── - - -class TestSessionState(SandboxSessionState): - """Concrete ``SandboxSessionState`` subclass for tests that don't need a real backend.""" - - __test__ = False - type: Literal["test"] = "test" # type: ignore - - -class _MockSandboxSession(BaseSandboxSession): - """Minimal mock session that tracks calls and returns canned results.""" - - def __init__(self, manifest: Manifest | None = None) -> None: - self.state = TestSessionState( - manifest=manifest or Manifest(), - snapshot=NoopSnapshot(id=str(uuid.uuid4())), - ) - self.exec_calls: list[tuple] = [] - self.read_calls: list[Path] = [] - self.write_calls: list[tuple[Path, bytes]] = [] - self.running_calls: int = 0 - self.start_calls: int = 0 - self.stop_calls: int = 0 - self.shutdown_calls: int = 0 - self.persist_workspace_calls: int = 0 - self.hydrate_workspace_calls: int = 0 - - async def start(self) -> None: - self.start_calls += 1 - - async def stop(self) -> None: - self.stop_calls += 1 - - async def shutdown(self) -> None: - self.shutdown_calls += 1 - - async def running(self) -> bool: - self.running_calls += 1 - return True - - async def _exec_internal( - self, - *command: str | Path, - timeout: float | None = None, - ) -> ExecResult: - self.exec_calls.append((command, timeout)) - return ExecResult(stdout=b"ok\n", stderr=b"", exit_code=0) - - async def read(self, path: Path, *, user: Any = None) -> io.IOBase: # type: ignore[reportUnusedParameter] - self.read_calls.append(path) - return io.BytesIO(b"file-content") - - async def write(self, path: Path, data: io.IOBase, *, user: Any = None) -> None: # type: ignore[reportUnusedParameter] - self.write_calls.append((path, data.read())) - - async def persist_workspace(self) -> io.IOBase: - self.persist_workspace_calls += 1 - return io.BytesIO(b"workspace-archive") - - async def hydrate_workspace(self, data: io.IOBase) -> None: - self.hydrate_workspace_calls += 1 - - def supports_pty(self) -> bool: - return False - - -class _MockSandboxClient(BaseSandboxClient[BaseSandboxClientOptions | None]): - """Mock client that tracks create/resume/delete calls and delegates to a mock session.""" - - backend_id = "mock" - supports_default_options = True - - def __init__(self, session: _MockSandboxSession | None = None) -> None: - self.inner_session = session or _MockSandboxSession() - self.session = self._wrap_session(self.inner_session) - self.create_calls: int = 0 - self.resume_calls: int = 0 - self.delete_calls: int = 0 - self.resolved_envs: dict[str, str] | None = None - - async def create( - self, - *, - snapshot: Any = None, - manifest: Manifest | None = None, - options: BaseSandboxClientOptions | None = None, - ) -> SandboxSession: - self.create_calls += 1 - if manifest is not None: - self.inner_session.state.manifest = manifest - self.resolved_envs = await manifest.environment.resolve() - return self.session - - async def resume(self, state: SandboxSessionState) -> SandboxSession: - self.resume_calls += 1 - self.inner_session.state = state - return self.session - - async def delete(self, session: SandboxSession) -> SandboxSession: - self.delete_calls += 1 - return session - - def deserialize_session_state(self, payload: dict[str, Any]) -> SandboxSessionState: - return SandboxSessionState.model_validate(payload) - - -# ── SandboxClientProvider unit tests (delegation) ── - - -@pytest.fixture -def mock_client() -> _MockSandboxClient: - return _MockSandboxClient() - - -@pytest.fixture -def sandbox_activities(mock_client: _MockSandboxClient) -> SandboxClientProvider: - return SandboxClientProvider("mock", mock_client) - - -def _make_state(manifest: Manifest | None = None) -> TestSessionState: - return TestSessionState( - manifest=manifest or Manifest(), - snapshot=NoopSnapshot(id=str(uuid.uuid4())), - ) - - -def _activity_map( - sandbox_activities: SandboxClientProvider, - resolvable_worker_env_vars: Collection[str] = (), -) -> dict[str, Any]: - """Build a short-name → callable dict from all() for easy test dispatch.""" - return { - act.__temporal_activity_definition.name: act # type: ignore[attr-defined, union-attr] - for act in sandbox_activities._get_activities(resolvable_worker_env_vars) - } - - -async def test_activities_create_session_delegates( - sandbox_activities: SandboxClientProvider, - mock_client: _MockSandboxClient, -): - """create_session activity should delegate to the real client's create().""" - acts = _activity_map(sandbox_activities) - args = CreateSessionArgs( - snapshot_spec=None, - manifest=Manifest(), - client_options=None, - ) - result = await acts["mock-sandbox_client_create"](args) - assert mock_client.create_calls == 1 - assert result.state is not None - assert isinstance(result.supports_pty, bool) - - -async def test_create_session_activity_resolves_worker_env_value_but_returns_it_unresolved( - sandbox_activities: SandboxClientProvider, - mock_client: _MockSandboxClient, - monkeypatch: pytest.MonkeyPatch, -): - secret = "sk-activity-boundary-secret" - monkeypatch.setenv("WORKER_ACTIVITY_SECRET", secret) - - def payload_bytes(value: BaseModel) -> bytes: - payload = OpenAIPayloadConverter().to_payload(value) - assert payload is not None - return payload.data - - args = CreateSessionArgs( - snapshot_spec=None, - manifest=Manifest( - environment=Environment( - value={"API_KEY": TemporalWorkerEnvValue(name="WORKER_ACTIVITY_SECRET")} - ) - ), - client_options=None, - ) - assert secret.encode() not in payload_bytes(args) - - acts = _activity_map(sandbox_activities, ["WORKER_ACTIVITY_SECRET"]) - result = await acts["mock-sandbox_client_create"](args) - - assert mock_client.resolved_envs == {"API_KEY": secret} - returned = payload_bytes(result) - assert secret.encode() not in returned - assert b"temporal.worker_env_value" in returned - - -async def test_create_session_activity_refuses_an_unlisted_worker_env_value( - sandbox_activities: SandboxClientProvider, - monkeypatch: pytest.MonkeyPatch, -): - secret = "sk-unlisted-secret" - monkeypatch.setenv("WORKER_ACTIVITY_SECRET", secret) - - args = CreateSessionArgs( - snapshot_spec=None, - manifest=Manifest( - environment=Environment( - value={"API_KEY": TemporalWorkerEnvValue(name="WORKER_ACTIVITY_SECRET")} - ) - ), - client_options=None, - ) - - acts = _activity_map(sandbox_activities, ["SOMETHING_ELSE"]) - with pytest.raises(ApplicationError) as exc_info: - await acts["mock-sandbox_client_create"](args) - - assert exc_info.value.type == "TemporalWorkerEnvValueUnresolved" - assert exc_info.value.non_retryable - assert "WORKER_ACTIVITY_SECRET" in str(exc_info.value) - assert "resolvable_worker_env_vars" in str(exc_info.value) - assert secret not in str(exc_info.value) - - -async def test_the_resolvable_names_are_snapshotted_when_the_activities_are_built( - sandbox_activities: SandboxClientProvider, - mock_client: _MockSandboxClient, - monkeypatch: pytest.MonkeyPatch, -): - secret = "sk-snapshot-secret" - monkeypatch.setenv("WORKER_ACTIVITY_SECRET", secret) - - args = CreateSessionArgs( - snapshot_spec=None, - manifest=Manifest( - environment=Environment( - value={"API_KEY": TemporalWorkerEnvValue(name="WORKER_ACTIVITY_SECRET")} - ) - ), - client_options=None, - ) - - resolvable = ["WORKER_ACTIVITY_SECRET"] - acts = _activity_map(sandbox_activities, resolvable) - resolvable.clear() - - await acts["mock-sandbox_client_create"](args) - assert mock_client.resolved_envs == {"API_KEY": secret} - - -class _ScopeRecordingSession(_MockSandboxSession): - def __init__(self, scopes: list[frozenset[str] | AllowAllWorkerEnvVars]) -> None: - super().__init__() - self._scopes = scopes - - def supports_pty(self) -> bool: - return True - - async def shutdown(self) -> None: - self._scopes.append(_resolvable_worker_env_vars.get(frozenset())) - await super().shutdown() - - async def pty_exec_start( - self, *command: str | Path, **kwargs: Any - ) -> PtyExecUpdate: - return PtyExecUpdate( - process_id=1, output=b"", exit_code=None, original_token_count=None - ) - - async def pty_write_stdin(self, **kwargs: Any) -> PtyExecUpdate: - return PtyExecUpdate( - process_id=1, output=b"", exit_code=None, original_token_count=None - ) - - -class _ScopeRecordingClient(_MockSandboxClient): - def __init__(self) -> None: - self.scopes: list[frozenset[str] | AllowAllWorkerEnvVars] = [] - super().__init__(_ScopeRecordingSession(self.scopes)) - - async def create(self, **kwargs: Any) -> SandboxSession: - self.scopes.append(_resolvable_worker_env_vars.get(frozenset())) - return await super().create(**kwargs) - - async def resume(self, state: SandboxSessionState) -> SandboxSession: - self.scopes.append(_resolvable_worker_env_vars.get(frozenset())) - return await super().resume(state) - - -async def test_every_activity_runs_its_body_inside_the_resolvable_names_scope(): - recording_client = _ScopeRecordingClient() - provider = SandboxClientProvider("mock", recording_client) - acts = _activity_map(provider, ["A_RESOLVABLE_NAME"]) - state = _make_state() - args_by_activity: dict[str, Any] = { - "mock-sandbox_client_create": CreateSessionArgs( - snapshot_spec=None, manifest=Manifest(), client_options=None - ), - "mock-sandbox_client_resume": ResumeSessionArgs(state=state), - "mock-sandbox_client_delete": StopArgs(state=state), - "mock-sandbox_session_exec": ExecArgs(state=state, command=["ls"], shell=True), - "mock-sandbox_session_read": ReadArgs(state=state, path="/tmp/f"), - "mock-sandbox_session_write": WriteArgs(state=state, path="/tmp/f", data=b"d"), - "mock-sandbox_session_running": RunningArgs(state=state), - "mock-sandbox_session_persist_workspace": PersistWorkspaceArgs(state=state), - "mock-sandbox_session_hydrate_workspace": HydrateWorkspaceArgs( - state=state, data=b"d" - ), - "mock-sandbox_session_pty_exec_start": PtyExecStartArgs( - state=state, command=["ls"] - ), - "mock-sandbox_session_pty_write_stdin": PtyWriteStdinArgs( - state=state, session_id=1, chars="x" - ), - "mock-sandbox_session_start": StartArgs(state=state), - "mock-sandbox_session_stop": StopArgs(state=state), - "mock-sandbox_session_shutdown": StopArgs(state=state), - } - assert set(args_by_activity) == set(acts) - - for name, args in args_by_activity.items(): - # Shutdown is the one activity that does not resume on a cache miss, so - # it alone needs a cached session to reach the client. - cached = name == "mock-sandbox_session_shutdown" - provider._sessions = ( - {str(state.session_id): recording_client.session} if cached else {} - ) - recording_client.scopes.clear() - await acts[name](args) - assert recording_client.scopes == [frozenset({"A_RESOLVABLE_NAME"})], name - - -def _plugin_with_one_shot_names( - *sandbox_clients: SandboxClientProvider, -) -> OpenAIAgentsPlugin: - return OpenAIAgentsPlugin( - model_params=ModelActivityParameters( - start_to_close_timeout=timedelta(seconds=30), - ), - model_provider=TestModelProvider( - TestModel.returning_responses([ResponseBuilders.output_message("done")]) - ), - sandbox_clients=list(sandbox_clients), - resolvable_worker_env_vars=(name for name in ["A_RESOLVABLE_NAME"]), # type: ignore[arg-type] - ) - - -def _plugin_activity_map(plugin: OpenAIAgentsPlugin) -> dict[str, Any]: - build_activities = plugin.activities - assert callable(build_activities) - return { - act.__temporal_activity_definition.name: act # type: ignore[attr-defined, union-attr] - for act in build_activities([]) - } - - -def _hosted_resolvable_names( - acts: dict[str, Any], -) -> frozenset[str] | AllowAllWorkerEnvVars: - return acts["invoke_model_activity"].__self__._env_refs._allowed - - -async def test_a_one_shot_names_iterable_reaches_the_hosted_and_the_sandbox_activities(): - recording_client = _ScopeRecordingClient() - acts = _plugin_activity_map( - _plugin_with_one_shot_names(SandboxClientProvider("mock", recording_client)) - ) - - assert _hosted_resolvable_names(acts) == frozenset({"A_RESOLVABLE_NAME"}) - await acts["mock-sandbox_client_create"]( - CreateSessionArgs(snapshot_spec=None, manifest=Manifest(), client_options=None) - ) - assert recording_client.scopes == [frozenset({"A_RESOLVABLE_NAME"})] - - -async def test_every_sandbox_provider_on_a_plugin_gets_the_names(): - first = _ScopeRecordingClient() - second = _ScopeRecordingClient() - acts = _plugin_activity_map( - _plugin_with_one_shot_names( - SandboxClientProvider("first", first), - SandboxClientProvider("second", second), - ) - ) - - args = CreateSessionArgs( - snapshot_spec=None, manifest=Manifest(), client_options=None - ) - await acts["first-sandbox_client_create"](args) - await acts["second-sandbox_client_create"](args) - assert first.scopes == [frozenset({"A_RESOLVABLE_NAME"})] - assert second.scopes == [frozenset({"A_RESOLVABLE_NAME"})] - - -async def test_a_second_worker_built_from_one_plugin_gets_the_names(): - recording_client = _ScopeRecordingClient() - plugin = _plugin_with_one_shot_names( - SandboxClientProvider("mock", recording_client) - ) - _plugin_activity_map(plugin) - acts = _plugin_activity_map(plugin) - - assert _hosted_resolvable_names(acts) == frozenset({"A_RESOLVABLE_NAME"}) - await acts["mock-sandbox_client_create"]( - CreateSessionArgs(snapshot_spec=None, manifest=Manifest(), client_options=None) - ) - assert recording_client.scopes == [frozenset({"A_RESOLVABLE_NAME"})] - - -async def test_activities_resume_session_delegates( - sandbox_activities: SandboxClientProvider, - mock_client: _MockSandboxClient, -): - """resume_session activity should delegate to the real client's resume().""" - acts = _activity_map(sandbox_activities) - state = _make_state() - args = ResumeSessionArgs(state=state) - result = await acts["mock-sandbox_client_resume"](args) - assert mock_client.resume_calls == 1 - assert result.state is not None - - -async def test_activities_exec_delegates( - sandbox_activities: SandboxClientProvider, - mock_client: _MockSandboxClient, -): - """exec activity should delegate to the real session's exec().""" - acts = _activity_map(sandbox_activities) - # First create a session so the activities cache is populated - await acts["mock-sandbox_client_create"]( - CreateSessionArgs(snapshot_spec=None, manifest=Manifest(), client_options=None) - ) - state = mock_client.inner_session.state - - args = ExecArgs(state=state, command=["echo", "hello"], timeout=10.0, shell=True) - result = await acts["mock-sandbox_session_exec"](args) - assert result.stdout == b"ok\n" - assert result.stderr == b"" - assert result.exit_code == 0 - assert len(mock_client.inner_session.exec_calls) == 1 - - -async def test_activities_read_delegates( - sandbox_activities: SandboxClientProvider, - mock_client: _MockSandboxClient, -): - """read activity should delegate to the real session's read().""" - acts = _activity_map(sandbox_activities) - await acts["mock-sandbox_client_create"]( - CreateSessionArgs(snapshot_spec=None, manifest=Manifest(), client_options=None) - ) - state = mock_client.inner_session.state - - args = ReadArgs(state=state, path="/tmp/test.txt") - result = await acts["mock-sandbox_session_read"](args) - assert result.data == b"file-content" - assert len(mock_client.inner_session.read_calls) == 1 - - -async def test_activities_write_delegates( - sandbox_activities: SandboxClientProvider, - mock_client: _MockSandboxClient, -): - """write activity should delegate to the real session's write().""" - acts = _activity_map(sandbox_activities) - await acts["mock-sandbox_client_create"]( - CreateSessionArgs(snapshot_spec=None, manifest=Manifest(), client_options=None) - ) - state = mock_client.inner_session.state - - args = WriteArgs(state=state, path="/tmp/out.txt", data=b"written-data") - await acts["mock-sandbox_session_write"](args) - assert len(mock_client.inner_session.write_calls) == 1 - assert mock_client.inner_session.write_calls[0][1] == b"written-data" - - -async def test_activities_running_delegates( - sandbox_activities: SandboxClientProvider, - mock_client: _MockSandboxClient, -): - """running activity should delegate to the real session's running().""" - acts = _activity_map(sandbox_activities) - await acts["mock-sandbox_client_create"]( - CreateSessionArgs(snapshot_spec=None, manifest=Manifest(), client_options=None) - ) - state = mock_client.inner_session.state - - args = RunningArgs(state=state) - result = await acts["mock-sandbox_session_running"](args) - assert result.is_running is True - assert mock_client.inner_session.running_calls == 1 - - -async def test_activities_client_delete_delegates( - sandbox_activities: SandboxClientProvider, - mock_client: _MockSandboxClient, -): - """client_delete activity should delegate to the real client's delete().""" - acts = _activity_map(sandbox_activities) - await acts["mock-sandbox_client_create"]( - CreateSessionArgs(snapshot_spec=None, manifest=Manifest(), client_options=None) - ) - state = mock_client.inner_session.state - - args = StopArgs(state=state) - await acts["mock-sandbox_client_delete"](args) - - assert mock_client.delete_calls == 1 - - -async def test_activities_session_shutdown_clears_cache( - sandbox_activities: SandboxClientProvider, - mock_client: _MockSandboxClient, -): - """session_shutdown activity should call session.shutdown() and evict from cache.""" - acts = _activity_map(sandbox_activities) - await acts["mock-sandbox_client_create"]( - CreateSessionArgs(snapshot_spec=None, manifest=Manifest(), client_options=None) - ) - state = mock_client.inner_session.state - session_key = str(state.session_id) - - # Session should be cached - assert session_key in sandbox_activities._sessions - - args = StopArgs(state=state) - await acts["mock-sandbox_session_shutdown"](args) - - assert mock_client.inner_session.shutdown_calls == 1 - # Session should be evicted from cache - assert session_key not in sandbox_activities._sessions - - -async def test_activities_session_shutdown_noop_for_unknown_session( - sandbox_activities: SandboxClientProvider, -): - """session_shutdown should be a no-op if the session isn't in the cache.""" - acts = _activity_map(sandbox_activities) - state = _make_state() - args = StopArgs(state=state) - # Should not raise - await acts["mock-sandbox_session_shutdown"](args) - - -async def test_activities_session_caching( - sandbox_activities: SandboxClientProvider, - mock_client: _MockSandboxClient, -): - """Multiple operations on the same session should reuse the cached session.""" - acts = _activity_map(sandbox_activities) - await acts["mock-sandbox_client_create"]( - CreateSessionArgs(snapshot_spec=None, manifest=Manifest(), client_options=None) - ) - state = mock_client.inner_session.state - - # Multiple exec calls should not trigger additional resume calls - await acts["mock-sandbox_session_exec"]( - ExecArgs(state=state, command=["cmd1"], shell=True) - ) - await acts["mock-sandbox_session_exec"]( - ExecArgs(state=state, command=["cmd2"], shell=True) - ) - assert mock_client.resume_calls == 0 - assert len(mock_client.inner_session.exec_calls) == 2 - - -async def test_activities_all_returns_all_activity_methods( - sandbox_activities: SandboxClientProvider, -): - """all() should return all 14 activity callables with prefixed names.""" - activities = sandbox_activities._get_activities(()) - assert len(activities) == 14 - # Verify they are all activity-decorated callables with prefixed names - activity_names = set() - for act in activities: - assert hasattr(act, "__temporal_activity_definition") - activity_names.add(act.__temporal_activity_definition.name) # type: ignore[union-attr] - expected = { - "mock-sandbox_client_create", - "mock-sandbox_client_resume", - "mock-sandbox_client_delete", - "mock-sandbox_session_exec", - "mock-sandbox_session_read", - "mock-sandbox_session_write", - "mock-sandbox_session_running", - "mock-sandbox_session_persist_workspace", - "mock-sandbox_session_hydrate_workspace", - "mock-sandbox_session_pty_exec_start", - "mock-sandbox_session_pty_write_stdin", - "mock-sandbox_session_start", - "mock-sandbox_session_stop", - "mock-sandbox_session_shutdown", - } - assert activity_names == expected - - -async def test_multiple_providers_register_distinct_activities(): - """Multiple SandboxClientProviders should produce distinct prefixed activity sets.""" - client1 = _MockSandboxClient() - client2 = _MockSandboxClient() - provider1 = SandboxClientProvider("daytona", client1) - provider2 = SandboxClientProvider("local", client2) - - activities1 = provider1._get_activities(()) - activities2 = provider2._get_activities(()) - - names1 = {a.__temporal_activity_definition.name for a in activities1} # type: ignore - names2 = {a.__temporal_activity_definition.name for a in activities2} # type: ignore - - # No overlap - assert names1.isdisjoint(names2) - # Both have 14 activities - assert len(names1) == 14 - assert len(names2) == 14 - # Verify prefixes - assert all( - n.startswith("daytona-sandbox_client_") - or n.startswith("daytona-sandbox_session_") - for n in names1 - ) - assert all( - n.startswith("local-sandbox_client_") or n.startswith("local-sandbox_session_") - for n in names2 - ) - - -# ── SandboxError retryable mapping tests ── - - -class _ExecRaisingSession(_MockSandboxSession): - """Mock session whose exec() raises a chosen SandboxError.""" - - def __init__(self, error: SandboxError) -> None: - super().__init__() - self._error = error - - async def _exec_internal( - self, - *command: str | Path, # type: ignore[reportUnusedParameter] - timeout: float | None = None, # type: ignore[reportUnusedParameter] - ) -> ExecResult: - raise self._error - - -async def _exec_with_error(error: SandboxError) -> None: - provider = SandboxClientProvider( - "mock", _MockSandboxClient(_ExecRaisingSession(error)) - ) - acts = _activity_map(provider) - state = ( - await acts["mock-sandbox_client_create"]( - CreateSessionArgs( - snapshot_spec=None, manifest=Manifest(), client_options=None - ) - ) - ).state - await acts["mock-sandbox_session_exec"]( - ExecArgs(state=state, command=["boom"], shell=True) - ) - - -async def test_exec_terminal_error_becomes_non_retryable_application_error(): - """retryable is False should map to a non-retryable ApplicationError.""" - with pytest.raises(ApplicationError) as exc_info: - await _exec_with_error(ExecTransportError(command=["boom"], retryable=False)) - assert exc_info.value.non_retryable is True - assert exc_info.value.type == "exec_transport_error" - - -async def test_exec_transient_error_propagates_unchanged(): - """retryable is True should let the original SandboxError propagate.""" - with pytest.raises(ExecTransportError): - await _exec_with_error(ExecTransportError(command=["boom"], retryable=True)) - - -async def test_exec_unclassified_error_propagates_unchanged(): - """retryable is None should let the original SandboxError propagate (not converted).""" - with pytest.raises(ExecTransportError): - await _exec_with_error(ExecTransportError(command=["boom"], retryable=None)) - - -class _ShutdownRaisingSession(_MockSandboxSession): - """Mock session whose shutdown() raises a chosen SandboxError.""" - - def __init__(self, error: SandboxError) -> None: - super().__init__() - self._error = error - - async def shutdown(self) -> None: - raise self._error - - -async def _create_shutdown_raising( - error: SandboxError, -) -> tuple[dict[str, Any], SandboxClientProvider, StopArgs, str]: - provider = SandboxClientProvider( - "mock", _MockSandboxClient(_ShutdownRaisingSession(error)) - ) - acts = _activity_map(provider) - state = ( - await acts["mock-sandbox_client_create"]( - CreateSessionArgs( - snapshot_spec=None, manifest=Manifest(), client_options=None - ) - ) - ).state - key = str(state.session_id) - assert key in provider._sessions - return acts, provider, StopArgs(state=state), key - - -async def test_shutdown_terminal_error_evicts_session_and_raises(): - """A terminal shutdown error maps to a non-retryable ApplicationError and - evicts the dead session from the cache.""" - acts, provider, args, key = await _create_shutdown_raising( - ExecTransportError(command=["shutdown"], retryable=False) - ) - - with pytest.raises(ApplicationError) as exc_info: - await acts["mock-sandbox_session_shutdown"](args) - assert exc_info.value.non_retryable is True - assert key not in provider._sessions - - -async def test_shutdown_retryable_error_keeps_session_cached(): - """A retryable shutdown error propagates unchanged and leaves the session - cached so the activity's retry can still shut it down.""" - acts, provider, args, key = await _create_shutdown_raising( - ExecTransportError(command=["shutdown"], retryable=True) - ) - - with pytest.raises(ExecTransportError): - await acts["mock-sandbox_session_shutdown"](args) - assert key in provider._sessions - - -class _RunningRaisingSession(_MockSandboxSession): - """Mock session whose running() raises a chosen SandboxError.""" - - def __init__(self, error: SandboxError) -> None: - super().__init__() - self._error = error - - async def running(self) -> bool: - raise self._error - - -async def test_running_terminal_error_becomes_non_retryable_application_error(): - """A terminal SandboxError from a non-exec activity also maps to a - non-retryable ApplicationError, with type set to its error_code.""" - error = WorkspaceArchiveReadError(path=Path("/workspace"), retryable=False) - provider = SandboxClientProvider( - "mock", _MockSandboxClient(_RunningRaisingSession(error)) - ) - acts = _activity_map(provider) - state = ( - await acts["mock-sandbox_client_create"]( - CreateSessionArgs( - snapshot_spec=None, manifest=Manifest(), client_options=None - ) - ) - ).state - - with pytest.raises(ApplicationError) as exc_info: - await acts["mock-sandbox_session_running"](RunningArgs(state=state)) - assert exc_info.value.non_retryable is True - assert exc_info.value.type == "workspace_archive_read_error" - - -# ── End-to-end test: Runner + SandboxAgent through Temporal activities ── - - -class _TestSandboxCapability(Capability): - """Minimal capability exposing exec, read, and write via FunctionTools.""" - - def __init__(self) -> None: - super().__init__(type="test_sandbox") - self._session: BaseSandboxSession | None = None - - def bind(self, session: BaseSandboxSession) -> None: - self._session = session - - def tools(self) -> list[Tool]: - session = self._session - - async def _run_cmd(ctx: Any, args: str) -> str: # type: ignore[reportUnusedParameter] - import json - - cmd = json.loads(args)["cmd"] - result = await session.exec(cmd, shell=True) # type: ignore[union-attr] - return result.stdout.decode() - - async def _read_file(ctx: Any, args: str) -> str: # type: ignore[reportUnusedParameter] - import json - - path = json.loads(args)["path"] - handle = await session.read(Path(path)) # type: ignore[union-attr] - return handle.read().decode() - - async def _write_file(ctx: Any, args: str) -> str: # type: ignore[reportUnusedParameter] - import json - - parsed = json.loads(args) - await session.write( # type: ignore[union-attr] - Path(parsed["path"]), io.BytesIO(parsed["data"].encode()) - ) - return "ok" - - return [ - FunctionTool( - name="run_command", - description="Run a shell command", - params_json_schema={ - "type": "object", - "properties": {"cmd": {"type": "string"}}, - "required": ["cmd"], - }, - on_invoke_tool=_run_cmd, - ), - FunctionTool( - name="read_file", - description="Read a file", - params_json_schema={ - "type": "object", - "properties": {"path": {"type": "string"}}, - "required": ["path"], - }, - on_invoke_tool=_read_file, - ), - FunctionTool( - name="write_file", - description="Write a file", - params_json_schema={ - "type": "object", - "properties": { - "path": {"type": "string"}, - "data": {"type": "string"}, - }, - "required": ["path", "data"], - }, - on_invoke_tool=_write_file, - ), - ] - - -class _TestSandboxClientOptions(BaseSandboxClientOptions): - type: str = "test" # type: ignore[reportIncompatibleVariableOverride] - - -@workflow.defn -class SandboxE2EWorkflow: - @workflow.run - async def run(self) -> str: - agent = SandboxAgent[None]( - name="sandbox-e2e", capabilities=[_TestSandboxCapability()] - ) - result = await Runner.run( - starting_agent=agent, - input="run a command", - run_config=RunConfig( - sandbox=SandboxRunConfig( - client=temporal_sandbox_client("mock"), - options=_TestSandboxClientOptions(), - ), - ), - ) - return result.final_output - - -_HOST_PATH = "/host/private-dir" -_HOST_PATH_GRANT_MANIFEST = Manifest( - extra_path_grants=( - SandboxPathGrant(path="/workspace/shared", host_path=_HOST_PATH), - ) -) -# A clean grant first, so a check that only inspects index 0 fails this. -_TRAILING_GRANT_MANIFEST = Manifest( - extra_path_grants=( - SandboxPathGrant(path="/workspace/clean"), - SandboxPathGrant(path="/workspace/shared", host_path=_HOST_PATH), - ) -) -_TWO_BOUND_GRANTS_MANIFEST = Manifest( - extra_path_grants=( - SandboxPathGrant(path="/workspace/shared", host_path=_HOST_PATH), - SandboxPathGrant(path="/workspace/other", host_path="/host/second-dir"), - ) -) - - -class _GrantInjectingCapability(Capability): - def __init__(self) -> None: - super().__init__(type="grant_injecting") - - def process_manifest(self, manifest: Manifest) -> Manifest: - return manifest.model_copy( - update={ - "extra_path_grants": ( - *manifest.extra_path_grants, - SandboxPathGrant(path="/workspace/injected", host_path=_HOST_PATH), - ) - } - ) - - -@workflow.defn -class HostPathGrantWorkflow: - @workflow.run - async def run(self, route: str) -> str: - agent = SandboxAgent[None](name="sandbox-grant") - client = temporal_sandbox_client("mock") - options = _TestSandboxClientOptions() - expected = "/workspace/shared" - - if route == "run_config_manifest": - sandbox = SandboxRunConfig( - client=client, options=options, manifest=_HOST_PATH_GRANT_MANIFEST - ) - elif route == "default_manifest": - agent = SandboxAgent[None]( - name="sandbox-grant", default_manifest=_HOST_PATH_GRANT_MANIFEST - ) - sandbox = SandboxRunConfig(client=client, options=options) - elif route == "session_state": - sandbox = SandboxRunConfig( - client=client, - options=options, - session_state=TestSessionState( - manifest=_HOST_PATH_GRANT_MANIFEST, - snapshot=NoopSnapshot(id=str(workflow.uuid4())), - ), - ) - elif route == "capability": - # The manifest must be present but empty: upstream skips capabilities - # when there is no manifest at all. - agent = SandboxAgent[None]( - name="sandbox-grant", capabilities=[_GrantInjectingCapability()] - ) - sandbox = SandboxRunConfig( - client=client, options=options, manifest=Manifest() - ) - expected = "/workspace/injected" - elif route == "trailing_grant": - sandbox = SandboxRunConfig( - client=client, options=options, manifest=_TRAILING_GRANT_MANIFEST - ) - elif route == "two_bound_grants": - sandbox = SandboxRunConfig( - client=client, options=options, manifest=_TWO_BOUND_GRANTS_MANIFEST - ) - expected = "/workspace/shared, /workspace/other" - else: - raise AssertionError(f"unknown route {route}") - - try: - await Runner.run( - starting_agent=agent, - input="hello", - run_config=RunConfig(sandbox=sandbox), - ) - except AgentsWorkflowError as e: - assert expected in str(e), str(e) - # The guard must not name the host path: this text reaches history. - assert _HOST_PATH not in str(e), str(e) - return "REJECTED" - return "NOT REJECTED" - - -@pytest.mark.parametrize( - "route", - [ - "run_config_manifest", - "default_manifest", - "session_state", - "capability", - "trailing_grant", - "two_bound_grants", - ], -) -async def test_host_path_grants_are_rejected_per_manifest_source( - client: Client, route: str -): - mock_sandbox_client = _MockSandboxClient(_MockSandboxSession()) - plugin = OpenAIAgentsPlugin( - model_params=ModelActivityParameters( - start_to_close_timeout=timedelta(seconds=30), - ), - model_provider=TestModelProvider( - TestModel.returning_responses([ResponseBuilders.output_message("done")]) - ), - sandbox_clients=[SandboxClientProvider("mock", mock_sandbox_client)], - ) - new_config = client.config() - new_config["plugins"] = [plugin] - test_client = Client(**new_config) - - async with new_worker( - test_client, - HostPathGrantWorkflow, - workflow_failure_exception_types=[Exception], - ) as worker: - result = await test_client.execute_workflow( - HostPathGrantWorkflow.run, - route, - id=f"host-path-grant-{route}-{uuid.uuid4()}", - task_queue=worker.task_queue, - execution_timeout=timedelta(seconds=15), - ) - - assert result == "REJECTED" - assert mock_sandbox_client.create_calls == 0 - assert mock_sandbox_client.resume_calls == 0 - - -@workflow.defn -class UncaughtHostPathGrantWorkflow: - @workflow.run - async def run(self) -> str: - await Runner.run( - starting_agent=SandboxAgent[None](name="sandbox-grant"), - input="hello", - run_config=RunConfig( - sandbox=SandboxRunConfig( - client=temporal_sandbox_client("mock"), - options=_TestSandboxClientOptions(), - manifest=_HOST_PATH_GRANT_MANIFEST, - ), - ), - ) - return "NOT REJECTED" - - -async def test_host_path_grant_fails_the_workflow_on_a_production_like_worker( - client: Client, -): - """Given no test-only ``workflow_failure_exception_types``, so only the plugin's own applies.""" - mock_sandbox_client = _MockSandboxClient(_MockSandboxSession()) - plugin = OpenAIAgentsPlugin( - model_params=ModelActivityParameters( - start_to_close_timeout=timedelta(seconds=30), - ), - model_provider=TestModelProvider( - TestModel.returning_responses([ResponseBuilders.output_message("done")]) - ), - sandbox_clients=[SandboxClientProvider("mock", mock_sandbox_client)], - ) - new_config = client.config() - new_config["plugins"] = [plugin] - test_client = Client(**new_config) - - async with new_worker(test_client, UncaughtHostPathGrantWorkflow) as worker: - with pytest.raises(WorkflowFailureError) as exc_info: - await test_client.execute_workflow( - UncaughtHostPathGrantWorkflow.run, - id=f"host-path-uncaught-{uuid.uuid4()}", - task_queue=worker.task_queue, - execution_timeout=timedelta(seconds=15), - ) - - cause = exc_info.value.cause - assert isinstance(cause, ApplicationError), cause - assert cause.type == "AgentsWorkflowError", cause.type - assert "/workspace/shared" in str(cause) - assert _HOST_PATH not in str(cause) - assert mock_sandbox_client.create_calls == 0 - - -_PLUGIN_ENV_NAME = "WORKER_ENV_VALUE_THROUGH_THE_PLUGIN" - - -@workflow.defn -class SandboxWorkerEnvValueWorkflow: - @workflow.run - async def run(self) -> str: - await Runner.run( - starting_agent=SandboxAgent[None](name="sandbox-env"), - input="hello", - run_config=RunConfig( - sandbox=SandboxRunConfig( - client=temporal_sandbox_client("mock"), - options=_TestSandboxClientOptions(), - manifest=Manifest( - environment=Environment( - value={ - "API_KEY": TemporalWorkerEnvValue(name=_PLUGIN_ENV_NAME) - } - ) - ), - ), - ), - ) - return "RAN" - - -@pytest.mark.parametrize( - ("resolvable", "resolves"), - [([_PLUGIN_ENV_NAME], True), (["SOMETHING_ELSE"], False)], -) -async def test_a_sandbox_activity_resolves_only_the_variables_its_plugin_names( - client: Client, - monkeypatch: pytest.MonkeyPatch, - resolvable: list[str], - resolves: bool, -): - secret = "sk-through-the-plugin" - monkeypatch.setenv(_PLUGIN_ENV_NAME, secret) - mock_sandbox_client = _MockSandboxClient(_MockSandboxSession()) - plugin = OpenAIAgentsPlugin( - model_params=ModelActivityParameters( - start_to_close_timeout=timedelta(seconds=30), - ), - model_provider=TestModelProvider( - TestModel.returning_responses([ResponseBuilders.output_message("done")]) - ), - sandbox_clients=[SandboxClientProvider("mock", mock_sandbox_client)], - resolvable_worker_env_vars=resolvable, - ) - new_config = client.config() - new_config["plugins"] = [plugin] - test_client = Client(**new_config) - - async def execute(worker: Any) -> str: - return await test_client.execute_workflow( - SandboxWorkerEnvValueWorkflow.run, - id=f"sandbox-env-value-{uuid.uuid4()}", - task_queue=worker.task_queue, - execution_timeout=timedelta(seconds=15), - ) - - async with new_worker(test_client, SandboxWorkerEnvValueWorkflow) as worker: - if resolves: - assert await execute(worker) == "RAN" - assert mock_sandbox_client.resolved_envs == {"API_KEY": secret} - else: - with pytest.raises(WorkflowFailureError) as exc_info: - await execute(worker) - activity_error = exc_info.value.cause - assert isinstance(activity_error, ActivityError), activity_error - cause = activity_error.cause - assert isinstance(cause, ApplicationError), cause - assert cause.type == "TemporalWorkerEnvValueUnresolved" - assert _PLUGIN_ENV_NAME in str(cause) - assert secret not in str(cause) - - -@workflow.defn -class ResolveOnWorkflowThreadWorkflow: - @workflow.run - async def run(self) -> str: - try: - await TemporalWorkerEnvValue(name="WORKER_THREAD_PROBE_NAME").resolve() - except ApplicationError as e: - return e.message - return "NO RAISE" - - -async def test_worker_env_value_resolve_raises_inside_a_real_workflow(client: Client): - """``in_workflow()`` is genuinely True here, unlike the monkeypatched unit test.""" - plugin = OpenAIAgentsPlugin( - model_params=ModelActivityParameters( - start_to_close_timeout=timedelta(seconds=30), - ), - model_provider=TestModelProvider( - TestModel.returning_responses([ResponseBuilders.output_message("done")]) - ), - ) - new_config = client.config() - new_config["plugins"] = [plugin] - test_client = Client(**new_config) - - async with new_worker( - test_client, - ResolveOnWorkflowThreadWorkflow, - workflow_failure_exception_types=[Exception], - ) as worker: - result = await test_client.execute_workflow( - ResolveOnWorkflowThreadWorkflow.run, - id=f"resolve-in-workflow-{uuid.uuid4()}", - task_queue=worker.task_queue, - execution_timeout=timedelta(seconds=15), - ) - - assert "must run in an activity" in result - - -@workflow.defn -class LiveSandboxSessionWorkflow: - @workflow.run - async def run(self) -> str: - try: - await Runner.run( - starting_agent=SandboxAgent[None](name="sandbox-live"), - input="hello", - run_config=RunConfig( - sandbox=SandboxRunConfig( - # Rejected on presence, so the value is never used. - session=object(), # type: ignore[arg-type] - ), - ), - ) - except AgentsWorkflowError as e: - assert "run_config.sandbox.session" in str(e), str(e) - return "REJECTED" - return "NOT REJECTED" - - -async def test_live_sandbox_session_is_rejected(client: Client): - mock_sandbox_client = _MockSandboxClient(_MockSandboxSession()) - plugin = OpenAIAgentsPlugin( - model_params=ModelActivityParameters( - start_to_close_timeout=timedelta(seconds=30), - ), - model_provider=TestModelProvider( - TestModel.returning_responses([ResponseBuilders.output_message("done")]) - ), - sandbox_clients=[SandboxClientProvider("mock", mock_sandbox_client)], - ) - new_config = client.config() - new_config["plugins"] = [plugin] - test_client = Client(**new_config) - - async with new_worker( - test_client, - LiveSandboxSessionWorkflow, - workflow_failure_exception_types=[Exception], - ) as worker: - result = await test_client.execute_workflow( - LiveSandboxSessionWorkflow.run, - id=f"live-session-{uuid.uuid4()}", - task_queue=worker.task_queue, - execution_timeout=timedelta(seconds=15), - ) - - assert result == "REJECTED" - assert mock_sandbox_client.create_calls == 0 - - -async def test_sandbox_e2e_runner(client: Client): - """End-to-end: Runner.run() with SandboxAgent exercises the full sandbox - lifecycle (create, start, stop, shutdown, delete) through Temporal activities.""" - mock_session = _MockSandboxSession() - mock_sandbox_client = _MockSandboxClient(mock_session) - - mock_model = TestModel.returning_responses( - [ - ResponseBuilders.tool_call('{"cmd": "echo hello"}', "run_command"), - ResponseBuilders.tool_call('{"path": "/tmp/test.txt"}', "read_file"), - ResponseBuilders.tool_call( - '{"path": "/tmp/out.txt", "data": "hello"}', "write_file" - ), - ResponseBuilders.output_message("Done."), - ] - ) - - plugin = OpenAIAgentsPlugin( - model_params=ModelActivityParameters( - start_to_close_timeout=timedelta(seconds=30), - ), - model_provider=TestModelProvider(mock_model), - sandbox_clients=[SandboxClientProvider("mock", mock_sandbox_client)], - ) - - new_config = client.config() - new_config["plugins"] = [plugin] - test_client = Client(**new_config) - - async with new_worker( - test_client, - SandboxE2EWorkflow, - workflow_failure_exception_types=[Exception], - ) as worker: - result = await test_client.execute_workflow( - SandboxE2EWorkflow.run, - id=f"sandbox-e2e-{uuid.uuid4()}", - task_queue=worker.task_queue, - execution_timeout=timedelta(seconds=10), - ) - - assert result == "Done." - # Full sandbox lifecycle exercised through Temporal activities - assert mock_sandbox_client.create_calls == 1, "client.create() not called" - assert mock_session.start_calls == 1, "session.start() not called" - assert len(mock_session.exec_calls) >= 1, "session.exec() not called" - assert len(mock_session.read_calls) >= 1, "session.read() not called" - assert len(mock_session.write_calls) >= 1, "session.write() not called" - assert mock_session.stop_calls >= 1, "session.stop() not called" - assert mock_session.shutdown_calls >= 1, "session.shutdown() not called" - assert mock_sandbox_client.delete_calls == 1, "client.delete() not called" - - -# ── JsonSafeBytes lossless serialization tests ── - -# Payloads that exercise edge cases for bytes → JSON → bytes roundtrip. -_BYTE_PAYLOADS = [ - pytest.param(b"", id="empty"), - pytest.param(b"hello world", id="ascii"), - pytest.param(b"\xc3\xa9\xc3\xa0", id="valid-utf8"), # éà - pytest.param(bytes(range(256)), id="all-byte-values"), - pytest.param(b"\xff\xfe\x80\x90\x00\x01", id="non-utf8-binary"), - pytest.param(b"ok\nWarning: \xff\xfe binary \x80\x90\x00\x01", id="mixed"), - pytest.param(b"\x00\x00\x00", id="null-bytes"), -] - - -def _roundtrip(model_cls: Any, **kwargs: Any) -> Any: - """Serialize a model to JSON via pydantic_core and deserialize back.""" - json_bytes = to_json(model_cls(**kwargs)) - return TypeAdapter(model_cls).validate_json(json_bytes) - - -@pytest.mark.parametrize("payload", _BYTE_PAYLOADS) -def test_exec_result_bytes_roundtrip(payload: bytes): - """ExecResult.stdout/stderr must survive a JSON roundtrip unchanged.""" - restored = _roundtrip(ExecResultModel, stdout=payload, stderr=payload, exit_code=1) - assert restored.stdout == payload - assert restored.stderr == payload - assert restored.exit_code == 1 - - -@pytest.mark.parametrize("payload", _BYTE_PAYLOADS) -def test_pty_exec_update_result_bytes_roundtrip(payload: bytes): - """PtyExecUpdateResult.output must survive a JSON roundtrip unchanged.""" - restored = _roundtrip( - PtyExecUpdateResult, - process_id=1, - output=payload, - exit_code=0, - original_token_count=None, - ) - assert restored.output == payload - - -@pytest.mark.parametrize("payload", _BYTE_PAYLOADS) -def test_read_result_bytes_roundtrip(payload: bytes): - """ReadResult.data must survive a JSON roundtrip unchanged.""" - restored = _roundtrip(ReadResult, data=payload) - assert restored.data == payload - - -@pytest.mark.parametrize("payload", _BYTE_PAYLOADS) -def test_persist_workspace_result_bytes_roundtrip(payload: bytes): - """PersistWorkspaceResult.data must survive a JSON roundtrip unchanged.""" - restored = _roundtrip(PersistWorkspaceResult, data=payload) - assert restored.data == payload - - -@pytest.mark.parametrize("payload", _BYTE_PAYLOADS) -def test_write_args_bytes_roundtrip(payload: bytes): - """WriteArgs.data must survive a JSON roundtrip unchanged (workflow → activity).""" - restored = _roundtrip(WriteArgs, state=_make_state(), path="/tmp/f", data=payload) - assert restored.data == payload - - -@pytest.mark.parametrize("payload", _BYTE_PAYLOADS) -def test_hydrate_workspace_args_bytes_roundtrip(payload: bytes): - """HydrateWorkspaceArgs.data must survive a JSON roundtrip unchanged.""" - restored = _roundtrip(HydrateWorkspaceArgs, state=_make_state(), data=payload) - assert restored.data == payload diff --git a/tests/contrib/openai_agents/test_openai_sandbox_worker_env_value.py b/tests/contrib/openai_agents/test_openai_sandbox_worker_env_value.py deleted file mode 100644 index bfeffef0e..000000000 --- a/tests/contrib/openai_agents/test_openai_sandbox_worker_env_value.py +++ /dev/null @@ -1,295 +0,0 @@ -from __future__ import annotations - -import uuid -from typing import Any, Literal - -import pytest -from agents.sandbox import Manifest -from agents.sandbox.manifest import EnvEntry, Environment, EnvValue, StrEnvValue -from agents.sandbox.session.sandbox_session_state import SandboxSessionState -from agents.sandbox.snapshot import NoopSnapshot -from agents.sandbox.workspace_paths import SandboxPathGrant -from pydantic import BaseModel, TypeAdapter -from pydantic_core import SchemaSerializer -from pydantic_core.core_schema import any_schema - -from temporalio.contrib.openai_agents import ( - AllowAllWorkerEnvVars, - OpenAIPayloadConverter, - TemporalWorkerEnvValue, -) -from temporalio.contrib.openai_agents.sandbox._temporal_activity_models import ExecArgs -from temporalio.contrib.openai_agents.sandbox._temporal_worker_env_value import ( - _resolvable_worker_env_vars_scope, -) -from temporalio.exceptions import ApplicationError - -SECRET = "sk-not-in-history-1234567890" -NAME = "TEST_WORKER_ENV_VALUE_NAME" - - -class _EnvValueSessionState(SandboxSessionState): - type: Literal["env_value_test"] = "env_value_test" # type: ignore[assignment] - - -def _payload_bytes(value: BaseModel) -> bytes: - payload = OpenAIPayloadConverter().to_payload(value) - assert payload is not None - return payload.data - - -def _round_trip(value: BaseModel, type_hint: type) -> Any: - converter = OpenAIPayloadConverter() - payload = converter.to_payload(value) - assert payload is not None - return converter.from_payload(payload, type_hint) - - -def _manifest(env: dict[str, Any]) -> Manifest: - return Manifest(environment=Environment(value=env)) - - -def _state(manifest: Manifest) -> _EnvValueSessionState: - return _EnvValueSessionState( - manifest=manifest, snapshot=NoopSnapshot(id=str(uuid.uuid4())) - ) - - -def test_literal_env_value_is_written_into_the_payload() -> None: - raw = _payload_bytes(_manifest({NAME: SECRET})) - assert SECRET.encode() in raw - - -def test_worker_env_value_round_trips_without_the_value() -> None: - raw = _payload_bytes(_manifest({NAME: TemporalWorkerEnvValue(name=NAME)})) - assert SECRET.encode() not in raw - assert b"temporal.worker_env_value" in raw - - back = _round_trip(_manifest({NAME: TemporalWorkerEnvValue(name=NAME)}), Manifest) - value = back.environment.value[NAME] - assert isinstance(value, TemporalWorkerEnvValue) - assert value.name == NAME - - -def test_worker_env_value_round_trips_inside_an_env_entry() -> None: - manifest = _manifest({NAME: EnvEntry(value=TemporalWorkerEnvValue(name=NAME))}) - raw = _payload_bytes(manifest) - assert SECRET.encode() not in raw - assert b"temporal.worker_env_value" in raw - - back = _round_trip(manifest, Manifest) - entry = back.environment.value[NAME] - assert isinstance(entry, EnvEntry) - assert isinstance(entry.value, TemporalWorkerEnvValue) - assert entry.value.name == NAME - - -def test_worker_env_value_survives_the_durable_activity_path() -> None: - args = ExecArgs( - state=_state(_manifest({NAME: TemporalWorkerEnvValue(name=NAME)})), - command=["ls"], - ) - raw = _payload_bytes(args) - assert SECRET.encode() not in raw - - back = _round_trip(args, ExecArgs) - value = back.state.manifest.environment.value[NAME] - assert isinstance(value, TemporalWorkerEnvValue) - assert value.name == NAME - - -def test_literal_env_values_are_untouched_alongside_a_worker_env_value() -> None: - manifest = _manifest( - {NAME: TemporalWorkerEnvValue(name=NAME), "REGION": "us-west-2"} - ) - back = _round_trip(manifest, Manifest) - - assert isinstance(back.environment.value[NAME], TemporalWorkerEnvValue) - assert back.environment.value["REGION"] == "us-west-2" - - normalized = back.environment.normalized() - assert isinstance(normalized["REGION"].value, StrEnvValue) - assert normalized["REGION"].value.value == "us-west-2" - - -def test_discriminator_survives_exclude_unset() -> None: - """``type`` is a class default, so ``exclude_unset=True`` must not drop it.""" - serializer = SchemaSerializer(any_schema()) - raw = serializer.to_json( - _manifest({NAME: TemporalWorkerEnvValue(name=NAME)}), exclude_unset=True - ) - assert b"temporal.worker_env_value" in raw - - back = TypeAdapter(Manifest).validate_json(raw) - assert isinstance(back.environment.value[NAME], TemporalWorkerEnvValue) - - -def test_a_host_path_grant_would_reach_the_payload_unprotected() -> None: - """Why host-path grants are refused: nothing keeps the host path out of history.""" - manifest = Manifest( - extra_path_grants=( - SandboxPathGrant(path="/workspace/shared", host_path="/host/private-dir"), - ) - ) - assert b"/host/private-dir" in _payload_bytes(manifest) - assert b"/host/private-dir" in _payload_bytes(_state(manifest)) - - -def test_worker_env_value_tag_is_namespaced() -> None: - """Upstream raises on a duplicate tag, so the namespace keeps it registrable.""" - tag = TemporalWorkerEnvValue(name=NAME).type - assert tag.startswith("temporal.") - assert EnvValue._subclass_registry[tag] is TemporalWorkerEnvValue - - -async def test_resolve_reads_the_worker_process_environment( - monkeypatch: pytest.MonkeyPatch, -) -> None: - monkeypatch.setenv(NAME, SECRET) - with _resolvable_worker_env_vars_scope([NAME]): - assert await TemporalWorkerEnvValue(name=NAME).resolve() == SECRET - - -async def test_resolve_raises_naming_the_variable_when_unset( - monkeypatch: pytest.MonkeyPatch, -) -> None: - monkeypatch.delenv(NAME, raising=False) - with pytest.raises(ApplicationError) as exc_info: - with _resolvable_worker_env_vars_scope([NAME]): - await TemporalWorkerEnvValue(name=NAME).resolve() - - assert NAME in str(exc_info.value) - # An allowed-but-unset variable must not read as a denial. - assert "resolvable_worker_env_vars" not in str(exc_info.value) - assert exc_info.value.type == "TemporalWorkerEnvValueUnresolved" - assert exc_info.value.non_retryable - - -async def test_resolve_raises_when_the_variable_is_set_but_empty( - monkeypatch: pytest.MonkeyPatch, -) -> None: - monkeypatch.setenv(NAME, "") - with pytest.raises(ApplicationError) as exc_info: - with _resolvable_worker_env_vars_scope([NAME]): - await TemporalWorkerEnvValue(name=NAME).resolve() - - assert NAME in str(exc_info.value) - assert exc_info.value.type == "TemporalWorkerEnvValueUnresolved" - assert exc_info.value.non_retryable - - -async def test_resolve_refuses_to_run_on_the_workflow_thread( - monkeypatch: pytest.MonkeyPatch, -) -> None: - monkeypatch.setenv(NAME, SECRET) - monkeypatch.setattr("temporalio.workflow.in_workflow", lambda: True) - with pytest.raises(ApplicationError) as exc_info: - with _resolvable_worker_env_vars_scope([NAME]): - await TemporalWorkerEnvValue(name=NAME).resolve() - - assert exc_info.value.non_retryable - assert SECRET not in str(exc_info.value) - - -async def test_resolve_refuses_a_name_the_worker_does_not_list( - monkeypatch: pytest.MonkeyPatch, -) -> None: - monkeypatch.setenv(NAME, SECRET) - with pytest.raises(ApplicationError) as exc_info: - with _resolvable_worker_env_vars_scope(["SOMETHING_ELSE"]): - await TemporalWorkerEnvValue(name=NAME).resolve() - - assert NAME in str(exc_info.value) - assert "resolvable_worker_env_vars" in str(exc_info.value) - assert SECRET not in str(exc_info.value) - assert exc_info.value.type == "TemporalWorkerEnvValueUnresolved" - assert exc_info.value.non_retryable - - -async def test_resolve_refuses_outside_a_sandbox_activity( - monkeypatch: pytest.MonkeyPatch, -) -> None: - monkeypatch.setenv(NAME, SECRET) - with pytest.raises(ApplicationError) as exc_info: - await TemporalWorkerEnvValue(name=NAME).resolve() - - assert "resolvable_worker_env_vars" in str(exc_info.value) - - -async def test_allow_all_makes_an_unlisted_name_resolvable( - monkeypatch: pytest.MonkeyPatch, -) -> None: - monkeypatch.setenv("A_NAME_LISTED_NOWHERE", SECRET) - value = TemporalWorkerEnvValue(name="A_NAME_LISTED_NOWHERE") - - with _resolvable_worker_env_vars_scope(AllowAllWorkerEnvVars()): - assert await value.resolve() == SECRET - - -async def test_a_literal_star_in_the_resolvable_names_matches_no_name( - monkeypatch: pytest.MonkeyPatch, -) -> None: - monkeypatch.setenv("A_NAME_LISTED_NOWHERE", SECRET) - value = TemporalWorkerEnvValue(name="A_NAME_LISTED_NOWHERE") - - with pytest.raises(ApplicationError): - with _resolvable_worker_env_vars_scope(["*"]): - await value.resolve() - - -async def test_a_glob_in_the_resolvable_names_matches_no_name( - monkeypatch: pytest.MonkeyPatch, -) -> None: - monkeypatch.setenv(NAME, SECRET) - with pytest.raises(ApplicationError) as exc_info: - with _resolvable_worker_env_vars_scope(["TEST_WORKER_ENV_VALUE_*"]): - await TemporalWorkerEnvValue(name=NAME).resolve() - - assert "resolvable_worker_env_vars" in str(exc_info.value) - - -async def test_each_env_value_resolves_its_own_variable_under_its_own_name( - monkeypatch: pytest.MonkeyPatch, -) -> None: - monkeypatch.setenv("WORKER_PRIMARY", "primary-secret") - monkeypatch.setenv("WORKER_SECONDARY", "secondary-secret") - manifest = _manifest( - { - "REGION": "us-west-2", - "SANDBOX_PRIMARY": TemporalWorkerEnvValue(name="WORKER_PRIMARY"), - "LOG_LEVEL": "debug", - "SANDBOX_SECONDARY": TemporalWorkerEnvValue(name="WORKER_SECONDARY"), - } - ) - - with _resolvable_worker_env_vars_scope(["WORKER_PRIMARY", "WORKER_SECONDARY"]): - assert await manifest.environment.resolve() == { - "REGION": "us-west-2", - "SANDBOX_PRIMARY": "primary-secret", - "LOG_LEVEL": "debug", - "SANDBOX_SECONDARY": "secondary-secret", - } - - raw = _payload_bytes(manifest) - for secret in (b"primary-secret", b"secondary-secret"): - assert secret not in raw - for name in (b"WORKER_PRIMARY", b"WORKER_SECONDARY"): - assert name in raw - - -async def test_environment_resolve_leaves_the_manifest_unresolved( - monkeypatch: pytest.MonkeyPatch, -) -> None: - monkeypatch.setenv(NAME, SECRET) - manifest = _manifest( - {NAME: TemporalWorkerEnvValue(name=NAME), "REGION": "us-west-2"} - ) - - with _resolvable_worker_env_vars_scope([NAME]): - assert await manifest.environment.resolve() == { - NAME: SECRET, - "REGION": "us-west-2", - } - - assert isinstance(manifest.environment.value[NAME], TemporalWorkerEnvValue) - assert SECRET.encode() not in _payload_bytes(manifest) diff --git a/tests/contrib/openai_agents/test_openai_streaming.py b/tests/contrib/openai_agents/test_openai_streaming.py deleted file mode 100644 index ab711cd86..000000000 --- a/tests/contrib/openai_agents/test_openai_streaming.py +++ /dev/null @@ -1,350 +0,0 @@ -"""Integration tests for OpenAI Agents streaming support. - -Streaming is opt-in via ``Runner.run_streamed``. Events flow back to the -workflow through ``RunResultStreaming.stream_events()`` (in batch after -each model activity completes) and to external consumers in real time -via the configured stream topic. -""" - -import asyncio -import logging -import uuid -from collections.abc import AsyncIterator -from datetime import timedelta -from typing import Any - -import pytest -from agents import ( - Agent, - AgentOutputSchemaBase, - Handoff, - Model, - ModelResponse, - ModelSettings, - ModelTracing, - Runner, - Tool, - TResponseInputItem, - Usage, -) -from agents.items import TResponseStreamEvent -from openai.types.responses import ( - Response, - ResponseCompletedEvent, - ResponseOutputMessage, - ResponseOutputText, - ResponseTextConfig, - ResponseTextDeltaEvent, - ResponseUsage, -) -from openai.types.responses.response_usage import ( - InputTokensDetails, - OutputTokensDetails, -) -from openai.types.shared.response_format_text import ResponseFormatText - -from temporalio import workflow -from temporalio.client import Client, WorkflowFailureError -from temporalio.contrib.openai_agents import ModelActivityParameters -from temporalio.contrib.openai_agents.testing import AgentEnvironment -from temporalio.contrib.workflow_streams import WorkflowStream, WorkflowStreamClient -from tests.helpers import new_worker - -logger = logging.getLogger(__name__) - - -class StreamingTestModel(Model): - """Test model that yields text deltas followed by a ResponseCompletedEvent.""" - - __test__ = False - - async def get_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, - ) -> ModelResponse: - return ModelResponse( - output=[ - ResponseOutputMessage( - id="msg_test", - content=[ - ResponseOutputText( - text="Hello world!", - annotations=[], - type="output_text", - logprobs=[], - ) - ], - role="assistant", - status="completed", - type="message", - ) - ], - usage=Usage(), - response_id=None, - ) - - 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]: - # Yield text deltas - yield ResponseTextDeltaEvent( - content_index=0, - delta="Hello ", - item_id="item1", - output_index=0, - sequence_number=0, - type="response.output_text.delta", - logprobs=[], - ) - yield ResponseTextDeltaEvent( - content_index=0, - delta="world!", - item_id="item1", - output_index=0, - sequence_number=1, - type="response.output_text.delta", - logprobs=[], - ) - - # Yield the final completed event - response = Response( - id="resp_test", - created_at=0, - error=None, - incomplete_details=None, - instructions=None, - metadata={}, - model="test", - object="response", - output=[ - ResponseOutputMessage( - id="msg_test", - content=[ - ResponseOutputText( - text="Hello world!", - annotations=[], - type="output_text", - logprobs=[], - ) - ], - role="assistant", - status="completed", - type="message", - ) - ], - parallel_tool_calls=True, - temperature=1.0, - tool_choice="auto", - tools=[], - top_p=1.0, - status="completed", - text=ResponseTextConfig(format=ResponseFormatText(type="text")), - truncation="disabled", - usage=ResponseUsage( - input_tokens=10, - output_tokens=5, - total_tokens=15, - input_tokens_details=InputTokensDetails.model_validate( - {"cached_tokens": 0, "cache_write_tokens": 0} - ), - output_tokens_details=OutputTokensDetails(reasoning_tokens=0), - ), - ) - yield ResponseCompletedEvent( - response=response, sequence_number=2, type="response.completed" - ) - - -@workflow.defn -class StreamingOpenAIWorkflow: - """Test workflow that opts into streaming via ``Runner.run_streamed``. - - Workflow code consumes events from ``stream_events()`` and exposes - the seen event types via a query so the test can verify both the - in-workflow iteration and the external stream subscriber observe the - same events. - """ - - @workflow.init - def __init__(self, prompt: str) -> None: - self.stream = WorkflowStream() - self.workflow_event_types: list[str] = [] - - @workflow.run - async def run(self, prompt: str) -> str: - agent = Agent[None]( - name="Assistant", - instructions="You are a test agent.", - ) - result = Runner.run_streamed(starting_agent=agent, input=prompt) - async for event in result.stream_events(): - raw = getattr(event, "data", None) - event_type = getattr(raw, "type", None) - if event_type is not None: - self.workflow_event_types.append(event_type) - return result.final_output - - @workflow.query - def get_workflow_event_types(self) -> list[str]: - return self.workflow_event_types - - -@workflow.defn -class StreamingRequiresTopicWorkflow: - """Workflow that opts into ``Runner.run_streamed`` while the model - plugin was configured without a ``streaming_topic``. - - The stub raises before scheduling the streaming activity; this - propagates out of ``Runner.run_streamed`` and fails the workflow. - """ - - @workflow.run - async def run(self, prompt: str) -> str: - agent = Agent[None]( - name="Assistant", - instructions="You are a test agent.", - ) - result = Runner.run_streamed(starting_agent=agent, input=prompt) - async for _ in result.stream_events(): - pass - return result.final_output - - -@pytest.mark.asyncio -async def test_streaming_publishes_raw_events(client: Client): - """Both the workflow consumer (via stream_events) and the stream - topic see the same native OpenAI events, in order, with no - normalization.""" - async with AgentEnvironment( - model=StreamingTestModel(), - model_params=ModelActivityParameters( - start_to_close_timeout=timedelta(seconds=30), - streaming_topic="events", - ), - ) as env: - client = env.applied_on_client(client) - workflow_id = f"openai-streaming-test-{uuid.uuid4()}" - - async with new_worker( - client, StreamingOpenAIWorkflow, max_cached_workflows=0 - ) as worker: - handle = await client.start_workflow( - StreamingOpenAIWorkflow.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, - # TResponseStreamEvent is a discriminated union - # (Annotated[..., Discriminator]); Pydantic decodes - # it via TypeAdapter at runtime, but the type - # checkers see ``Annotated`` rather than ``type``. - result_type=TResponseStreamEvent, # type: ignore[arg-type,call-overload] - poll_cooldown=timedelta(milliseconds=50), - ): - published.append(item.data) - if item.data.type == "response.completed": - break - - collect_task = asyncio.create_task(collect_events()) - result = await handle.result() - await asyncio.wait_for(collect_task, timeout=10.0) - - workflow_event_types = await handle.query( - StreamingOpenAIWorkflow.get_workflow_event_types - ) - - assert result == "Hello world!" - - published_types = [e.type for e in published] - assert published_types == [ - "response.output_text.delta", - "response.output_text.delta", - "response.completed", - ], f"Unexpected published event sequence: {published_types}" - - deltas = [e.delta for e in published if e.type == "response.output_text.delta"] - assert deltas == ["Hello ", "world!"] - - # Workflow-side iteration sees the same model events in the same order. - assert workflow_event_types == published_types - - -@pytest.mark.asyncio -async def test_streaming_requires_topic(client: Client): - """``Runner.run_streamed`` fails fast when the plugin has no topic - configured. The error is raised in ``stream_response`` before any - streaming activity is scheduled.""" - async with AgentEnvironment( - model=StreamingTestModel(), - model_params=ModelActivityParameters( - start_to_close_timeout=timedelta(seconds=30), - streaming_topic=None, - ), - ) as env: - client = env.applied_on_client(client) - async with new_worker( - client, StreamingRequiresTopicWorkflow, max_cached_workflows=0 - ) as worker: - with pytest.raises(WorkflowFailureError) as exc_info: - await client.execute_workflow( - StreamingRequiresTopicWorkflow.run, - "Hi", - id=f"openai-streaming-requires-topic-{uuid.uuid4()}", - task_queue=worker.task_queue, - execution_timeout=timedelta(seconds=30), - ) - - assert "streaming_topic" in str(exc_info.value.cause) - - -@pytest.mark.asyncio -async def test_streaming_rejects_local_activity(client: Client): - """``Runner.run_streamed`` fails fast when the plugin is configured - with ``use_local_activity=True``. Local activities support neither - heartbeats nor the workflow-stream signal channel.""" - async with AgentEnvironment( - model=StreamingTestModel(), - model_params=ModelActivityParameters( - start_to_close_timeout=timedelta(seconds=30), - streaming_topic="events", - use_local_activity=True, - ), - ) as env: - client = env.applied_on_client(client) - async with new_worker( - client, StreamingRequiresTopicWorkflow, max_cached_workflows=0 - ) as worker: - with pytest.raises(WorkflowFailureError) as exc_info: - await client.execute_workflow( - StreamingRequiresTopicWorkflow.run, - "Hi", - id=f"openai-streaming-rejects-local-{uuid.uuid4()}", - task_queue=worker.task_queue, - execution_timeout=timedelta(seconds=30), - ) - - assert "use_local_activity" in str(exc_info.value.cause) diff --git a/tests/contrib/openai_agents/test_openai_tool_secrets.py b/tests/contrib/openai_agents/test_openai_tool_secrets.py deleted file mode 100644 index 949b232e4..000000000 --- a/tests/contrib/openai_agents/test_openai_tool_secrets.py +++ /dev/null @@ -1,908 +0,0 @@ -"""Tests for worker environment references in hosted tool secrets.""" - -import time -import uuid -from collections.abc import AsyncIterator, Collection -from typing import Any, cast - -import pytest -from agents import ( - Agent, - AgentOutputSchemaBase, - CodeInterpreterTool, - Handoff, - HostedMCPTool, - Model, - ModelResponse, - ModelSettings, - ModelTracing, - Runner, - Tool, - TResponseInputItem, - Usage, -) -from agents.items import TResponseStreamEvent -from agents.tool import ShellTool, ShellToolEnvironment - -from temporalio import workflow -from temporalio.client import Client -from temporalio.contrib.openai_agents import ( - ModelActivityParameters, - OpenAIAgentsPlugin, - OpenAIPayloadConverter, - temporal_worker_env_ref, -) -from temporalio.contrib.openai_agents._invoke_model_activity import ( - ActivityModelInput, - ModelActivity, - StreamingActivityModelInput, - _build_tool, -) -from temporalio.contrib.openai_agents._temporal_model_stub import _TemporalModelStub -from temporalio.contrib.openai_agents._temporal_worker_env_ref import ( - AllowAllWorkerEnvVars, - _WorkerEnvRefResolver, -) -from temporalio.contrib.openai_agents.testing import ( - AgentEnvironment, - TestModelProvider, -) -from temporalio.testing import ActivityEnvironment -from tests.helpers import new_worker - -SENTINEL = "sk-test-sentinel-4f1a9c7e2b" -ENV_NAME = "TEMPORAL_TEST_TOOL_SECRET" -OTHER_SENTINEL = "sk-test-other-8c3d5e0a1f" -OTHER_ENV_NAME = "TEMPORAL_TEST_OTHER_TOOL_SECRET" - -_RESOLVER_ALLOWING_TEST_NAMES = _WorkerEnvRefResolver([ENV_NAME, OTHER_ENV_NAME]) - - -def _round_trip_activity_input(tool: Tool) -> tuple[bytes, ActivityModelInput]: - stub = _TemporalModelStub( - model_name="gpt-5", - model_params=ModelActivityParameters(), - agent=None, - ) - activity_input, _summary = stub._build_activity_input( - system_instructions=None, - input="hi", - model_settings=ModelSettings(), - tools=[tool], - output_schema=None, - handoffs=[], - tracing=ModelTracing.DISABLED, - previous_response_id=None, - conversation_id=None, - prompt=None, - ) - converter = OpenAIPayloadConverter() - payload = converter.to_payload(activity_input) - return payload.data, converter.from_payload(payload, ActivityModelInput) - - -def _activity_input_payload_and_tool(tool: Tool) -> tuple[bytes, Any]: - payload, received = _round_trip_activity_input(tool) - tools = received.get("tools") or [] - assert len(tools) == 1 - return payload, tools[0] - - -def _hosted_mcp_tool(authorization: str, header_value: str) -> HostedMCPTool: - return HostedMCPTool( - tool_config={ - "type": "mcp", - "server_label": "test_server", - "server_url": "https://example.com/mcp", - "authorization": authorization, - "headers": {"X-Token": header_value, "X-Plain": "not-a-secret"}, - } - ) - - -def _domain_secret(name: str, value: str) -> dict[str, str]: - return {"domain": "example.com", "name": name, "value": value} - - -def _network_policy(domain_secrets: tuple[Any, ...]) -> Any: - return { - "type": "allowlist", - "allowed_domains": ["example.com"], - "domain_secrets": list(domain_secrets), - } - - -def _shell_tool(*domain_secrets: Any) -> ShellTool: - environment: Any = { - "type": "container_auto", - "network_policy": _network_policy(domain_secrets), - } - return ShellTool(environment=environment) - - -def _code_interpreter_tool(*domain_secrets: Any) -> CodeInterpreterTool: - tool_config: Any = { - "type": "code_interpreter", - "container": { - "type": "auto", - "network_policy": _network_policy(domain_secrets), - }, - } - return CodeInterpreterTool(tool_config=tool_config) - - -def _as_dict(value: Any) -> dict[str, Any]: - return cast(dict[str, Any], value) - - -def _secrets_in(network_policy: Any) -> list[Any]: - return list(_as_dict(network_policy)["domain_secrets"]) - - -def _shell_secrets(built: Tool) -> list[Any]: - assert isinstance(built, ShellTool) - assert built.environment is not None - return _secrets_in(_as_dict(built.environment)["network_policy"]) - - -def _code_interpreter_secrets(built: Tool) -> list[Any]: - assert isinstance(built, CodeInterpreterTool) - container = _as_dict(built.tool_config)["container"] - return _secrets_in(_as_dict(container)["network_policy"]) - - -def test_hosted_mcp_secrets_stay_out_of_activity_arguments( - monkeypatch: pytest.MonkeyPatch, -): - monkeypatch.setenv(ENV_NAME, SENTINEL) - ref = temporal_worker_env_ref(ENV_NAME) - - payload, received = _activity_input_payload_and_tool(_hosted_mcp_tool(ref, ref)) - - assert SENTINEL.encode() not in payload - assert payload.count(ref.encode()) == 2 - assert received.tool_config["authorization"] == ref - assert received.tool_config["headers"]["X-Token"] == ref - - -def test_hosted_shell_domain_secret_stays_out_of_activity_arguments( - monkeypatch: pytest.MonkeyPatch, -): - monkeypatch.setenv(ENV_NAME, SENTINEL) - ref = temporal_worker_env_ref(ENV_NAME) - - payload, received = _activity_input_payload_and_tool( - _shell_tool(_domain_secret("TOKEN", ref)) - ) - - assert SENTINEL.encode() not in payload - assert ref.encode() in payload - secrets = _secrets_in(received.environment["network_policy"]) - assert secrets[0]["value"] == ref - - -def test_code_interpreter_domain_secret_stays_out_of_activity_arguments( - monkeypatch: pytest.MonkeyPatch, -): - monkeypatch.setenv(ENV_NAME, SENTINEL) - ref = temporal_worker_env_ref(ENV_NAME) - - payload, received = _activity_input_payload_and_tool( - _code_interpreter_tool(_domain_secret("TOKEN", ref)) - ) - - assert SENTINEL.encode() not in payload - assert ref.encode() in payload - secrets = _secrets_in(received.tool_config["container"]["network_policy"]) - assert secrets[0]["value"] == ref - - -def test_hosted_mcp_secrets_resolve_for_the_model_call( - monkeypatch: pytest.MonkeyPatch, -): - monkeypatch.setenv(ENV_NAME, SENTINEL) - ref = temporal_worker_env_ref(ENV_NAME) - _payload, received = _activity_input_payload_and_tool(_hosted_mcp_tool(ref, ref)) - - built = _build_tool(received, _RESOLVER_ALLOWING_TEST_NAMES) - - assert isinstance(built, HostedMCPTool) - assert _as_dict(built.tool_config)["authorization"] == SENTINEL - assert _as_dict(built.tool_config)["headers"] == { - "X-Token": SENTINEL, - "X-Plain": "not-a-secret", - } - assert received.tool_config["authorization"] == ref - assert received.tool_config["headers"]["X-Token"] == ref - - -@pytest.mark.parametrize( - ("resolvable", "resolves"), - [ - ([ENV_NAME], True), - (AllowAllWorkerEnvVars(), True), - ([OTHER_ENV_NAME], False), - ], - ids=["the_name", "allow_all", "another_name"], -) -def test_a_reference_resolves_only_from_a_variable_the_worker_allows( - monkeypatch: pytest.MonkeyPatch, - resolvable: Collection[str] | AllowAllWorkerEnvVars, - resolves: bool, -): - monkeypatch.setenv(ENV_NAME, SENTINEL) - ref = temporal_worker_env_ref(ENV_NAME) - _payload, received = _activity_input_payload_and_tool(_hosted_mcp_tool(ref, ref)) - - built = _build_tool(received, _WorkerEnvRefResolver(resolvable)) - - assert isinstance(built, HostedMCPTool) - expected = SENTINEL if resolves else ref - assert _as_dict(built.tool_config)["authorization"] == expected - assert _as_dict(built.tool_config)["headers"]["X-Token"] == expected - - -def test_a_star_in_the_resolvable_names_is_an_ordinary_name( - monkeypatch: pytest.MonkeyPatch, -): - monkeypatch.setenv(ENV_NAME, SENTINEL) - ref = temporal_worker_env_ref(ENV_NAME) - _payload, received = _activity_input_payload_and_tool(_hosted_mcp_tool(ref, ref)) - - built = _build_tool(received, _WorkerEnvRefResolver([OTHER_ENV_NAME, "*"])) - - assert isinstance(built, HostedMCPTool) - assert _as_dict(built.tool_config)["authorization"] == ref - assert _as_dict(built.tool_config)["headers"]["X-Token"] == ref - - -def test_a_glob_in_the_resolvable_names_matches_no_name( - monkeypatch: pytest.MonkeyPatch, -): - monkeypatch.setenv(ENV_NAME, SENTINEL) - ref = temporal_worker_env_ref(ENV_NAME) - _payload, received = _activity_input_payload_and_tool(_hosted_mcp_tool(ref, ref)) - - built = _build_tool(received, _WorkerEnvRefResolver(["TEMPORAL_TEST_*"])) - - assert isinstance(built, HostedMCPTool) - assert _as_dict(built.tool_config)["authorization"] == ref - assert _as_dict(built.tool_config)["headers"]["X-Token"] == ref - - -def test_hosted_shell_domain_secret_resolves_for_the_model_call( - monkeypatch: pytest.MonkeyPatch, -): - monkeypatch.setenv(ENV_NAME, SENTINEL) - ref = temporal_worker_env_ref(ENV_NAME) - _payload, received = _activity_input_payload_and_tool( - _shell_tool(_domain_secret("TOKEN", ref)) - ) - - built = _build_tool(received, _RESOLVER_ALLOWING_TEST_NAMES) - - assert _shell_secrets(built) == [_domain_secret("TOKEN", SENTINEL)] - - -def test_code_interpreter_domain_secret_resolves_for_the_model_call( - monkeypatch: pytest.MonkeyPatch, -): - monkeypatch.setenv(ENV_NAME, SENTINEL) - ref = temporal_worker_env_ref(ENV_NAME) - _payload, received = _activity_input_payload_and_tool( - _code_interpreter_tool(_domain_secret("TOKEN", ref)) - ) - - built = _build_tool(received, _RESOLVER_ALLOWING_TEST_NAMES) - - assert _code_interpreter_secrets(built) == [_domain_secret("TOKEN", SENTINEL)] - - -def test_code_interpreter_domain_secret_survives_a_second_build( - monkeypatch: pytest.MonkeyPatch, -): - monkeypatch.setenv(ENV_NAME, SENTINEL) - ref = temporal_worker_env_ref(ENV_NAME) - _payload, received = _activity_input_payload_and_tool( - _code_interpreter_tool(_domain_secret("TOKEN", ref)) - ) - - first = _build_tool(received, _RESOLVER_ALLOWING_TEST_NAMES) - second = _build_tool(received, _RESOLVER_ALLOWING_TEST_NAMES) - - assert _code_interpreter_secrets(first) == [_domain_secret("TOKEN", SENTINEL)] - assert _code_interpreter_secrets(second) == [_domain_secret("TOKEN", SENTINEL)] - assert _secrets_in(received.tool_config["container"]["network_policy"]) == [ - _domain_secret("TOKEN", ref) - ] - - -def test_only_domain_secrets_holding_a_worker_env_ref_are_resolved( - monkeypatch: pytest.MonkeyPatch, -): - monkeypatch.setenv(ENV_NAME, SENTINEL) - ref = temporal_worker_env_ref(ENV_NAME) - literal = _domain_secret("PLAIN", "plain-token-value") - _payload, received = _activity_input_payload_and_tool( - _code_interpreter_tool(literal, _domain_secret("TOKEN", ref)) - ) - - built = _build_tool(received, _RESOLVER_ALLOWING_TEST_NAMES) - - assert _code_interpreter_secrets(built) == [ - literal, - _domain_secret("TOKEN", SENTINEL), - ] - - -def test_two_domain_secrets_resolve_to_their_own_secrets( - monkeypatch: pytest.MonkeyPatch, -): - monkeypatch.setenv(ENV_NAME, SENTINEL) - monkeypatch.setenv(OTHER_ENV_NAME, OTHER_SENTINEL) - _payload, received = _activity_input_payload_and_tool( - _code_interpreter_tool( - _domain_secret("TOKEN", temporal_worker_env_ref(ENV_NAME)), - _domain_secret("OTHER", temporal_worker_env_ref(OTHER_ENV_NAME)), - ) - ) - - built = _build_tool(received, _RESOLVER_ALLOWING_TEST_NAMES) - - assert _code_interpreter_secrets(built) == [ - _domain_secret("TOKEN", SENTINEL), - _domain_secret("OTHER", OTHER_SENTINEL), - ] - - -def test_shell_domain_secrets_resolve_to_their_own_secrets_in_order( - monkeypatch: pytest.MonkeyPatch, -): - monkeypatch.setenv(ENV_NAME, SENTINEL) - monkeypatch.setenv(OTHER_ENV_NAME, OTHER_SENTINEL) - literal = _domain_secret("PLAIN", "plain-token-value") - _payload, received = _activity_input_payload_and_tool( - _shell_tool( - literal, - _domain_secret("TOKEN", temporal_worker_env_ref(ENV_NAME)), - _domain_secret("OTHER", temporal_worker_env_ref(OTHER_ENV_NAME)), - ) - ) - - built = _build_tool(received, _RESOLVER_ALLOWING_TEST_NAMES) - - assert _shell_secrets(built) == [ - literal, - _domain_secret("TOKEN", SENTINEL), - _domain_secret("OTHER", OTHER_SENTINEL), - ] - - -def test_a_shell_domain_secret_naming_a_denied_variable_is_passed_through( - monkeypatch: pytest.MonkeyPatch, -): - monkeypatch.setenv(OTHER_ENV_NAME, OTHER_SENTINEL) - denied = temporal_worker_env_ref(OTHER_ENV_NAME) - _payload, received = _activity_input_payload_and_tool( - _shell_tool(_domain_secret("TOKEN", denied)) - ) - - built = _build_tool(received, _WorkerEnvRefResolver([ENV_NAME])) - - assert _shell_secrets(built) == [_domain_secret("TOKEN", denied)] - - -def test_a_code_interpreter_domain_secret_naming_a_denied_variable_is_passed_through( - monkeypatch: pytest.MonkeyPatch, -): - monkeypatch.setenv(OTHER_ENV_NAME, OTHER_SENTINEL) - denied = temporal_worker_env_ref(OTHER_ENV_NAME) - _payload, received = _activity_input_payload_and_tool( - _code_interpreter_tool(_domain_secret("TOKEN", denied)) - ) - - built = _build_tool(received, _WorkerEnvRefResolver([ENV_NAME])) - - assert _code_interpreter_secrets(built) == [_domain_secret("TOKEN", denied)] - - -def test_two_worker_env_refs_in_one_mcp_config_resolve_to_their_own_secrets( - monkeypatch: pytest.MonkeyPatch, -): - monkeypatch.setenv(ENV_NAME, SENTINEL) - monkeypatch.setenv(OTHER_ENV_NAME, OTHER_SENTINEL) - _payload, received = _activity_input_payload_and_tool( - _hosted_mcp_tool( - temporal_worker_env_ref(ENV_NAME), temporal_worker_env_ref(OTHER_ENV_NAME) - ) - ) - - built = _build_tool(received, _RESOLVER_ALLOWING_TEST_NAMES) - - assert isinstance(built, HostedMCPTool) - assert _as_dict(built.tool_config)["authorization"] == SENTINEL - assert _as_dict(built.tool_config)["headers"] == { - "X-Token": OTHER_SENTINEL, - "X-Plain": "not-a-secret", - } - - -def test_a_worker_env_ref_in_a_header_name_is_passed_through( - monkeypatch: pytest.MonkeyPatch, -): - monkeypatch.setenv(ENV_NAME, SENTINEL) - ref = temporal_worker_env_ref(ENV_NAME) - tool_config: Any = { - "type": "mcp", - "server_label": "test_server", - "server_url": "https://example.com/mcp", - "headers": {ref: "not-a-secret"}, - } - - payload, received = _activity_input_payload_and_tool( - HostedMCPTool(tool_config=tool_config) - ) - - built = _build_tool(received, _RESOLVER_ALLOWING_TEST_NAMES) - - assert isinstance(built, HostedMCPTool) - assert _as_dict(built.tool_config)["headers"] == {ref: "not-a-secret"} - assert SENTINEL.encode() not in payload - - -def test_an_mcp_config_without_an_authorization_resolves_its_headers( - monkeypatch: pytest.MonkeyPatch, -): - monkeypatch.setenv(ENV_NAME, SENTINEL) - tool_config: Any = { - "type": "mcp", - "server_label": "test_server", - "server_url": "https://example.com/mcp", - "headers": {"X-Token": temporal_worker_env_ref(ENV_NAME)}, - } - - _payload, received = _activity_input_payload_and_tool( - HostedMCPTool(tool_config=tool_config) - ) - - built = _build_tool(received, _RESOLVER_ALLOWING_TEST_NAMES) - - assert isinstance(built, HostedMCPTool) - assert "authorization" not in _as_dict(built.tool_config) - assert _as_dict(built.tool_config)["headers"] == {"X-Token": SENTINEL} - - -def test_an_mcp_config_without_headers_resolves_its_authorization( - monkeypatch: pytest.MonkeyPatch, -): - monkeypatch.setenv(ENV_NAME, SENTINEL) - tool_config: Any = { - "type": "mcp", - "server_label": "test_server", - "server_url": "https://example.com/mcp", - "authorization": temporal_worker_env_ref(ENV_NAME), - } - - _payload, received = _activity_input_payload_and_tool( - HostedMCPTool(tool_config=tool_config) - ) - - built = _build_tool(received, _RESOLVER_ALLOWING_TEST_NAMES) - - assert isinstance(built, HostedMCPTool) - assert _as_dict(built.tool_config)["authorization"] == SENTINEL - assert "headers" not in _as_dict(built.tool_config) - - -def test_local_shell_environment_keeps_its_executor(): - _payload, received = _activity_input_payload_and_tool( - ShellTool(environment={"type": "local"}, executor=lambda _request: "") - ) - - built = _build_tool(received, _RESOLVER_ALLOWING_TEST_NAMES) - - assert isinstance(built, ShellTool) - assert built.executor is not None - assert _as_dict(built.environment) == {"type": "local"} - - -@pytest.mark.parametrize( - "environment", - [ - {"type": "container_auto"}, - { - "type": "container_auto", - "network_policy": {"type": "disabled"}, - }, - {"type": "container_reference", "container_id": "cntr_abc"}, - ], - ids=["container_auto", "container_auto_disabled_policy", "container_reference"], -) -def test_hosted_shell_environment_gets_no_executor( - environment: ShellToolEnvironment, -): - _payload, received = _activity_input_payload_and_tool( - ShellTool(environment=environment) - ) - - built = _build_tool(received, _RESOLVER_ALLOWING_TEST_NAMES) - - assert isinstance(built, ShellTool) - assert built.executor is None - assert _as_dict(built.environment) == environment - - -def test_code_interpreter_container_id_is_passed_through(): - _payload, received = _activity_input_payload_and_tool( - CodeInterpreterTool( - tool_config={"type": "code_interpreter", "container": "cntr_abc"} - ) - ) - - built = _build_tool(received, _RESOLVER_ALLOWING_TEST_NAMES) - - assert isinstance(built, CodeInterpreterTool) - assert _as_dict(built.tool_config)["container"] == "cntr_abc" - - -@pytest.mark.parametrize( - "container", - [ - {"type": "auto"}, - {"type": "auto", "network_policy": {"type": "disabled"}}, - ], - ids=["no_policy", "disabled_policy"], -) -def test_code_interpreter_container_without_domain_secrets_is_passed_through( - container: Any, -): - _payload, received = _activity_input_payload_and_tool( - CodeInterpreterTool( - tool_config={"type": "code_interpreter", "container": container} - ) - ) - - built = _build_tool(received, _RESOLVER_ALLOWING_TEST_NAMES) - - assert isinstance(built, CodeInterpreterTool) - assert _as_dict(built.tool_config)["container"] == container - - -def test_an_unset_environment_variable_resolves_to_an_empty_value( - monkeypatch: pytest.MonkeyPatch, -): - monkeypatch.delenv(ENV_NAME, raising=False) - ref = temporal_worker_env_ref(ENV_NAME) - _payload, received = _activity_input_payload_and_tool(_hosted_mcp_tool(ref, ref)) - - built = _build_tool(received, _RESOLVER_ALLOWING_TEST_NAMES) - - assert isinstance(built, HostedMCPTool) - assert _as_dict(built.tool_config)["authorization"] == "" - assert _as_dict(built.tool_config)["headers"]["X-Token"] == "" - - -def test_a_policy_with_no_domain_secrets_is_passed_through(): - policy: Any = {"type": "allowlist", "allowed_domains": ["example.com"]} - _payload, received = _activity_input_payload_and_tool( - CodeInterpreterTool( - tool_config={ - "type": "code_interpreter", - "container": {"type": "auto", "network_policy": policy}, - } - ) - ) - - built = _build_tool(received, _RESOLVER_ALLOWING_TEST_NAMES) - - assert isinstance(built, CodeInterpreterTool) - assert _as_dict(built.tool_config)["container"] == { - "type": "auto", - "network_policy": policy, - } - - -def test_plain_values_are_passed_through_unchanged( - monkeypatch: pytest.MonkeyPatch, -): - monkeypatch.setenv(ENV_NAME, SENTINEL) - plain = "temporal.worker_env_ref-but-not-quite" - _payload, received = _activity_input_payload_and_tool( - _hosted_mcp_tool(plain, plain) - ) - - built = _build_tool(received, _RESOLVER_ALLOWING_TEST_NAMES) - - assert isinstance(built, HostedMCPTool) - assert _as_dict(built.tool_config)["authorization"] == plain - assert _as_dict(built.tool_config)["headers"] == { - "X-Token": plain, - "X-Plain": "not-a-secret", - } - - -def test_a_worker_env_ref_inside_a_larger_value_is_substituted_in_place( - monkeypatch: pytest.MonkeyPatch, -): - monkeypatch.setenv(ENV_NAME, SENTINEL) - composed = "Bearer " + temporal_worker_env_ref(ENV_NAME) - _payload, received = _activity_input_payload_and_tool( - _hosted_mcp_tool(composed, composed) - ) - - built = _build_tool(received, _RESOLVER_ALLOWING_TEST_NAMES) - - assert isinstance(built, HostedMCPTool) - assert _as_dict(built.tool_config)["authorization"] == f"Bearer {SENTINEL}" - assert _as_dict(built.tool_config)["headers"]["X-Token"] == f"Bearer {SENTINEL}" - - -def test_one_value_holding_two_refs_resolves_only_the_allowed_name( - monkeypatch: pytest.MonkeyPatch, -): - monkeypatch.setenv(ENV_NAME, SENTINEL) - monkeypatch.setenv(OTHER_ENV_NAME, OTHER_SENTINEL) - denied = temporal_worker_env_ref(OTHER_ENV_NAME) - composed = f"{temporal_worker_env_ref(ENV_NAME)} {denied}" - _payload, received = _activity_input_payload_and_tool( - _hosted_mcp_tool(composed, composed) - ) - - built = _build_tool(received, _WorkerEnvRefResolver([ENV_NAME])) - - assert isinstance(built, HostedMCPTool) - assert _as_dict(built.tool_config)["authorization"] == f"{SENTINEL} {denied}" - assert _as_dict(built.tool_config)["headers"]["X-Token"] == f"{SENTINEL} {denied}" - - -def test_a_worker_env_ref_with_no_closing_brace_is_passed_through( - monkeypatch: pytest.MonkeyPatch, -): - monkeypatch.setenv(ENV_NAME, SENTINEL) - truncated = "temporal.worker_env_ref:{" + ENV_NAME - _payload, received = _activity_input_payload_and_tool( - _hosted_mcp_tool(truncated, truncated) - ) - - built = _build_tool(received, _WorkerEnvRefResolver(AllowAllWorkerEnvVars())) - - assert isinstance(built, HostedMCPTool) - assert _as_dict(built.tool_config)["authorization"] == truncated - assert _as_dict(built.tool_config)["headers"]["X-Token"] == truncated - - -def test_a_value_packed_with_unclosed_references_does_not_stall_the_resolver(): - opener = "temporal.worker_env_ref:{" - packed = opener * (1024 * 1024 // len(opener)) - _payload, received = _activity_input_payload_and_tool( - _hosted_mcp_tool(packed, "not-a-secret") - ) - - start = time.monotonic() - built = _build_tool(received, _WorkerEnvRefResolver([])) - elapsed = time.monotonic() - start - - assert isinstance(built, HostedMCPTool) - assert _as_dict(built.tool_config)["authorization"] == packed - assert elapsed < 5.0 - - -def test_a_bare_string_is_rejected_as_the_resolvable_variable_names(): - with pytest.raises(TypeError, match="resolvable_worker_env_vars"): - _WorkerEnvRefResolver(ENV_NAME) - - -def test_the_allow_all_class_itself_is_rejected_as_the_resolvable_variable_names(): - with pytest.raises(TypeError, match=r"AllowAllWorkerEnvVars\(\)"): - _WorkerEnvRefResolver(AllowAllWorkerEnvVars) # type: ignore[arg-type] - - -def test_the_plugin_rejects_a_bare_string_as_the_resolvable_variable_names(): - with pytest.raises(TypeError, match="resolvable_worker_env_vars"): - OpenAIAgentsPlugin(resolvable_worker_env_vars=ENV_NAME) - - -async def _no_stream_events() -> AsyncIterator[TResponseStreamEvent]: - """Publishing an event here makes the flusher retry for ten minutes against a workflow that does not exist.""" - events: list[TResponseStreamEvent] = [] - for event in events: - yield event - - -class _ToolRecordingModel(Model): - def __init__(self) -> None: - self.tools: list[Tool] = [] - - async def get_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, - ) -> ModelResponse: - self.tools = tools - return ModelResponse(output=[], usage=Usage(), response_id=None) - - 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]: - self.tools = tools - return _no_stream_events() - - -def _hosted_mcp_config_the_model_received(model: _ToolRecordingModel) -> dict[str, Any]: - assert len(model.tools) == 1 - tool = model.tools[0] - assert isinstance(tool, HostedMCPTool) - return _as_dict(tool.tool_config) - - -async def test_invoke_model_activity_resolves_tool_secrets( - monkeypatch: pytest.MonkeyPatch, -): - monkeypatch.setenv(ENV_NAME, SENTINEL) - sent = _hosted_mcp_tool(temporal_worker_env_ref(ENV_NAME), "not-a-secret") - _payload, activity_input = _round_trip_activity_input(sent) - model = _ToolRecordingModel() - - await ActivityEnvironment().run( - ModelActivity( - TestModelProvider(model), resolvable_worker_env_vars=[ENV_NAME] - ).invoke_model_activity, - activity_input, - ) - - assert _hosted_mcp_config_the_model_received(model) == { - **_as_dict(sent.tool_config), - "authorization": SENTINEL, - } - - -async def test_invoke_model_activity_resolves_nothing_by_default( - monkeypatch: pytest.MonkeyPatch, -): - monkeypatch.setenv(ENV_NAME, SENTINEL) - sent = _hosted_mcp_tool(temporal_worker_env_ref(ENV_NAME), "not-a-secret") - _payload, activity_input = _round_trip_activity_input(sent) - model = _ToolRecordingModel() - - await ActivityEnvironment().run( - ModelActivity(TestModelProvider(model)).invoke_model_activity, - activity_input, - ) - - assert _hosted_mcp_config_the_model_received(model) == _as_dict(sent.tool_config) - - -async def test_invoke_model_activity_streaming_resolves_tool_secrets( - monkeypatch: pytest.MonkeyPatch, client: Client -): - monkeypatch.setenv(ENV_NAME, SENTINEL) - sent = _hosted_mcp_tool(temporal_worker_env_ref(ENV_NAME), "not-a-secret") - _payload, activity_input = _round_trip_activity_input(sent) - streaming_input: StreamingActivityModelInput = { - **activity_input, - "streaming_topic": "events", - } - model = _ToolRecordingModel() - - await ActivityEnvironment(client).run( - ModelActivity( - TestModelProvider(model), resolvable_worker_env_vars=[ENV_NAME] - ).invoke_model_activity_streaming, - streaming_input, - ) - - assert _hosted_mcp_config_the_model_received(model) == { - **_as_dict(sent.tool_config), - "authorization": SENTINEL, - } - - -@workflow.defn -class WorkerEnvRefAgentWorkflow: - @workflow.run - async def run(self) -> None: - tool_config: Any = { - "type": "mcp", - "server_label": "test_server", - "server_url": "https://example.com/mcp", - "authorization": temporal_worker_env_ref(ENV_NAME), - "headers": {"X-Token": temporal_worker_env_ref(OTHER_ENV_NAME)}, - } - agent = Agent[None]( - name="Worker env ref agent", - tools=[HostedMCPTool(tool_config=tool_config)], - ) - await Runner.run(starting_agent=agent, input="hi") - - -async def test_a_worker_resolves_only_the_variables_its_plugin_names( - monkeypatch: pytest.MonkeyPatch, client: Client -): - monkeypatch.setenv(ENV_NAME, SENTINEL) - monkeypatch.setenv(OTHER_ENV_NAME, OTHER_SENTINEL) - model = _ToolRecordingModel() - config = client.config() - config["plugins"] = [ - *config.get("plugins", []), - OpenAIAgentsPlugin( - model_provider=TestModelProvider(model), - resolvable_worker_env_vars=[ENV_NAME], - ), - ] - client = Client(**config) - - async with new_worker(client, WorkerEnvRefAgentWorkflow) as worker: - await client.execute_workflow( - WorkerEnvRefAgentWorkflow.run, - id=f"worker-env-ref-allowlist-{uuid.uuid4()}", - task_queue=worker.task_queue, - ) - - tool_config = _hosted_mcp_config_the_model_received(model) - assert tool_config["authorization"] == SENTINEL - assert tool_config["headers"] == { - "X-Token": temporal_worker_env_ref(OTHER_ENV_NAME) - } - - -async def test_an_agent_environment_forwards_the_variables_it_names_to_the_worker( - monkeypatch: pytest.MonkeyPatch, client: Client -): - monkeypatch.setenv(ENV_NAME, SENTINEL) - monkeypatch.setenv(OTHER_ENV_NAME, OTHER_SENTINEL) - model = _ToolRecordingModel() - - async with AgentEnvironment( - model=model, resolvable_worker_env_vars=[ENV_NAME] - ) as env: - client = env.applied_on_client(client) - async with new_worker(client, WorkerEnvRefAgentWorkflow) as worker: - await client.execute_workflow( - WorkerEnvRefAgentWorkflow.run, - id=f"agent-environment-env-ref-allowlist-{uuid.uuid4()}", - task_queue=worker.task_queue, - ) - - tool_config = _hosted_mcp_config_the_model_received(model) - assert tool_config["authorization"] == SENTINEL - assert tool_config["headers"] == { - "X-Token": temporal_worker_env_ref(OTHER_ENV_NAME) - } - - -async def test_re_entering_an_agent_environment_keeps_the_variables_it_names( - monkeypatch: pytest.MonkeyPatch, client: Client -): - monkeypatch.setenv(ENV_NAME, SENTINEL) - model = _ToolRecordingModel() - names = [ENV_NAME] - env = AgentEnvironment(model=model, resolvable_worker_env_vars=names) - - async with env: - pass - names.clear() - - async with env: - client = env.applied_on_client(client) - async with new_worker(client, WorkerEnvRefAgentWorkflow) as worker: - await client.execute_workflow( - WorkerEnvRefAgentWorkflow.run, - id=f"agent-environment-re-entry-{uuid.uuid4()}", - task_queue=worker.task_queue, - ) - - assert _hosted_mcp_config_the_model_received(model)["authorization"] == SENTINEL diff --git a/tests/contrib/openai_agents/test_openai_tracing.py b/tests/contrib/openai_agents/test_openai_tracing.py deleted file mode 100644 index 28b804cc1..000000000 --- a/tests/contrib/openai_agents/test_openai_tracing.py +++ /dev/null @@ -1,955 +0,0 @@ -import uuid -from datetime import timedelta -from typing import Any - -import opentelemetry.trace -from agents import Span, Trace, TracingProcessor, custom_span, trace -from agents.tracing import get_trace_provider -from opentelemetry.sdk.trace import ReadableSpan -from opentelemetry.sdk.trace.export import SimpleSpanProcessor -from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter - -from temporalio import activity, workflow -from temporalio.client import Client -from temporalio.contrib.openai_agents import _temporal_openai_agents -from temporalio.contrib.openai_agents.testing import ( - AgentEnvironment, -) -from temporalio.contrib.opentelemetry import create_tracer_provider -from temporalio.worker.workflow_sandbox import ( - SandboxedWorkflowRunner, - SandboxRestrictions, -) -from tests.contrib.openai_agents.test_openai import ( - ResearchWorkflow, - research_mock_model, -) -from tests.helpers import assert_eq_eventually, new_worker - - -class MemoryTracingProcessor(TracingProcessor): - # True for start events, false for end - trace_events: list[tuple[Trace, bool]] = [] - span_events: list[tuple[Span, bool]] = [] - - def on_trace_start(self, trace: Trace) -> None: - self.trace_events.append((trace, True)) - - def on_trace_end(self, trace: Trace) -> None: - self.trace_events.append((trace, False)) - - def on_span_start(self, span: Span[Any]) -> None: - self.span_events.append((span, True)) - - def on_span_end(self, span: Span[Any]) -> None: - self.span_events.append((span, False)) - - def shutdown(self) -> None: - pass - - def force_flush(self) -> None: - pass - - -def test_otel_instrumentation_lifecycle_does_not_nest() -> None: - from openinference.instrumentation.openai_agents._processor import ( - OpenInferenceTracingProcessor, - ) - from opentelemetry import trace - - original = OpenInferenceTracingProcessor.on_trace_start - _temporal_openai_agents._install_otel_instrumentation(trace.get_tracer_provider()) - try: - installed_patch = OpenInferenceTracingProcessor.on_trace_start - assert installed_patch is not original - - _temporal_openai_agents._install_otel_instrumentation( - trace.get_tracer_provider() - ) - try: - assert OpenInferenceTracingProcessor.on_trace_start is installed_patch - finally: - _temporal_openai_agents._uninstall_otel_instrumentation() - - assert OpenInferenceTracingProcessor.on_trace_start is installed_patch - finally: - _temporal_openai_agents._uninstall_otel_instrumentation() - - assert OpenInferenceTracingProcessor.on_trace_start is original - - -async def test_tracing(client: Client): - async with AgentEnvironment(model=research_mock_model()) as env: - client = env.applied_on_client(client) - provider = get_trace_provider() - - processor = MemoryTracingProcessor() - provider.set_processors([processor]) - - async with new_worker( - client, - ResearchWorkflow, - ) as worker: - with trace("Research workflow"): - workflow_handle = await client.start_workflow( - ResearchWorkflow.run, - "Caribbean vacation spots in April, optimizing for surfing, hiking and water sports", - id=f"research-workflow-{uuid.uuid4()}", - task_queue=worker.task_queue, - execution_timeout=timedelta(seconds=120), - ) - await workflow_handle.result() - print("\n".join([str({"name": t.name}) for t, _ in processor.trace_events])) - - # There are two traces, one is created in the client because it is needed to start the temporal spans - assert len(processor.trace_events) == 2 - assert ( - processor.trace_events[0][0].trace_id - == processor.trace_events[1][0].trace_id - ) - assert processor.trace_events[0][1] - assert not processor.trace_events[1][1] - - def paired_span(a: tuple[Span[Any], bool], b: tuple[Span[Any], bool]) -> None: - assert a[0].trace_id == b[0].trace_id - assert a[1] - assert not b[1] - - print( - "\n".join( - [ - str({"id": t.span_id, "data": t.span_data.export()}) - for t, _ in processor.span_events - ] - ) - ) - - # Start workflow traces - paired_span(processor.span_events[0], processor.span_events[1]) - assert ( - processor.span_events[0][0].span_data.export().get("name") - == "temporal:startWorkflow:ResearchWorkflow" - ) - - # Execute workflow - paired_span(processor.span_events[2], processor.span_events[-1]) - assert ( - processor.span_events[2][0].span_data.export().get("name") - == "temporal:executeWorkflow" - ) - - # Research manager span - paired_span(processor.span_events[3], processor.span_events[-2]) - assert ( - processor.span_events[3][0].span_data.export().get("name") - == "Research manager" - ) - - # Initial planner spans - task wraps agent, agent wraps turn, turn wraps activity - paired_span(processor.span_events[4], processor.span_events[13]) - assert processor.span_events[4][0].span_data.export().get("name") == "task" - - paired_span(processor.span_events[5], processor.span_events[12]) - assert ( - processor.span_events[5][0].span_data.export().get("name") == "PlannerAgent" - ) - - paired_span(processor.span_events[6], processor.span_events[11]) - assert processor.span_events[6][0].span_data.export().get("name") == "turn" - - paired_span(processor.span_events[7], processor.span_events[10]) - assert ( - processor.span_events[7][0].span_data.export().get("name") - == "temporal:startActivity" - ) - - paired_span(processor.span_events[8], processor.span_events[9]) - assert ( - processor.span_events[8][0].span_data.export().get("name") - == "temporal:executeActivity" - ) - - for span, start in processor.span_events[14:-12]: - span_data = span.span_data.export() - - # All spans should be closed - if start: - assert any( - span.span_id == s.span_id and not s_start - for (s, s_start) in processor.span_events - ) - - # Start activity is always parented to a turn span, which is parented to an agent - if span_data.get("name") == "temporal:startActivity": - turn_spans = [ - s for (s, _) in processor.span_events if s.span_id == span.parent_id - ] - assert len(turn_spans) == 2 - assert ( - turn_spans[0] - .span_data.export() - .get("data", {}) - .get("sdk_span_type") - == "turn" - ) - agent_spans = [ - s - for (s, _) in processor.span_events - if s.span_id == turn_spans[0].parent_id - ] - assert len(agent_spans) == 2 - assert agent_spans[0].span_data.export()["type"] == "agent" - - # Execute is parented to start - if span_data.get("name") == "temporal:executeActivity": - parents = [ - s for (s, _) in processor.span_events if s.span_id == span.parent_id - ] - assert ( - len(parents) == 2 - and parents[0].span_data.export()["name"] - == "temporal:startActivity" - ) - - # Final writer spans - task wraps agent, agent wraps turn, turn wraps activity - paired_span(processor.span_events[-12], processor.span_events[-3]) - assert processor.span_events[-12][0].span_data.export().get("name") == "task" - - paired_span(processor.span_events[-11], processor.span_events[-4]) - assert ( - processor.span_events[-11][0].span_data.export().get("name") - == "WriterAgent" - ) - - paired_span(processor.span_events[-10], processor.span_events[-5]) - assert processor.span_events[-10][0].span_data.export().get("name") == "turn" - - paired_span(processor.span_events[-9], processor.span_events[-6]) - assert ( - processor.span_events[-9][0].span_data.export().get("name") - == "temporal:startActivity" - ) - - paired_span(processor.span_events[-8], processor.span_events[-7]) - assert ( - processor.span_events[-8][0].span_data.export().get("name") - == "temporal:executeActivity" - ) - - -@activity.defn -async def simple_no_context_activity() -> str: - return "success" - - -@workflow.defn -class TraceWorkflow: - def __init__(self) -> None: - self._proceed = False - self._ready = False - - @workflow.run - async def run(self): - # Workflow creates spans within existing trace context - with custom_span("Workflow span"): - await workflow.execute_activity( - simple_no_context_activity, - start_to_close_timeout=timedelta(seconds=10), - ) - self._ready = True - await workflow.wait_condition(lambda: self._proceed) - return "done" - - @workflow.query - def ready(self) -> bool: - return self._ready - - @workflow.signal - def proceed(self) -> None: - self._proceed = True - - -@workflow.defn -class SelfTracingWorkflow: - def __init__(self) -> None: - self._proceed = False - self._ready = False - - @workflow.run - async def run(self): - # Workflow starts its own trace - with trace("Workflow trace"): - with custom_span("Workflow span"): - await workflow.execute_activity( - simple_no_context_activity, - start_to_close_timeout=timedelta(seconds=10), - ) - self._ready = True - await workflow.wait_condition(lambda: self._proceed) - return "done" - - @workflow.query - def ready(self) -> bool: - return self._ready - - @workflow.signal - def proceed(self) -> None: - self._proceed = True - - -def print_otel_spans(spans: tuple[ReadableSpan, ...]): - print( - "\n".join( - [ - str( - { - "Name": span.name, - "Id": span.context.span_id if span.context else None, - "Parent": span.parent.span_id if span.parent else None, - } - ) - for span in spans - ] - ) - ) - - -def set_test_tracer_provider() -> InMemorySpanExporter: - exporter = InMemorySpanExporter() - - provider = create_tracer_provider() - provider.add_span_processor(SimpleSpanProcessor(exporter)) - opentelemetry.trace.set_tracer_provider(provider) - # set_tracer_provider is set-once per process: if another test left a - # global provider installed (e.g. leaked from a sibling test in the same - # pytest-xdist worker), the call above silently no-ops and every span in - # this test bypasses the exporter. Fail at the cause instead. - assert opentelemetry.trace.get_tracer_provider() is provider, ( - "Global tracer provider install was a no-op; a previous test in this" - " process left a provider set without resetting it" - ) - return exporter - - -async def test_external_trace_to_workflow_spans( - client: Client, - reset_otel_tracer_provider: Any, # type: ignore[reportUnusedParameter] -): - """Test: External trace -> workflow spans (with worker restart).""" - exporter = set_test_tracer_provider() - workflow_id = None - task_queue = str(uuid.uuid4()) - - # First worker: Start workflow with external trace context - async with AgentEnvironment( - model=research_mock_model(), - add_temporal_spans=False, - use_otel_instrumentation=True, - ) as env: - new_client = env.applied_on_client(client) - # Start external trace, then start workflow within that trace - # Start it outside of the worker to validate provider usage without worker's runcontext - with env.openai_agents_plugin.tracing_context(): - with trace("External trace"): - workflow_handle = await new_client.start_workflow( - TraceWorkflow.run, - id=f"external-trace-workflow-{uuid.uuid4()}", - task_queue=task_queue, - execution_timeout=timedelta(seconds=120), - ) - workflow_id = workflow_handle.id - - async with new_worker( - new_client, - TraceWorkflow, - activities=[simple_no_context_activity], - max_cached_workflows=0, - task_queue=task_queue, - ): - # Wait for workflow to be ready - async def ready() -> bool: - return await workflow_handle.query(TraceWorkflow.ready) - - await assert_eq_eventually(True, ready) - - # Second worker: Complete the workflow with fresh objects (new instrumentation) - async with AgentEnvironment( - model=research_mock_model(), - add_temporal_spans=False, - use_otel_instrumentation=True, - ) as env: - new_client = env.applied_on_client(client) - - async with new_worker( - new_client, - TraceWorkflow, - activities=[simple_no_context_activity], - max_cached_workflows=0, - task_queue=task_queue, - ): - workflow_handle = new_client.get_workflow_handle(workflow_id) - await workflow_handle.signal(TraceWorkflow.proceed) - result = await workflow_handle.result() - assert result == "done" - - spans = exporter.get_finished_spans() - print_otel_spans(spans) - - assert len(spans) >= 2 # External trace + workflow span - - # Find the spans - external_span = next((s for s in spans if s.name == "External trace"), None) - workflow_span = next((s for s in spans if s.name == "Workflow span"), None) - - assert external_span is not None, "External trace span should exist" - assert workflow_span is not None, "Workflow span should exist" - - # Verify parenting: External trace should be root, workflow span should be child of external trace - assert external_span.parent is None, ( - "External trace should have no parent (be root)" - ) - assert workflow_span.parent is not None, "Workflow span should have a parent" - assert external_span.context is not None, "External span should have context" - assert workflow_span.parent.span_id == external_span.context.span_id, ( - "Workflow span should be child of external trace" - ) - - # Verify all spans have unique IDs - span_ids = [span.context.span_id for span in spans if span.context] - assert len(span_ids) == len(set(span_ids)), ( - f"All spans should have unique IDs, got: {span_ids}" - ) - - -async def test_external_trace_and_span_to_workflow_spans( - client: Client, - reset_otel_tracer_provider: Any, # type: ignore[reportUnusedParameter] -): - """Test: External trace + span -> workflow spans (with worker restart).""" - exporter = set_test_tracer_provider() - workflow_id = None - task_queue = str(uuid.uuid4()) - - # First worker: Start workflow with external trace + span context - async with AgentEnvironment( - model=research_mock_model(), - add_temporal_spans=False, - use_otel_instrumentation=True, - ) as env: - new_client = env.applied_on_client(client) - - # Start external trace + span, then start workflow within that context - # Start it outside of the worker to validate provider usage without worker's runcontext - with env.openai_agents_plugin.tracing_context(): - with trace("External trace"): - with custom_span("External span"): - workflow_handle = await new_client.start_workflow( - TraceWorkflow.run, - id=f"external-span-workflow-{uuid.uuid4()}", - task_queue=task_queue, - execution_timeout=timedelta(seconds=120), - ) - workflow_id = workflow_handle.id - - async with new_worker( - new_client, - TraceWorkflow, - activities=[simple_no_context_activity], - max_cached_workflows=0, - task_queue=task_queue, - ): - # Wait for workflow to be ready - async def ready() -> bool: - return await workflow_handle.query(TraceWorkflow.ready) - - await assert_eq_eventually(True, ready) - - # Second worker: Complete the workflow with fresh objects (new instrumentation) - async with AgentEnvironment( - model=research_mock_model(), - add_temporal_spans=False, - use_otel_instrumentation=True, - ) as env: - new_client = env.applied_on_client(client) - - async with new_worker( - new_client, - TraceWorkflow, - activities=[simple_no_context_activity], - max_cached_workflows=0, - task_queue=task_queue, - ): - workflow_handle = new_client.get_workflow_handle(workflow_id) - await workflow_handle.signal(TraceWorkflow.proceed) - result = await workflow_handle.result() - assert result == "done" - - spans = exporter.get_finished_spans() - - assert len(spans) >= 3 # External trace + external span + workflow span - - # Find the spans - external_trace_span = next((s for s in spans if s.name == "External trace"), None) - external_span = next((s for s in spans if s.name == "External span"), None) - workflow_span = next((s for s in spans if s.name == "Workflow span"), None) - - assert external_trace_span is not None, "External trace span should exist" - assert external_span is not None, "External span should exist" - assert workflow_span is not None, "Workflow span should exist" - - # Verify parenting: External span should be child of trace, workflow span should be child of external span - assert external_trace_span.parent is None, ( - "External trace should have no parent (be root)" - ) - assert external_span.parent is not None, "External span should have a parent" - assert external_trace_span.context is not None, ( - "External trace span should have context" - ) - assert external_span.parent.span_id == external_trace_span.context.span_id, ( - "External span should be child of external trace" - ) - assert workflow_span.parent is not None, "Workflow span should have a parent" - assert external_span.context is not None, "External span should have context" - assert workflow_span.parent.span_id == external_span.context.span_id, ( - "Workflow span should be child of external span" - ) - - # Verify all spans have unique IDs - span_ids = [span.context.span_id for span in spans if span.context] - assert len(span_ids) == len(set(span_ids)), ( - f"All spans should have unique IDs, got: {span_ids}" - ) - - -async def test_workflow_only_trace_to_spans( - client: Client, - reset_otel_tracer_provider: Any, # type: ignore[reportUnusedParameter] -): - """Test: Workflow-only trace -> spans (with worker restart).""" - exporter = set_test_tracer_provider() - workflow_id = None - task_queue = str(uuid.uuid4()) - - # First worker: Start workflow (no external trace context) - async with AgentEnvironment( - model=research_mock_model(), - add_temporal_spans=False, - use_otel_instrumentation=True, - ) as env: - new_client = env.applied_on_client(client) - - async with new_worker( - new_client, - SelfTracingWorkflow, - activities=[simple_no_context_activity], - max_cached_workflows=0, - task_queue=task_queue, - ) as worker: - # No external trace - workflow starts its own - workflow_handle = await new_client.start_workflow( - SelfTracingWorkflow.run, - id=f"self-tracing-workflow-{uuid.uuid4()}", - task_queue=worker.task_queue, - execution_timeout=timedelta(seconds=120), - ) - workflow_id = workflow_handle.id - - # Wait for workflow to be ready - async def ready() -> bool: - return await workflow_handle.query(SelfTracingWorkflow.ready) - - await assert_eq_eventually(True, ready) - - # Second worker: Complete the workflow with fresh objects (new instrumentation) - async with AgentEnvironment( - model=research_mock_model(), - add_temporal_spans=False, - use_otel_instrumentation=True, - ) as env: - new_client = env.applied_on_client(client) - - async with new_worker( - new_client, - SelfTracingWorkflow, - activities=[simple_no_context_activity], - max_cached_workflows=0, - task_queue=task_queue, - ) as worker: - workflow_handle = new_client.get_workflow_handle(workflow_id) - await workflow_handle.signal(SelfTracingWorkflow.proceed) - result = await workflow_handle.result() - assert result == "done" - - spans = exporter.get_finished_spans() - - assert len(spans) >= 2 # Workflow trace + workflow span - - # Find the spans - workflow_trace_span = next((s for s in spans if s.name == "Workflow trace"), None) - workflow_span = next((s for s in spans if s.name == "Workflow span"), None) - - assert workflow_trace_span is not None, "Workflow trace span should exist" - assert workflow_span is not None, "Workflow span should exist" - - # Verify parenting: Workflow trace should be root, workflow span should be child of workflow trace - assert workflow_trace_span.parent is None, ( - "Workflow trace should have no parent (be root)" - ) - assert workflow_span.parent is not None, "Workflow span should have a parent" - assert workflow_trace_span.context is not None, ( - "Workflow trace span should have context" - ) - assert workflow_span.parent.span_id == workflow_trace_span.context.span_id, ( - "Workflow span should be child of workflow trace" - ) - - -@workflow.defn -class SimpleWorkflow: - @workflow.run - async def run(self) -> str: - # Use custom_span without starting a trace - should be a no-op - with custom_span("Should not appear"): - with custom_span("Neither should this"): - return "done" - - -async def test_custom_span_without_trace_context( - client: Client, - reset_otel_tracer_provider: Any, # type: ignore[reportUnusedParameter] -): - """Test that custom_span() without a trace context emits no spans. - - This validates our hypothesis about why the main test fails: - If no OpenAI trace is started, custom_span() calls should be no-ops. - """ - exporter = set_test_tracer_provider() - - async with AgentEnvironment( - model=research_mock_model(), use_otel_instrumentation=True - ) as env: - client = env.applied_on_client(client) - - async with new_worker(client, SimpleWorkflow) as worker: - result = await client.execute_workflow( - SimpleWorkflow.run, - id=f"simple-workflow-{uuid.uuid4()}", - task_queue=worker.task_queue, - ) - assert result == "done" - - spans = exporter.get_finished_spans() - - # Should have no custom spans since no trace was started - custom_spans = [ - span - for span in spans - if "Should not appear" in span.name or "Neither should this" in span.name - ] - - assert len(custom_spans) == 0, ( - f"Expected no custom spans without trace context, but found: {[s.name for s in custom_spans]}" - ) - - # Should have no spans at all since no trace was started and spans should be dropped - assert len(spans) == 0, ( - f"Expected no spans without trace context, but found: {[s.name for s in spans]}" - ) - - -async def test_otel_tracing_in_runner( - client: Client, - reset_otel_tracer_provider: Any, # type: ignore[reportUnusedParameter] -): - """Test the tracing when executing an actual OpenAI Runner.""" - exporter = set_test_tracer_provider() - - # Test the new ergonomic API - just pass exporters to AgentEnvironment - async with AgentEnvironment( - model=research_mock_model(), - add_temporal_spans=False, - use_otel_instrumentation=True, - ) as env: - client = env.applied_on_client(client) - - async with new_worker( - client, - ResearchWorkflow, - max_cached_workflows=0, - ) as worker: - with trace("Research workflow"): - workflow_handle = await client.start_workflow( - ResearchWorkflow.run, - "Caribbean vacation spots in April, optimizing for surfing, hiking and water sports", - id=f"research-workflow-{uuid.uuid4()}", - task_queue=worker.task_queue, - execution_timeout=timedelta(seconds=120), - ) - await workflow_handle.result() - - spans = exporter.get_finished_spans() - print("OTEL tracing in runner spans:") - print_otel_spans(spans) - - # Verify basic span capture - assert len(spans) > 0, "Should have captured some spans from the research workflow" - - # Categorize spans that users expect to see in their agents workflow - research_manager_spans = [span for span in spans if "Research manager" in span.name] - search_web_spans = [span for span in spans if "Search the web" in span.name] - agent_execution_spans = [ - span - for span in spans - if any( - agent_name in span.name.lower() - for agent_name in ["planner", "search", "writer"] - ) - and "workflow" not in span.name.lower() - ] - - all_span_names = [span.name for span in spans] - unique_span_names = list(set(all_span_names)) - - # Assert users get visibility into their workflow coordination - assert len(research_manager_spans) > 0, ( - f"Expected 'Research manager' spans for workflow coordination visibility, " - f"but only found: {unique_span_names}" - ) - - # Assert users can see their search phases - assert len(search_web_spans) > 0, ( - f"Expected 'Search the web' spans for search phase visibility, " - f"but only found: {unique_span_names}" - ) - - # Assert users can see individual agent executions - assert len(agent_execution_spans) > 0, ( - f"Expected agent execution spans (planner, search, writer) for individual agent visibility, " - f"but only found: {unique_span_names}" - ) - - # Validate span hierarchy integrity - span_ids = {span.context.span_id for span in spans if span.context} - for span in spans: - if span.parent: - assert span.parent.span_id in span_ids, ( - f"Span '{span.name}' has invalid parent reference - parent span doesn't exist" - ) - - # Validate logical parent-child relationships match user code structure - workflow_trace_spans = [span for span in spans if "Research workflow" in span.name] - assert len(workflow_trace_spans) == 1, ( - f"Expected exactly one 'Research workflow' trace, got {len(workflow_trace_spans)}" - ) - workflow_span = workflow_trace_spans[0] - assert workflow_span.context is not None - - # Research manager should be child of workflow trace - research_span = research_manager_spans[0] - assert research_span.context is not None - assert research_span.parent is not None, ( - "Research manager span should have a parent" - ) - assert research_span.parent.span_id == workflow_span.context.span_id, ( - "Expected 'Research manager' to be child of 'Research workflow' trace" - ) - - # Search the web should be child of research manager - search_span = search_web_spans[0] - assert search_span.context is not None - assert search_span.parent is not None, "Search the web span should have a parent" - assert search_span.parent.span_id == research_span.context.span_id, ( - "Expected 'Search the web' to be child of 'Research manager' span" - ) - - # All search agent spans should be descendants of "Search the web" - # (the SDK now inserts a "task" span between "Search the web" and the agent) - span_by_id = {span.context.span_id: span for span in spans if span.context} - search_agent_spans = [span for span in spans if "Search agent" in span.name] - - def is_descendant_of(child: ReadableSpan, ancestor_span_id: int) -> bool: - """Check if child is a descendant of the span with ancestor_span_id.""" - current: ReadableSpan | None = child - while current and current.parent: - if current.parent.span_id == ancestor_span_id: - return True - current = span_by_id.get(current.parent.span_id) - return False - - for search_agent_span in search_agent_spans: - assert search_agent_span.parent is not None, ( - f"Search agent span '{search_agent_span.name}' should have a parent" - ) - assert is_descendant_of(search_agent_span, search_span.context.span_id), ( - f"Expected all 'Search agent' spans to be descendants of 'Search the web' span" - ) - - # PlannerAgent and WriterAgent should be descendants of research manager - planner_spans = [span for span in spans if "PlannerAgent" in span.name] - writer_spans = [span for span in spans if "WriterAgent" in span.name] - - for planner_span in planner_spans: - assert planner_span.parent is not None, "PlannerAgent span should have a parent" - assert is_descendant_of(planner_span, research_span.context.span_id), ( - "Expected 'PlannerAgent' to be descendant of 'Research manager' span" - ) - - for writer_span in writer_spans: - assert writer_span.parent is not None, "WriterAgent span should have a parent" - assert is_descendant_of(writer_span, research_span.context.span_id), ( - "Expected 'WriterAgent' to be descendant of 'Research manager' span" - ) - - -@workflow.defn -class OtelSpanWorkflow: - def __init__(self) -> None: - self._proceed = False - self._ready = False - - @workflow.run - async def run(self): - # Start an SDK custom_span first to establish OTEL context - with custom_span("Workflow SDK span"): - # Workflow starts OTEL span directly using opentelemetry.trace - tracer = opentelemetry.trace.get_tracer(__name__) - with tracer.start_as_current_span("Direct OTEL span"): - await workflow.execute_activity( - simple_no_context_activity, - start_to_close_timeout=timedelta(seconds=10), - ) - self._ready = True - await workflow.wait_condition(lambda: self._proceed) - return "done" - - @workflow.query - def ready(self) -> bool: - return self._ready - - @workflow.signal - def proceed(self) -> None: - self._proceed = True - - -async def test_sdk_trace_to_otel_span_parenting( - client: Client, - reset_otel_tracer_provider: Any, # type: ignore[reportUnusedParameter] -): - """Test that OTEL spans started in workflow are properly parented to client SDK trace.""" - exporter = set_test_tracer_provider() - workflow_id = None - task_queue = str(uuid.uuid4()) - - # First worker: Start workflow with client SDK trace context - async with AgentEnvironment( - model=research_mock_model(), - add_temporal_spans=False, - use_otel_instrumentation=True, - ) as env: - new_client = env.applied_on_client(client) - - async with new_worker( - new_client, - OtelSpanWorkflow, - activities=[simple_no_context_activity], - max_cached_workflows=0, - task_queue=task_queue, - workflow_runner=SandboxedWorkflowRunner( - SandboxRestrictions.default.with_passthrough_modules("opentelemetry") - ), - ) as worker: - # Start SDK trace in client, then start workflow within that trace - with trace("Client SDK trace"): - workflow_handle = await new_client.start_workflow( - OtelSpanWorkflow.run, - id=f"sdk-trace-otel-span-workflow-{uuid.uuid4()}", - task_queue=worker.task_queue, - execution_timeout=timedelta(seconds=120), - ) - workflow_id = workflow_handle.id - - # Wait for workflow to be ready - async def ready() -> bool: - return await workflow_handle.query(OtelSpanWorkflow.ready) - - await assert_eq_eventually(True, ready) - - # Second worker: Complete the workflow with fresh objects (new instrumentation) - async with AgentEnvironment( - model=research_mock_model(), - add_temporal_spans=False, - use_otel_instrumentation=True, - ) as env: - new_client = env.applied_on_client(client) - - async with new_worker( - new_client, - OtelSpanWorkflow, - activities=[simple_no_context_activity], - max_cached_workflows=0, - task_queue=task_queue, - workflow_runner=SandboxedWorkflowRunner( - SandboxRestrictions.default.with_passthrough_modules("opentelemetry") - ), - ) as worker: - workflow_handle = new_client.get_workflow_handle(workflow_id) - await workflow_handle.signal(OtelSpanWorkflow.proceed) - result = await workflow_handle.result() - assert result == "done" - - spans = exporter.get_finished_spans() - print("SDK trace to OTEL span parenting:") - print_otel_spans(spans) - - assert len(spans) >= 3 # Client SDK trace + Workflow SDK span + Direct OTEL span - - # Find the spans - client_sdk_trace_span = next( - (s for s in spans if s.name == "Client SDK trace"), None - ) - workflow_sdk_span = next((s for s in spans if s.name == "Workflow SDK span"), None) - direct_otel_span = next((s for s in spans if s.name == "Direct OTEL span"), None) - - assert client_sdk_trace_span is not None, "Client SDK trace span should exist" - assert workflow_sdk_span is not None, "Workflow SDK span should exist" - assert direct_otel_span is not None, "Direct OTEL span should exist" - - # Verify parenting chain: Client SDK trace -> Workflow SDK span -> Direct OTEL span - assert client_sdk_trace_span.parent is None, ( - "Client SDK trace should have no parent (be root)" - ) - - assert workflow_sdk_span.parent is not None, ( - "Workflow SDK span should have a parent" - ) - assert client_sdk_trace_span.context is not None, ( - "Client SDK trace span should have context" - ) - assert workflow_sdk_span.parent.span_id == client_sdk_trace_span.context.span_id, ( - "Workflow SDK span should be child of Client SDK trace" - ) - - assert direct_otel_span.parent is not None, "Direct OTEL span should have a parent" - assert workflow_sdk_span.context is not None, ( - "Workflow SDK span should have context" - ) - assert direct_otel_span.parent.span_id == workflow_sdk_span.context.span_id, ( - "Direct OTEL span should be child of Workflow SDK span" - ) - - # Verify all spans belong to the same trace - assert workflow_sdk_span.context is not None, ( - "Workflow SDK span should have context" - ) - assert direct_otel_span.context is not None, "Direct OTEL span should have context" - assert ( - client_sdk_trace_span.context.trace_id - == workflow_sdk_span.context.trace_id - == direct_otel_span.context.trace_id - ), "All spans should belong to the same trace" - - # Verify all spans have unique IDs - span_ids = [span.context.span_id for span in spans if span.context] - assert len(span_ids) == len(set(span_ids)), ( - f"All spans should have unique IDs, got: {span_ids}" - ) diff --git a/uv.lock b/uv.lock index ab1c17005..3fe5c0dc2 100644 --- a/uv.lock +++ b/uv.lock @@ -9,7 +9,7 @@ resolution-markers = [ ] [options] -exclude-newer = "2026-08-31T19:12:49.465398Z" +exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. exclude-newer-span = "P2W" [[package]] @@ -257,14 +257,14 @@ name = "anthropic" version = "0.117.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio", marker = "python_full_version >= '3.11'" }, - { name = "distro", marker = "python_full_version >= '3.11'" }, - { name = "docstring-parser", marker = "python_full_version >= '3.11'" }, - { name = "httpx", marker = "python_full_version >= '3.11'" }, - { name = "jiter", marker = "python_full_version >= '3.11'" }, - { name = "pydantic", marker = "python_full_version >= '3.11'" }, - { name = "sniffio", marker = "python_full_version >= '3.11'" }, - { name = "typing-extensions", marker = "python_full_version >= '3.11'" }, + { name = "anyio" }, + { name = "distro" }, + { name = "docstring-parser" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/41/0d/8f71d535edb0d438f023bd825fb65f67c14fa88a2bd6b75f292a58a63de4/anthropic-0.117.0.tar.gz", hash = "sha256:98107f2b76439641e0ae2a1754087534b8f178dbab99d6eb1bc4b7bc8c744496", size = 989933, upload-time = "2026-07-16T19:36:13.07Z" } wheels = [ @@ -942,12 +942,12 @@ name = "deepagents" version = "0.6.12" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "langchain", marker = "python_full_version >= '3.11'" }, - { name = "langchain-anthropic", marker = "python_full_version >= '3.11'" }, - { name = "langchain-core", marker = "python_full_version >= '3.11'" }, - { name = "langchain-google-genai", marker = "python_full_version >= '3.11'" }, - { name = "langsmith", marker = "python_full_version >= '3.11'" }, - { name = "wcmatch", marker = "python_full_version >= '3.11'" }, + { name = "langchain" }, + { name = "langchain-anthropic" }, + { name = "langchain-core" }, + { name = "langchain-google-genai" }, + { name = "langsmith" }, + { name = "wcmatch" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e0/db/a6acdc72a9e90c3f07ed10de35c951734a02d4facb693bb59684ad368801/deepagents-0.6.12.tar.gz", hash = "sha256:1f281c0bc5a63132f62e2ee345c1dc593b23188da6e23016401f6879fbe54b5f", size = 211364, upload-time = "2026-06-25T17:26:52.775Z" } wheels = [ @@ -1022,7 +1022,7 @@ name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ @@ -1403,15 +1403,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/91/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl", hash = "sha256:54f33de9f4f911d7e84e4191749cac8cc5653f815b06738c54db9a15ab8b1e42", size = 47300, upload-time = "2025-06-15T09:35:04.433Z" }, ] -[[package]] -name = "griffelib" -version = "2.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/33/e4/8d187ea29c2e30b3a09505c567513077d6117861bde1fbd997a167f262ec/griffelib-2.1.0.tar.gz", hash = "sha256:762a186d2c6fd6794d4ea20d428d597ffb857cb56b66421651cbba15bdd5e813", size = 216234, upload-time = "2026-06-19T12:05:42.278Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e4/d3/5268aeabf2ad82658c4e2ff3a060648d0f02f3926cb53247c0e4d0dab49e/griffelib-2.1.0-py3-none-any.whl", hash = "sha256:cc7b3d2d2865ad0b909fcc38086e3f554b5ea7acbaa7bbb7ecaa3f5dfb7d9f00", size = 142560, upload-time = "2026-06-19T12:05:38.742Z" }, -] - [[package]] name = "grpcio" version = "1.82.1" @@ -1971,9 +1962,9 @@ name = "langchain" version = "1.3.14" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "langchain-core", marker = "python_full_version >= '3.11'" }, - { name = "langgraph", marker = "python_full_version >= '3.11'" }, - { name = "pydantic", marker = "python_full_version >= '3.11'" }, + { name = "langchain-core" }, + { name = "langgraph" }, + { name = "pydantic" }, ] sdist = { url = "https://files.pythonhosted.org/packages/29/68/a6dbad9c22df4087a0f9e79ddd46226c442b30128bfeee538d5889492a73/langchain-1.3.14.tar.gz", hash = "sha256:1b6696c72ba3bbbce54d745e0180742c9f6ece8bbc59ed5a46c3e20b9a435929", size = 645181, upload-time = "2026-07-16T13:28:18.29Z" } wheels = [ @@ -1985,9 +1976,9 @@ name = "langchain-anthropic" version = "1.4.8" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anthropic", marker = "python_full_version >= '3.11'" }, - { name = "langchain-core", marker = "python_full_version >= '3.11'" }, - { name = "pydantic", marker = "python_full_version >= '3.11'" }, + { name = "anthropic" }, + { name = "langchain-core" }, + { name = "pydantic" }, ] sdist = { url = "https://files.pythonhosted.org/packages/98/22/40ab129b08329ca295b391aa1d48267692b42594757084c6918e22b655ac/langchain_anthropic-1.4.8.tar.gz", hash = "sha256:c76891b2044d56105ff13c106ed12650637b53bd598a4bdf15b4796eefa2a4ec", size = 708524, upload-time = "2026-06-26T21:28:46.916Z" } wheels = [ @@ -2019,10 +2010,10 @@ name = "langchain-google-genai" version = "4.2.7" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "filetype", marker = "python_full_version >= '3.11'" }, - { name = "google-genai", marker = "python_full_version >= '3.11'" }, - { name = "langchain-core", marker = "python_full_version >= '3.11'" }, - { name = "pydantic", marker = "python_full_version >= '3.11'" }, + { name = "filetype" }, + { name = "google-genai" }, + { name = "langchain-core" }, + { name = "pydantic" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1d/0c/bc60dabc362ca7c6ffe8c4bcc2f724c7e566b43eb230cee51419f88f784c/langchain_google_genai-4.2.7.tar.gz", hash = "sha256:03b1463ffe4d42435f43c7870467f2215f684bb46400d2543435d10157c80ac7", size = 281605, upload-time = "2026-07-06T13:51:58.724Z" } wheels = [ @@ -2818,29 +2809,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/b0/2291689e3ec4723fbf5bbf3b54afcd7b160f9ddc98ca7aedfd0132af5677/openai-2.45.0-py3-none-any.whl", hash = "sha256:5df105f5f8c9b711fcb9d06d2d3888cebc82506db216484c14a4e53cdf651777", size = 1629470, upload-time = "2026-07-09T18:02:42.21Z" }, ] -[[package]] -name = "openai-agents" -version = "0.19.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "griffelib" }, - { name = "mcp" }, - { name = "openai" }, - { name = "pydantic" }, - { name = "requests" }, - { name = "typing-extensions" }, - { name = "websockets" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ff/ea/a8cae2dadf798f369be5f9cb544a169f5f6aecc096a80f1a209dddc4c00f/openai_agents-0.19.4.tar.gz", hash = "sha256:fe21778ee1e8216c9cdb775fa86d11b08be68c0184e14023993088d3f812c0be", size = 5784063, upload-time = "2026-08-05T02:59:12.939Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/90/d8/98925e1e4888e58d7694ba71af2ba93b94540f481c45ee8b7f7be7e30fcd/openai_agents-0.19.4-py3-none-any.whl", hash = "sha256:12e0372fae9698fe6f78e05aaeb4ccdb229602f7ef99b8195a7d68dc82869f51", size = 968498, upload-time = "2026-08-05T02:59:11.191Z" }, -] - -[package.optional-dependencies] -litellm = [ - { name = "litellm", marker = "python_full_version < '3.14'" }, -] - [[package]] name = "openapi-schema-validator" version = "0.9.0" @@ -2908,24 +2876,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/27/c0/b0c301a0a1de9fe377e666e89b9e87fa368bd5ac1341ab125624e2bbc081/openinference_instrumentation_google_adk-0.1.17-py3-none-any.whl", hash = "sha256:3f45e38cfd5ffb41c18deddc747e48c68595d4dfa1a89b7b1aa1f3d31e46ce0d", size = 16755, upload-time = "2026-07-01T15:44:18.364Z" }, ] -[[package]] -name = "openinference-instrumentation-openai-agents" -version = "1.6.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "openinference-instrumentation" }, - { name = "openinference-semantic-conventions" }, - { name = "opentelemetry-api" }, - { name = "opentelemetry-instrumentation" }, - { name = "opentelemetry-semantic-conventions" }, - { name = "typing-extensions" }, - { name = "wrapt" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/cc/6f/281a267b837d33505c8b4ef70644b8ebfeb7ed902a909200333916185851/openinference_instrumentation_openai_agents-1.6.1.tar.gz", hash = "sha256:39b211b7ff28d59a401b2659f3885f967fb3e17580b29b4b4aedac6b8fc98e0e", size = 26089, upload-time = "2026-06-05T19:54:22.737Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/08/54/c3d4e67bdad5170e315bb03086f034d2a60ccb26c4200010428456c9f4d6/openinference_instrumentation_openai_agents-1.6.1-py3-none-any.whl", hash = "sha256:ca5dc650fc53461cbce0f8f19bfa78d43de605c439a9d3c44d99fd0b6e10e3ba", size = 28466, upload-time = "2026-06-05T19:54:21.48Z" }, -] - [[package]] name = "openinference-semantic-conventions" version = "0.1.30" @@ -4745,10 +4695,6 @@ langgraph = [ langsmith = [ { name = "langsmith" }, ] -openai-agents = [ - { name = "mcp" }, - { name = "openai-agents" }, -] opentelemetry = [ { name = "opentelemetry-api" }, { name = "opentelemetry-sdk" }, @@ -4781,10 +4727,7 @@ dev = [ { name = "moto", extra = ["s3", "server"] }, { name = "mypy" }, { name = "mypy-protobuf" }, - { name = "openai-agents" }, - { name = "openai-agents", extra = ["litellm"], marker = "python_full_version < '3.14'" }, { name = "openinference-instrumentation-google-adk" }, - { name = "openinference-instrumentation-openai-agents" }, { name = "opentelemetry-exporter-otlp-proto-grpc" }, { name = "opentelemetry-sdk-extension-aws" }, { name = "opentelemetry-semantic-conventions" }, @@ -4820,9 +4763,7 @@ requires-dist = [ { name = "langgraph", marker = "extra == 'langgraph'", specifier = ">=1.1.0" }, { name = "langsmith", marker = "extra == 'langsmith'", specifier = ">=0.7.34,<0.9" }, { name = "mcp", marker = "extra == 'google-adk'", specifier = ">=1.24,<2" }, - { name = "mcp", marker = "extra == 'openai-agents'", specifier = ">=1.9.4,<2" }, { name = "nexus-rpc", specifier = "==1.4.0" }, - { name = "openai-agents", marker = "extra == 'openai-agents'", specifier = ">=0.19.2,<0.20" }, { name = "opentelemetry-api", marker = "extra == 'cloud-run-worker-otel'", specifier = ">=1.26,<2" }, { name = "opentelemetry-api", marker = "extra == 'lambda-worker-otel'", specifier = ">=1.26,<2" }, { name = "opentelemetry-api", marker = "extra == 'opentelemetry'", specifier = ">=1.26,<2" }, @@ -4842,7 +4783,7 @@ requires-dist = [ { name = "types-protobuf", specifier = ">=3.20,<8.0.0" }, { name = "typing-extensions", specifier = ">=4.2.0,<5" }, ] -provides-extras = ["grpc", "opentelemetry", "pydantic", "openai-agents", "google-adk", "langgraph", "langsmith", "deepagents", "lambda-worker-otel", "cloud-run-worker-otel", "aioboto3", "google-genai", "strands-agents"] +provides-extras = ["grpc", "opentelemetry", "pydantic", "google-adk", "langgraph", "langsmith", "deepagents", "lambda-worker-otel", "cloud-run-worker-otel", "aioboto3", "google-genai", "strands-agents"] [package.metadata.requires-dev] dev = [ @@ -4865,10 +4806,7 @@ dev = [ { name = "moto", extras = ["s3", "server"], specifier = ">=5" }, { name = "mypy", specifier = "==1.18.2" }, { name = "mypy-protobuf", specifier = ">=3.3.0,<4" }, - { name = "openai-agents", marker = "python_full_version >= '3.14'", specifier = ">=0.19.2,<0.20" }, - { name = "openai-agents", extras = ["litellm"], marker = "python_full_version < '3.14'", specifier = ">=0.19.2,<0.20" }, { name = "openinference-instrumentation-google-adk", specifier = ">=0.1.11" }, - { name = "openinference-instrumentation-openai-agents", specifier = ">=0.1.0" }, { name = "opentelemetry-exporter-otlp-proto-grpc", specifier = ">=1.11.1,<2" }, { name = "opentelemetry-sdk-extension-aws", specifier = ">=2.0.0,<3" }, { name = "opentelemetry-semantic-conventions", specifier = ">=0.40b0,<1" }, @@ -5377,7 +5315,7 @@ name = "wcmatch" version = "11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "bracex", marker = "python_full_version >= '3.11'" }, + { name = "bracex" }, ] sdist = { url = "https://files.pythonhosted.org/packages/16/25/1da725838132221e33568973da484ff43813662ccc06ebf7f6e3abddfcd5/wcmatch-11.0.tar.gz", hash = "sha256:55d95c2447789712774b198ceec72939e88b5618f1f8f0a9b605bf7740b63b96", size = 141360, upload-time = "2026-07-10T05:50:24.183Z" } wheels = [ From abc1236f063fca98c291fdd2b734e7060165e9ba Mon Sep 17 00:00:00 2001 From: Brian Strauch Date: Tue, 15 Sep 2026 15:39:24 -0700 Subject: [PATCH 02/16] Add OpenAI Agents migration error --- temporalio/contrib/__init__.py | 12 ++++++++++++ tests/test_contrib.py | 16 ++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 tests/test_contrib.py diff --git a/temporalio/contrib/__init__.py b/temporalio/contrib/__init__.py index 4b3ea62c5..82bc7eaaa 100644 --- a/temporalio/contrib/__init__.py +++ b/temporalio/contrib/__init__.py @@ -1 +1,13 @@ """Extra modules that may have optional dependencies.""" + +from typing import NoReturn + + +def __getattr__(name: str) -> NoReturn: + if name == "openai_agents": + raise ImportError( + "The OpenAI Agents integration has moved to the " + "temporalio-openai-agents package. Install it with " + "`uv add temporalio-openai-agents`." + ) + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/tests/test_contrib.py b/tests/test_contrib.py new file mode 100644 index 000000000..389b8be23 --- /dev/null +++ b/tests/test_contrib.py @@ -0,0 +1,16 @@ +import pytest + +import temporalio.contrib + + +def test_openai_agents_migration_error() -> None: + with pytest.raises( + ImportError, + match=r"uv add temporalio-openai-agents", + ): + exec("from temporalio.contrib import openai_agents", {}) + + +def test_unknown_attribute_error() -> None: + with pytest.raises(AttributeError, match="does_not_exist"): + getattr(temporalio.contrib, "does_not_exist") From e2772d4dcb76401dec7e63414097e7f290b5c898 Mon Sep 17 00:00:00 2001 From: Brian Strauch Date: Tue, 15 Sep 2026 15:42:26 -0700 Subject: [PATCH 03/16] Load standalone OpenAI Agents module --- temporalio/contrib/__init__.py | 21 ++++++++++++++------- tests/test_contrib.py | 12 ++++++++++++ 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/temporalio/contrib/__init__.py b/temporalio/contrib/__init__.py index 82bc7eaaa..4ef863ec4 100644 --- a/temporalio/contrib/__init__.py +++ b/temporalio/contrib/__init__.py @@ -1,13 +1,20 @@ """Extra modules that may have optional dependencies.""" -from typing import NoReturn +from importlib import import_module +from types import ModuleType -def __getattr__(name: str) -> NoReturn: +def __getattr__(name: str) -> ModuleType: if name == "openai_agents": - raise ImportError( - "The OpenAI Agents integration has moved to the " - "temporalio-openai-agents package. Install it with " - "`uv add temporalio-openai-agents`." - ) + module_name = f"{__name__}.{name}" + try: + return import_module(module_name) + except ModuleNotFoundError as err: + if err.name != module_name: + raise + raise ImportError( + "The OpenAI Agents integration has moved to the " + "temporalio-openai-agents package. Install it with " + "`uv add temporalio-openai-agents`." + ) from err raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/tests/test_contrib.py b/tests/test_contrib.py index 389b8be23..da4d1fe07 100644 --- a/tests/test_contrib.py +++ b/tests/test_contrib.py @@ -1,3 +1,5 @@ +from types import ModuleType + import pytest import temporalio.contrib @@ -11,6 +13,16 @@ def test_openai_agents_migration_error() -> None: exec("from temporalio.contrib import openai_agents", {}) +def test_openai_agents_standalone_module(monkeypatch: pytest.MonkeyPatch) -> None: + standalone_module = ModuleType("temporalio.contrib.openai_agents") + monkeypatch.setattr( + temporalio.contrib, + "import_module", + lambda name: standalone_module, + ) + assert getattr(temporalio.contrib, "openai_agents") is standalone_module + + def test_unknown_attribute_error() -> None: with pytest.raises(AttributeError, match="does_not_exist"): getattr(temporalio.contrib, "does_not_exist") From 448646b7c535bf32d0b9c7af24494b767e4366f4 Mon Sep 17 00:00:00 2001 From: Brian Strauch Date: Tue, 15 Sep 2026 15:44:21 -0700 Subject: [PATCH 04/16] Keep contrib migration helpers private --- temporalio/contrib/__init__.py | 8 ++++---- tests/test_contrib.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/temporalio/contrib/__init__.py b/temporalio/contrib/__init__.py index 4ef863ec4..947d43363 100644 --- a/temporalio/contrib/__init__.py +++ b/temporalio/contrib/__init__.py @@ -1,14 +1,14 @@ """Extra modules that may have optional dependencies.""" -from importlib import import_module -from types import ModuleType +from importlib import import_module as _import_module +from types import ModuleType as _ModuleType -def __getattr__(name: str) -> ModuleType: +def __getattr__(name: str) -> _ModuleType: if name == "openai_agents": module_name = f"{__name__}.{name}" try: - return import_module(module_name) + return _import_module(module_name) except ModuleNotFoundError as err: if err.name != module_name: raise diff --git a/tests/test_contrib.py b/tests/test_contrib.py index da4d1fe07..190efe240 100644 --- a/tests/test_contrib.py +++ b/tests/test_contrib.py @@ -17,7 +17,7 @@ def test_openai_agents_standalone_module(monkeypatch: pytest.MonkeyPatch) -> Non standalone_module = ModuleType("temporalio.contrib.openai_agents") monkeypatch.setattr( temporalio.contrib, - "import_module", + "_import_module", lambda name: standalone_module, ) assert getattr(temporalio.contrib, "openai_agents") is standalone_module From bd81bfd38652f012e7896630396b5199f39bed46 Mon Sep 17 00:00:00 2001 From: Brian Strauch Date: Tue, 15 Sep 2026 17:50:34 -0700 Subject: [PATCH 05/16] Forward OpenAI Agents extra to standalone package --- CHANGELOG.md | 7 +-- pyproject.toml | 1 + temporalio/__init__.py | 4 ++ temporalio/contrib/__init__.py | 19 -------- temporalio/contrib/openai_agents/__init__.py | 34 ++++++++++++++ .../contrib/openai_agents/sandbox/__init__.py | 1 + temporalio/contrib/openai_agents/testing.py | 15 +++++++ temporalio/contrib/openai_agents/workflow.py | 21 +++++++++ tests/test_contrib.py | 45 ++++++++----------- 9 files changed, 98 insertions(+), 49 deletions(-) create mode 100644 temporalio/contrib/openai_agents/__init__.py create mode 100644 temporalio/contrib/openai_agents/sandbox/__init__.py create mode 100644 temporalio/contrib/openai_agents/testing.py create mode 100644 temporalio/contrib/openai_agents/workflow.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 91b3e9b14..60e3d7adf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,9 +28,10 @@ to include examples, links to docs, or any other relevant information. - The OpenAI Agents integration has moved to the independently versioned [`temporalio-openai-agents`](https://pypi.org/project/temporalio-openai-agents/) - package. Install it with `uv add temporalio-openai-agents`; the - `temporalio[openai-agents]` extra and bundled `temporalio.contrib.openai_agents` - module have been removed. + package. The existing `temporalio[openai-agents]` extra now installs that + package, and compatibility modules preserve the old public + `temporalio.contrib.openai_agents` imports. New code should depend on + `temporalio-openai-agents` directly and import `temporalio.openai_agents`. ### Fixed diff --git a/pyproject.toml b/pyproject.toml index 779c9afb9..9cad62b63 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,7 @@ classifiers = [ grpc = ["grpcio>=1.48.2,<2"] opentelemetry = ["opentelemetry-api>=1.26,<2", "opentelemetry-sdk>=1.26,<2"] pydantic = ["pydantic>=2.0.0,<3"] +openai-agents = ["temporalio-openai-agents>=1.0.0"] google-adk = ["google-adk>=2.2.0,<3", "mcp>=1.24,<2"] langgraph = ["langgraph>=1.1.0"] langsmith = ["langsmith>=0.7.34,<0.9"] diff --git a/temporalio/__init__.py b/temporalio/__init__.py index 2afb165a7..b94c19ef2 100644 --- a/temporalio/__init__.py +++ b/temporalio/__init__.py @@ -8,6 +8,10 @@ :py:mod:`worker` to run workflows and activities. """ +from pkgutil import extend_path as _extend_path + from .service import __version__ as __sdk_version +__path__ = _extend_path(__path__, __name__) + __version__ = __sdk_version diff --git a/temporalio/contrib/__init__.py b/temporalio/contrib/__init__.py index 947d43363..4b3ea62c5 100644 --- a/temporalio/contrib/__init__.py +++ b/temporalio/contrib/__init__.py @@ -1,20 +1 @@ """Extra modules that may have optional dependencies.""" - -from importlib import import_module as _import_module -from types import ModuleType as _ModuleType - - -def __getattr__(name: str) -> _ModuleType: - if name == "openai_agents": - module_name = f"{__name__}.{name}" - try: - return _import_module(module_name) - except ModuleNotFoundError as err: - if err.name != module_name: - raise - raise ImportError( - "The OpenAI Agents integration has moved to the " - "temporalio-openai-agents package. Install it with " - "`uv add temporalio-openai-agents`." - ) from err - raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/temporalio/contrib/openai_agents/__init__.py b/temporalio/contrib/openai_agents/__init__.py new file mode 100644 index 000000000..f84013276 --- /dev/null +++ b/temporalio/contrib/openai_agents/__init__.py @@ -0,0 +1,34 @@ +"""Compatibility imports for the standalone OpenAI Agents integration. + +New code should import :mod:`temporalio.openai_agents` directly. +""" + +from temporalio.openai_agents import ( + AgentsWorkflowError, + AllowAllWorkerEnvVars, + ModelActivityParameters, + OpenAIAgentsPlugin, + OpenAIPayloadConverter, + SandboxClientProvider, + StatefulMCPServerProvider, + StatelessMCPServerProvider, + TemporalWorkerEnvValue, + temporal_worker_env_ref, +) + +from . import testing, workflow + +__all__ = [ + "AgentsWorkflowError", + "AllowAllWorkerEnvVars", + "ModelActivityParameters", + "OpenAIAgentsPlugin", + "OpenAIPayloadConverter", + "SandboxClientProvider", + "StatelessMCPServerProvider", + "StatefulMCPServerProvider", + "TemporalWorkerEnvValue", + "temporal_worker_env_ref", + "testing", + "workflow", +] diff --git a/temporalio/contrib/openai_agents/sandbox/__init__.py b/temporalio/contrib/openai_agents/sandbox/__init__.py new file mode 100644 index 000000000..ec92a33ee --- /dev/null +++ b/temporalio/contrib/openai_agents/sandbox/__init__.py @@ -0,0 +1 @@ +"""Compatibility package for OpenAI Agents sandbox support.""" diff --git a/temporalio/contrib/openai_agents/testing.py b/temporalio/contrib/openai_agents/testing.py new file mode 100644 index 000000000..7d3223193 --- /dev/null +++ b/temporalio/contrib/openai_agents/testing.py @@ -0,0 +1,15 @@ +"""Compatibility imports for OpenAI Agents testing helpers.""" + +from temporalio.openai_agents.testing import ( + AgentEnvironment, + ResponseBuilders, + TestModel, + TestModelProvider, +) + +__all__ = [ + "AgentEnvironment", + "ResponseBuilders", + "TestModel", + "TestModelProvider", +] diff --git a/temporalio/contrib/openai_agents/workflow.py b/temporalio/contrib/openai_agents/workflow.py new file mode 100644 index 000000000..bf3a4bc44 --- /dev/null +++ b/temporalio/contrib/openai_agents/workflow.py @@ -0,0 +1,21 @@ +"""Compatibility imports for OpenAI Agents workflow helpers.""" + +from temporalio.openai_agents.workflow import ( + AgentsWorkflowError, + ToolSerializationError, + activity_as_tool, + nexus_operation_as_tool, + stateful_mcp_server, + stateless_mcp_server, + temporal_sandbox_client, +) + +__all__ = [ + "AgentsWorkflowError", + "ToolSerializationError", + "activity_as_tool", + "nexus_operation_as_tool", + "stateful_mcp_server", + "stateless_mcp_server", + "temporal_sandbox_client", +] diff --git a/tests/test_contrib.py b/tests/test_contrib.py index 190efe240..8129a1cf5 100644 --- a/tests/test_contrib.py +++ b/tests/test_contrib.py @@ -1,28 +1,19 @@ -from types import ModuleType - -import pytest - -import temporalio.contrib - - -def test_openai_agents_migration_error() -> None: - with pytest.raises( - ImportError, - match=r"uv add temporalio-openai-agents", - ): - exec("from temporalio.contrib import openai_agents", {}) - - -def test_openai_agents_standalone_module(monkeypatch: pytest.MonkeyPatch) -> None: - standalone_module = ModuleType("temporalio.contrib.openai_agents") - monkeypatch.setattr( - temporalio.contrib, - "_import_module", - lambda name: standalone_module, +import temporalio.contrib.openai_agents as compatibility +import temporalio.contrib.openai_agents.testing as compatibility_testing +import temporalio.contrib.openai_agents.workflow as compatibility_workflow +import temporalio.openai_agents as standalone +import temporalio.openai_agents.testing as standalone_testing +import temporalio.openai_agents.workflow as standalone_workflow + + +def test_openai_agents_compatibility_imports() -> None: + assert compatibility.OpenAIAgentsPlugin is standalone.OpenAIAgentsPlugin + assert compatibility.OpenAIPayloadConverter is standalone.OpenAIPayloadConverter + assert compatibility_testing.AgentEnvironment is standalone_testing.AgentEnvironment + assert ( + compatibility_workflow.activity_as_tool is standalone_workflow.activity_as_tool + ) + assert ( + compatibility_workflow.temporal_sandbox_client + is standalone_workflow.temporal_sandbox_client ) - assert getattr(temporalio.contrib, "openai_agents") is standalone_module - - -def test_unknown_attribute_error() -> None: - with pytest.raises(AttributeError, match="does_not_exist"): - getattr(temporalio.contrib, "does_not_exist") From 4244497401eb36cf8e82d525eb5fae15bedc2a69 Mon Sep 17 00:00:00 2001 From: Brian Strauch Date: Tue, 15 Sep 2026 19:59:21 -0700 Subject: [PATCH 06/16] Apply suggestion from @brianstrauch --- temporalio/contrib/google_adk_agents/_mcp.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/temporalio/contrib/google_adk_agents/_mcp.py b/temporalio/contrib/google_adk_agents/_mcp.py index 60d8adc8f..8b8cd50e5 100644 --- a/temporalio/contrib/google_adk_agents/_mcp.py +++ b/temporalio/contrib/google_adk_agents/_mcp.py @@ -333,8 +333,6 @@ def _handle_worker_failure(func: Callable) -> Callable: state, so we re-raise as an ``ApplicationError`` of type ``"DedicatedWorkerFailure"`` for the caller to handle. - Kept local rather than shared with the separately distributed OpenAI Agents - integration so the two packages do not depend on each other's internals. """ @functools.wraps(func) From c914f5ce32f6d905eeeabd4b853bb16255fd0b5f Mon Sep 17 00:00:00 2001 From: Brian Strauch Date: Tue, 15 Sep 2026 20:29:25 -0700 Subject: [PATCH 07/16] Add OpenAI Agents migration guide --- temporalio/contrib/openai_agents/README.md | 51 ++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 temporalio/contrib/openai_agents/README.md diff --git a/temporalio/contrib/openai_agents/README.md b/temporalio/contrib/openai_agents/README.md new file mode 100644 index 000000000..bca5a4bc4 --- /dev/null +++ b/temporalio/contrib/openai_agents/README.md @@ -0,0 +1,51 @@ +# OpenAI Agents SDK integration + +The OpenAI Agents integration has moved to the independently versioned +[`temporalio-openai-agents`](https://pypi.org/project/temporalio-openai-agents/) +package. Its canonical Python API is `temporalio.openai_agents`. + +The complete integration guide now lives in the +[`temporalio-openai-agents` README](https://github.com/temporalio/ai-integrations/tree/main/python/openai_agents#readme). + +## Migrating to Temporal 1.34 + +Existing users can keep their complete Temporal extras list, including +`openai-agents`. Update the Temporal SDK version to 1.34.0 or later: + +```toml +# Before +dependencies = [ + "temporalio[openai-agents,otel,pydantic]>=1.33.0", +] + +# After +dependencies = [ + "temporalio[openai-agents,otel,pydantic]>=1.34.0", +] +``` + +Starting with Temporal 1.34.0, the `openai-agents` extra installs the +standalone distribution. Existing public imports continue to work through +compatibility modules: + +```python +from temporalio.contrib.openai_agents import OpenAIAgentsPlugin +``` + +Applications can migrate independently to the canonical import: + +```python +from temporalio.openai_agents import OpenAIAgentsPlugin +``` + +New applications may depend on the standalone package directly instead of +using the forwarding extra: + +```bash +uv add temporalio-openai-agents +``` + +The standalone package can coexist with Temporal 1.33 because it installs at +`temporalio.openai_agents`, which does not overlap the SDK's bundled +`temporalio.contrib.openai_agents` implementation. On Temporal 1.33, use the +new canonical import to select the standalone implementation. From 4c5ed16df0f980b8101fbd2dd1219324acf7b76e Mon Sep 17 00:00:00 2001 From: Brian Strauch Date: Tue, 15 Sep 2026 20:37:43 -0700 Subject: [PATCH 08/16] Recommend direct OpenAI Agents dependency --- temporalio/contrib/openai_agents/README.md | 28 ++++++++++++---------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/temporalio/contrib/openai_agents/README.md b/temporalio/contrib/openai_agents/README.md index bca5a4bc4..17365e96a 100644 --- a/temporalio/contrib/openai_agents/README.md +++ b/temporalio/contrib/openai_agents/README.md @@ -9,8 +9,9 @@ The complete integration guide now lives in the ## Migrating to Temporal 1.34 -Existing users can keep their complete Temporal extras list, including -`openai-agents`. Update the Temporal SDK version to 1.34.0 or later: +Remove only the `openai-agents` extra from the existing Temporal dependency, +preserve every other Temporal extra, update the Temporal SDK to 1.34.0 or +later, and add the standalone package: ```toml # Before @@ -20,13 +21,21 @@ dependencies = [ # After dependencies = [ - "temporalio[openai-agents,otel,pydantic]>=1.34.0", + "temporalio[otel,pydantic]>=1.34.0", + "temporalio-openai-agents", ] ``` -Starting with Temporal 1.34.0, the `openai-agents` extra installs the -standalone distribution. Existing public imports continue to work through -compatibility modules: +Apply the same transformation regardless of how many extras are installed: +delete only `openai-agents` from the bracketed list, preserve all other extras, +and remove the brackets if no extras remain. Then install the standalone +package directly: + +```bash +uv add temporalio-openai-agents +``` + +Existing public imports continue to work through compatibility modules: ```python from temporalio.contrib.openai_agents import OpenAIAgentsPlugin @@ -38,13 +47,6 @@ Applications can migrate independently to the canonical import: from temporalio.openai_agents import OpenAIAgentsPlugin ``` -New applications may depend on the standalone package directly instead of -using the forwarding extra: - -```bash -uv add temporalio-openai-agents -``` - The standalone package can coexist with Temporal 1.33 because it installs at `temporalio.openai_agents`, which does not overlap the SDK's bundled `temporalio.contrib.openai_agents` implementation. On Temporal 1.33, use the From 2f515425867212a2675262bc1a9b49ae677ea47f Mon Sep 17 00:00:00 2001 From: Brian Strauch Date: Tue, 15 Sep 2026 20:39:19 -0700 Subject: [PATCH 09/16] Remove SDK upgrade from migration steps --- temporalio/contrib/openai_agents/README.md | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/temporalio/contrib/openai_agents/README.md b/temporalio/contrib/openai_agents/README.md index 17365e96a..14c4ad845 100644 --- a/temporalio/contrib/openai_agents/README.md +++ b/temporalio/contrib/openai_agents/README.md @@ -7,11 +7,11 @@ package. Its canonical Python API is `temporalio.openai_agents`. The complete integration guide now lives in the [`temporalio-openai-agents` README](https://github.com/temporalio/ai-integrations/tree/main/python/openai_agents#readme). -## Migrating to Temporal 1.34 +## Migrating to the standalone package Remove only the `openai-agents` extra from the existing Temporal dependency, -preserve every other Temporal extra, update the Temporal SDK to 1.34.0 or -later, and add the standalone package: +preserve every other Temporal extra and the existing Temporal version +constraint, and add the standalone package: ```toml # Before @@ -21,7 +21,7 @@ dependencies = [ # After dependencies = [ - "temporalio[otel,pydantic]>=1.34.0", + "temporalio[otel,pydantic]>=1.33.0", "temporalio-openai-agents", ] ``` @@ -35,13 +35,7 @@ package directly: uv add temporalio-openai-agents ``` -Existing public imports continue to work through compatibility modules: - -```python -from temporalio.contrib.openai_agents import OpenAIAgentsPlugin -``` - -Applications can migrate independently to the canonical import: +Change application imports to the standalone package: ```python from temporalio.openai_agents import OpenAIAgentsPlugin @@ -49,5 +43,5 @@ from temporalio.openai_agents import OpenAIAgentsPlugin The standalone package can coexist with Temporal 1.33 because it installs at `temporalio.openai_agents`, which does not overlap the SDK's bundled -`temporalio.contrib.openai_agents` implementation. On Temporal 1.33, use the -new canonical import to select the standalone implementation. +`temporalio.contrib.openai_agents` implementation. The new import path selects +the standalone implementation on both Temporal 1.33 and 1.34 or later. From 2038407570e844224ed4f970ccf4e19a87c57d57 Mon Sep 17 00:00:00 2001 From: Brian Strauch Date: Tue, 15 Sep 2026 20:41:09 -0700 Subject: [PATCH 10/16] Remove versions from migration guide --- temporalio/contrib/openai_agents/README.md | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/temporalio/contrib/openai_agents/README.md b/temporalio/contrib/openai_agents/README.md index 14c4ad845..e6aaecaa6 100644 --- a/temporalio/contrib/openai_agents/README.md +++ b/temporalio/contrib/openai_agents/README.md @@ -10,18 +10,17 @@ The complete integration guide now lives in the ## Migrating to the standalone package Remove only the `openai-agents` extra from the existing Temporal dependency, -preserve every other Temporal extra and the existing Temporal version -constraint, and add the standalone package: +preserve every other Temporal extra, and add the standalone package: ```toml # Before dependencies = [ - "temporalio[openai-agents,otel,pydantic]>=1.33.0", + "temporalio[openai-agents,otel,pydantic]", ] # After dependencies = [ - "temporalio[otel,pydantic]>=1.33.0", + "temporalio[otel,pydantic]", "temporalio-openai-agents", ] ``` @@ -41,7 +40,5 @@ Change application imports to the standalone package: from temporalio.openai_agents import OpenAIAgentsPlugin ``` -The standalone package can coexist with Temporal 1.33 because it installs at -`temporalio.openai_agents`, which does not overlap the SDK's bundled -`temporalio.contrib.openai_agents` implementation. The new import path selects -the standalone implementation on both Temporal 1.33 and 1.34 or later. +The new import path selects the standalone implementation instead of the +SDK-bundled `temporalio.contrib.openai_agents` implementation. From 1fbd21b58b372a10f8226365dd3a3b999a94f427 Mon Sep 17 00:00:00 2001 From: Brian Strauch Date: Tue, 15 Sep 2026 20:42:05 -0700 Subject: [PATCH 11/16] Simplify OpenAI Agents migration steps --- temporalio/contrib/openai_agents/README.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/temporalio/contrib/openai_agents/README.md b/temporalio/contrib/openai_agents/README.md index e6aaecaa6..4f41b8349 100644 --- a/temporalio/contrib/openai_agents/README.md +++ b/temporalio/contrib/openai_agents/README.md @@ -9,8 +9,8 @@ The complete integration guide now lives in the ## Migrating to the standalone package -Remove only the `openai-agents` extra from the existing Temporal dependency, -preserve every other Temporal extra, and add the standalone package: +Remove the `openai-agents` extra from the existing Temporal dependency and add +the standalone package: ```toml # Before @@ -26,9 +26,8 @@ dependencies = [ ``` Apply the same transformation regardless of how many extras are installed: -delete only `openai-agents` from the bracketed list, preserve all other extras, -and remove the brackets if no extras remain. Then install the standalone -package directly: +delete `openai-agents` from the bracketed list and remove the brackets if no +extras remain. Then install the standalone package directly: ```bash uv add temporalio-openai-agents From 80d24076d99ddbff9b003e29c1c935bd7f2a167d Mon Sep 17 00:00:00 2001 From: Brian Strauch Date: Tue, 15 Sep 2026 20:47:47 -0700 Subject: [PATCH 12/16] Apply batched suggestions from code review Co-authored-by: Brian Strauch --- temporalio/contrib/openai_agents/README.md | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/temporalio/contrib/openai_agents/README.md b/temporalio/contrib/openai_agents/README.md index 4f41b8349..8cd45c5e5 100644 --- a/temporalio/contrib/openai_agents/README.md +++ b/temporalio/contrib/openai_agents/README.md @@ -9,8 +9,7 @@ The complete integration guide now lives in the ## Migrating to the standalone package -Remove the `openai-agents` extra from the existing Temporal dependency and add -the standalone package: +Remove the `openai-agents` extra from the existing Temporal dependency: ```toml # Before @@ -21,13 +20,10 @@ dependencies = [ # After dependencies = [ "temporalio[otel,pydantic]", - "temporalio-openai-agents", ] ``` -Apply the same transformation regardless of how many extras are installed: -delete `openai-agents` from the bracketed list and remove the brackets if no -extras remain. Then install the standalone package directly: +Remove the brackets if no extras remain. Then install the standalone package directly: ```bash uv add temporalio-openai-agents From c7fa310ac339c20ca79e89bedc14eafaf80116ad Mon Sep 17 00:00:00 2001 From: Brian Strauch Date: Wed, 16 Sep 2026 10:26:49 -0700 Subject: [PATCH 13/16] Deprecate OpenAI Agents compatibility imports --- temporalio/contrib/openai_agents/__init__.py | 13 +++++++++++++ tests/test_contrib.py | 12 ++++++++++++ 2 files changed, 25 insertions(+) diff --git a/temporalio/contrib/openai_agents/__init__.py b/temporalio/contrib/openai_agents/__init__.py index f84013276..42dfdf246 100644 --- a/temporalio/contrib/openai_agents/__init__.py +++ b/temporalio/contrib/openai_agents/__init__.py @@ -1,8 +1,14 @@ """Compatibility imports for the standalone OpenAI Agents integration. New code should import :mod:`temporalio.openai_agents` directly. + +.. deprecated:: + Install ``temporalio-openai-agents`` and import + :mod:`temporalio.openai_agents` instead. """ +import warnings + from temporalio.openai_agents import ( AgentsWorkflowError, AllowAllWorkerEnvVars, @@ -18,6 +24,13 @@ from . import testing, workflow +warnings.warn( + "temporalio.contrib.openai_agents is deprecated; install " + "temporalio-openai-agents and import temporalio.openai_agents instead.", + DeprecationWarning, + stacklevel=2, +) + __all__ = [ "AgentsWorkflowError", "AllowAllWorkerEnvVars", diff --git a/tests/test_contrib.py b/tests/test_contrib.py index 8129a1cf5..5fe8f5873 100644 --- a/tests/test_contrib.py +++ b/tests/test_contrib.py @@ -1,3 +1,7 @@ +import importlib + +import pytest + import temporalio.contrib.openai_agents as compatibility import temporalio.contrib.openai_agents.testing as compatibility_testing import temporalio.contrib.openai_agents.workflow as compatibility_workflow @@ -6,6 +10,14 @@ import temporalio.openai_agents.workflow as standalone_workflow +def test_openai_agents_compatibility_deprecation() -> None: + with pytest.warns( + DeprecationWarning, + match=r"temporalio\.contrib\.openai_agents is deprecated", + ): + importlib.reload(compatibility) + + def test_openai_agents_compatibility_imports() -> None: assert compatibility.OpenAIAgentsPlugin is standalone.OpenAIAgentsPlugin assert compatibility.OpenAIPayloadConverter is standalone.OpenAIPayloadConverter From 72dcc01d8166ff2e177d009c38e99d546f05ffc5 Mon Sep 17 00:00:00 2001 From: Brian Strauch Date: Wed, 16 Sep 2026 14:20:36 -0700 Subject: [PATCH 14/16] Lock released OpenAI Agents integration --- pyproject.toml | 3 ++ temporalio/contrib/openai_agents/__init__.py | 2 +- temporalio/contrib/openai_agents/testing.py | 2 +- temporalio/contrib/openai_agents/workflow.py | 2 +- tests/test_contrib.py | 6 +-- uv.lock | 56 +++++++++++++++++++- 6 files changed, 64 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 9cad62b63..0639f4cc3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -83,6 +83,7 @@ dev = [ "toml>=0.10.2,<0.11", "twine>=4.0.1,<5", "maturin>=1.8.2", + "temporalio-openai-agents>=1.0.0", "pytest-cov>=6.1.1", "httpx>=0.28.1", "pytest-pretty>=1.3.0", @@ -291,3 +292,5 @@ exclude = ["temporalio/bridge/target/**/*", "temporalio/bridge/sdk-core/.git"] # Prevent uv commands from building the package by default package = false exclude-newer = "2 weeks" +# The SDK and integration releases are newer than the global cutoff. +exclude-newer-package = { temporalio = "2026-09-16T00:00:00Z", temporalio-openai-agents = "2026-09-17T00:00:00Z" } diff --git a/temporalio/contrib/openai_agents/__init__.py b/temporalio/contrib/openai_agents/__init__.py index 42dfdf246..0383c2038 100644 --- a/temporalio/contrib/openai_agents/__init__.py +++ b/temporalio/contrib/openai_agents/__init__.py @@ -9,7 +9,7 @@ import warnings -from temporalio.openai_agents import ( +from temporalio.openai_agents import ( # pyright: ignore[reportMissingImports] AgentsWorkflowError, AllowAllWorkerEnvVars, ModelActivityParameters, diff --git a/temporalio/contrib/openai_agents/testing.py b/temporalio/contrib/openai_agents/testing.py index 7d3223193..3aa765297 100644 --- a/temporalio/contrib/openai_agents/testing.py +++ b/temporalio/contrib/openai_agents/testing.py @@ -1,6 +1,6 @@ """Compatibility imports for OpenAI Agents testing helpers.""" -from temporalio.openai_agents.testing import ( +from temporalio.openai_agents.testing import ( # pyright: ignore[reportMissingImports] AgentEnvironment, ResponseBuilders, TestModel, diff --git a/temporalio/contrib/openai_agents/workflow.py b/temporalio/contrib/openai_agents/workflow.py index bf3a4bc44..8cdf48696 100644 --- a/temporalio/contrib/openai_agents/workflow.py +++ b/temporalio/contrib/openai_agents/workflow.py @@ -1,6 +1,6 @@ """Compatibility imports for OpenAI Agents workflow helpers.""" -from temporalio.openai_agents.workflow import ( +from temporalio.openai_agents.workflow import ( # pyright: ignore[reportMissingImports] AgentsWorkflowError, ToolSerializationError, activity_as_tool, diff --git a/tests/test_contrib.py b/tests/test_contrib.py index 5fe8f5873..73d41afcd 100644 --- a/tests/test_contrib.py +++ b/tests/test_contrib.py @@ -1,13 +1,13 @@ import importlib import pytest +import temporalio.openai_agents as standalone # pyright: ignore[reportMissingImports] +import temporalio.openai_agents.testing as standalone_testing # pyright: ignore[reportMissingImports] +import temporalio.openai_agents.workflow as standalone_workflow # pyright: ignore[reportMissingImports] import temporalio.contrib.openai_agents as compatibility import temporalio.contrib.openai_agents.testing as compatibility_testing import temporalio.contrib.openai_agents.workflow as compatibility_workflow -import temporalio.openai_agents as standalone -import temporalio.openai_agents.testing as standalone_testing -import temporalio.openai_agents.workflow as standalone_workflow def test_openai_agents_compatibility_deprecation() -> None: diff --git a/uv.lock b/uv.lock index 3fe5c0dc2..66574bcd7 100644 --- a/uv.lock +++ b/uv.lock @@ -12,6 +12,10 @@ resolution-markers = [ exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. exclude-newer-span = "P2W" +[options.exclude-newer-package] +temporalio = "2026-09-16T00:00:00Z" +temporalio-openai-agents = "2026-09-17T00:00:00Z" + [[package]] name = "aioboto3" version = "15.5.0" @@ -1403,6 +1407,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/91/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl", hash = "sha256:54f33de9f4f911d7e84e4191749cac8cc5653f815b06738c54db9a15ab8b1e42", size = 47300, upload-time = "2025-06-15T09:35:04.433Z" }, ] +[[package]] +name = "griffelib" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/b4/a767e91c606deefc447a96eaf59edd77397960b1d677dffd833ee8449831/griffelib-2.2.0.tar.gz", hash = "sha256:e1bc36fe9cd21d4b6b659b456346755e4cfdc5676c0a5214083126ee12612b3c", size = 227048, upload-time = "2026-08-16T14:04:58.383Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/b6/f65ac785d4ac90dcf7c831ac6256f5dd4a19780f4e1575b2c0d6eeebe319/griffelib-2.2.0-py3-none-any.whl", hash = "sha256:d71c3bc2bbed9f958488634fe788b843a9f705d6d2838ca32cd6c25eeb64dfc4", size = 166779, upload-time = "2026-08-16T14:04:54.365Z" }, +] + [[package]] name = "grpcio" version = "1.82.1" @@ -2809,6 +2822,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/b0/2291689e3ec4723fbf5bbf3b54afcd7b160f9ddc98ca7aedfd0132af5677/openai-2.45.0-py3-none-any.whl", hash = "sha256:5df105f5f8c9b711fcb9d06d2d3888cebc82506db216484c14a4e53cdf651777", size = 1629470, upload-time = "2026-07-09T18:02:42.21Z" }, ] +[[package]] +name = "openai-agents" +version = "0.20.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "griffelib" }, + { name = "mcp" }, + { name = "openai" }, + { name = "pydantic" }, + { name = "requests" }, + { name = "typing-extensions" }, + { name = "websockets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/18/1204bde976436ad85498b759d5297843ebd2e0a214f729eb2dc4423cdfc7/openai_agents-0.20.0.tar.gz", hash = "sha256:a6b37954877868ac233876a27dab1e2a461c53ecbd5fcf6e19e2d276d3e848cd", size = 6146452, upload-time = "2026-08-11T03:12:49.556Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/8b/18528f00cb1a5d0b8e00f457f7dc88e7b17439bc520861aae2b638b88710/openai_agents-0.20.0-py3-none-any.whl", hash = "sha256:aaff662b802fa90762ad539e131b9ea387e12e3664b87bc75157ad1b3fc88850", size = 1064338, upload-time = "2026-08-11T03:12:47.615Z" }, +] + [[package]] name = "openapi-schema-validator" version = "0.9.0" @@ -4695,6 +4726,9 @@ langgraph = [ langsmith = [ { name = "langsmith" }, ] +openai-agents = [ + { name = "temporalio-openai-agents" }, +] opentelemetry = [ { name = "opentelemetry-api" }, { name = "opentelemetry-sdk" }, @@ -4747,6 +4781,7 @@ dev = [ { name = "setuptools" }, { name = "strands-agents" }, { name = "strands-agents-tools" }, + { name = "temporalio-openai-agents" }, { name = "toml" }, { name = "twine" }, ] @@ -4779,11 +4814,12 @@ requires-dist = [ { name = "pydantic", marker = "extra == 'pydantic'", specifier = ">=2.0.0,<3" }, { name = "python-dateutil", marker = "python_full_version < '3.11'", specifier = ">=2.8.2,<3" }, { name = "strands-agents", marker = "extra == 'strands-agents'", specifier = ">=1.39.0" }, + { name = "temporalio-openai-agents", marker = "extra == 'openai-agents'", specifier = ">=1.0.0" }, { name = "types-aioboto3", extras = ["s3"], marker = "extra == 'aioboto3'", specifier = ">=10.4.0" }, { name = "types-protobuf", specifier = ">=3.20,<8.0.0" }, { name = "typing-extensions", specifier = ">=4.2.0,<5" }, ] -provides-extras = ["grpc", "opentelemetry", "pydantic", "google-adk", "langgraph", "langsmith", "deepagents", "lambda-worker-otel", "cloud-run-worker-otel", "aioboto3", "google-genai", "strands-agents"] +provides-extras = ["grpc", "opentelemetry", "pydantic", "openai-agents", "google-adk", "langgraph", "langsmith", "deepagents", "lambda-worker-otel", "cloud-run-worker-otel", "aioboto3", "google-genai", "strands-agents"] [package.metadata.requires-dev] dev = [ @@ -4826,10 +4862,28 @@ dev = [ { name = "setuptools", specifier = "<82" }, { name = "strands-agents", specifier = ">=1.39.0" }, { name = "strands-agents-tools", specifier = ">=0.5.2" }, + { name = "temporalio-openai-agents", specifier = ">=1.0.0" }, { name = "toml", specifier = ">=0.10.2,<0.11" }, { name = "twine", specifier = ">=4.0.1,<5" }, ] +[[package]] +name = "temporalio-openai-agents" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mcp" }, + { name = "openai" }, + { name = "openai-agents" }, + { name = "pydantic" }, + { name = "temporalio", extra = ["opentelemetry", "pydantic"] }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/5c/394834735b6987ab398bc37f22483d0e3239c509ba39e31eb56f3267584f/temporalio_openai_agents-1.0.0.tar.gz", hash = "sha256:ae49add8d91f40762b3cd7f4bbc13b93fc667b738b063212c59378d03733e623", size = 72601, upload-time = "2026-09-16T20:54:40.938Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/e8/d1f31f0ce6f50dac5ed669127a1408da87362678ce63489a9e0b328697df/temporalio_openai_agents-1.0.0-py3-none-any.whl", hash = "sha256:451944e7b2b02f32c7f9035d027f95b26be43913a705a757a8e2bf112dcbd5da", size = 71619, upload-time = "2026-09-16T20:54:39.561Z" }, +] + [[package]] name = "tenacity" version = "9.1.4" From 1518f0809e2ac4cfc30a40ff65a47ab63313b84f Mon Sep 17 00:00:00 2001 From: Brian Strauch Date: Wed, 16 Sep 2026 16:00:09 -0700 Subject: [PATCH 15/16] Address OpenAI integration review feedback --- pyproject.toml | 2 -- temporalio/__init__.py | 2 ++ temporalio/contrib/openai_agents/sandbox/__init__.py | 1 - uv.lock | 1 - 4 files changed, 2 insertions(+), 4 deletions(-) delete mode 100644 temporalio/contrib/openai_agents/sandbox/__init__.py diff --git a/pyproject.toml b/pyproject.toml index 0639f4cc3..c92d96ad1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -292,5 +292,3 @@ exclude = ["temporalio/bridge/target/**/*", "temporalio/bridge/sdk-core/.git"] # Prevent uv commands from building the package by default package = false exclude-newer = "2 weeks" -# The SDK and integration releases are newer than the global cutoff. -exclude-newer-package = { temporalio = "2026-09-16T00:00:00Z", temporalio-openai-agents = "2026-09-17T00:00:00Z" } diff --git a/temporalio/__init__.py b/temporalio/__init__.py index b94c19ef2..c2ecde122 100644 --- a/temporalio/__init__.py +++ b/temporalio/__init__.py @@ -12,6 +12,8 @@ from .service import __version__ as __sdk_version +# For example, find ``temporalio.openai_agents`` in site-packages when this SDK +# is imported from an editable checkout in a different directory. __path__ = _extend_path(__path__, __name__) __version__ = __sdk_version diff --git a/temporalio/contrib/openai_agents/sandbox/__init__.py b/temporalio/contrib/openai_agents/sandbox/__init__.py deleted file mode 100644 index ec92a33ee..000000000 --- a/temporalio/contrib/openai_agents/sandbox/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Compatibility package for OpenAI Agents sandbox support.""" diff --git a/uv.lock b/uv.lock index 66574bcd7..07a599815 100644 --- a/uv.lock +++ b/uv.lock @@ -13,7 +13,6 @@ exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for exclude-newer-span = "P2W" [options.exclude-newer-package] -temporalio = "2026-09-16T00:00:00Z" temporalio-openai-agents = "2026-09-17T00:00:00Z" [[package]] From 42e6d8d7fb62229f5c90a59d89af2f286bc7b832 Mon Sep 17 00:00:00 2001 From: Brian Strauch Date: Wed, 16 Sep 2026 16:21:33 -0700 Subject: [PATCH 16/16] Explain Temporal namespace extension --- temporalio/__init__.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/temporalio/__init__.py b/temporalio/__init__.py index c2ecde122..b7fe2da87 100644 --- a/temporalio/__init__.py +++ b/temporalio/__init__.py @@ -12,8 +12,11 @@ from .service import __version__ as __sdk_version -# For example, find ``temporalio.openai_agents`` in site-packages when this SDK -# is imported from an editable checkout in a different directory. +# After Python finds this ``temporalio`` package, it does not keep searching for +# other ``temporalio`` directories, such as one containing the separately +# installed ``temporalio-openai-agents`` package. Explicitly extend the search +# path so they can be found. Every package that provides ``temporalio/__init__.py`` +# must do this: https://docs.python.org/3.14/library/pkgutil.html#pkgutil.extend_path __path__ = _extend_path(__path__, __name__) __version__ = __sdk_version