Skip to content

feat(openai-agents): add @temporalio/openai-agents package#2024

Open
xumaple wants to merge 20 commits into
mainfrom
maplexu/openai-agents-plugin
Open

feat(openai-agents): add @temporalio/openai-agents package#2024
xumaple wants to merge 20 commits into
mainfrom
maplexu/openai-agents-plugin

Conversation

@xumaple
Copy link
Copy Markdown

@xumaple xumaple commented Apr 24, 2026

Summary

Adds @temporalio/openai-agents, a Temporal plugin that runs OpenAI Agents SDK agents as durable workflows. The agent loop (model reasoning, tool dispatch, handoffs) executes deterministically inside the workflow sandbox; each model invocation is dispatched to a Temporal activity where the real ModelProvider lives.

API

// Workflow side — from '@temporalio/openai-agents/workflow'
import '@temporalio/openai-agents/load-polyfills'; // first import in any workflow file
import { TemporalOpenAIRunner, activityAsTool, statelessMcpServer, statefulMcpServer } from '@temporalio/openai-agents/workflow';

const result = await new TemporalOpenAIRunner().run(agent, input);

// Worker side — from '@temporalio/openai-agents'
new OpenAIAgentsPlugin({
  modelProvider: new OpenAIProvider(),
  modelParams: { startToCloseTimeout: '30s' },
});

Also ships StatelessMCPServerProvider / StatefulMCPServerProvider, a replay-safe tracer provider, and an OpenAIAgentsTraceClientInterceptor for end-to-end trace propagation.

Correctness-critical behavior

  • Handoff conversion — shallow-clones user Handoff objects (no mutation); preserves onHandoff, inputType, isEnabled; cycle detection for cyclic handoff graphs.
  • Error classification — when the model Activity catches an APIError from the OpenAI SDK, it inspects error.status and error.headers, honors x-should-retry and retry-after, and maps the result to ModelInvocationError subtypes (.RateLimit, .Authentication, .BadRequest, .ServerError, .Timeout, .Conflict).
  • Tool validation — the runner rejects raw functions passed in the tools array (with a hint to use tool() or activityAsTool() instead), and recurses into handoff agents' tools. Inline tool()-built FunctionTools are supported and run in the Workflow sandbox; they must be deterministic.
  • AgentsWorkflowError — wraps non-Temporal errors; TemporalFailures buried in the cause chain are unwrapped rather than re-wrapped.
  • Sandbox polyfillsHeaders, ReadableStream, structuredClone, crypto.randomUUID (deterministic via uuid4()), EventTarget / Event / CustomEvent (workflow-safe, isolated listener errors).

Tests

  • 53 unit tests in contrib/openai-agents/src/__tests__/ covering classifier, agent conversion, polyfills, serialized model, errors, sink bridge, and client trace interceptor.
  • 8 integration tests in packages/test/src/test-openai-agents.ts exercising the full plugin path with FakeModelProvider / GeneratorFakeModelProvider.

@socket-security
Copy link
Copy Markdown

socket-security Bot commented Apr 27, 2026

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addednpm/​@​openai/​agents-openai@​0.3.9991007899100
Addednpm/​@​openai/​agents-core@​0.3.9991008099100

View full report

xumaple added a commit that referenced this pull request May 14, 2026
…-42)

Final review batch on PR #2024. 8 files / +619/-19. Audit fixes folded in
pre-commit.

- Item 36 — exact-count assertions added to comprehensive Test 2
  (`executeActivityCount=0`, `agentRunCount=9`, `generationCount=16`),
  closing the replay-doubling guard gap relative to Test 1.
- Item 37 — error-path comprehensive E2E added. `errorPathComprehensiveWorkflow`
  runs BasicAgent + EchoToolAgent + a FailingAgent whose `instructions` throws;
  the runner wraps it as `ApplicationFailure(type='AgentsWorkflowError')`.
  Test asserts (a) partial span tree for completed agents + the failing one,
  (b) follow-up workflow on the same isolate sees clean trace context.
- Item 38 — multi-workflow config isolation E2E strengthened. Now also
  partitions spans by trace ID and asserts `temporal:*` agent spans appear
  only in the `addTemporalSpans=true` workflow's trace.
- Item 39 — stateful-MCP comprehensive E2E. `statefulMcpComprehensiveWorkflow`
  exercises BasicAgent + WeatherToolAgent + StatefulMcpAgent + TriageAgent
  →Specialist in a single run.
- Item 40 — `TEMPORAL_ACTIVITY_TOOL_MARKER` symbol and the writer that
  stamped it removed entirely. Zero readers existed in the codebase; the
  prior `(t as any)[MARKER]` was dead infrastructure. Cleaner than swapping
  it for an also-dead WeakMap.
- Item 41 — `BaseAgentTracingProcessor` constructor now throws loudly when
  the global TracerProvider's delegate isn't a `ReplaySafeTracerProvider`.
  Replaces the prior silent no-op fallback for the misconfiguration case.
  WARNING comment documents both the "OTel internals changed" trigger and
  the "user enabled addTemporalSpans without registering a provider" trigger.
- Item 42 — three user-facing error messages rewritten to be actionable
  (stateful MCP schedule/heartbeat/not-connected). Public-contract JSDoc
  above `DEDICATED_WORKER_*_FAILURE_MESSAGE` constants updated to reflect
  the pre-publish iteration window (the load-bearing contract is
  `DEDICATED_WORKER_FAILURE_TYPE`, not the messages).

Tests: 14/14 tracing + openai-agents suite passes (modulo 3 pre-existing
rate-limit flakes unrelated to this batch). Lint clean for all source
files in scope.
@xumaple xumaple force-pushed the maplexu/openai-agents-plugin branch 2 times, most recently from f173bf2 to 4507fe0 Compare May 18, 2026 19:55
@brianstrauch brianstrauch requested a review from Copilot May 18, 2026 20:23
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds @temporalio/openai-agents, a new Temporal integration package for running OpenAI Agents SDK agent loops durably in workflows while delegating model/tool/MCP operations to Temporal activities.

Changes:

  • Adds the OpenAI Agents package with workflow/worker/client tracing, model activity delegation, MCP support, polyfills, and tests.
  • Adds deterministic newRandom() support and shared workflow sandbox polyfills.
  • Wires the new package into the workspace, lockfile, and integration test suite.

Reviewed changes

Copilot reviewed 79 out of 80 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
pnpm-workspace.yaml Adds packages/openai-agents to the workspace.
pnpm-lock.yaml Adds dependencies and workspace links for the new package and tests.
package.json Adds root workspace dependency on @temporalio/openai-agents.
eslint.config.mjs Adds restricted-import lint rules for the new package.
packages/ai-sdk/package.json Moves shared workflow polyfill dependencies to @temporalio/workflow.
packages/ai-sdk/src/load-polyfills.ts Reuses the shared workflow polyfills module.
packages/workflow/package.json Adds shared Web API polyfill dependencies.
packages/workflow/src/alea.ts Allows readonly seeds for deterministic RNG construction.
packages/workflow/src/internals.ts Tracks the current randomness seed and seed version.
packages/workflow/src/polyfills.ts Adds workflow sandbox Web API polyfills.
packages/workflow/src/workflow.ts Adds newRandom() deterministic RNG helper.
packages/openai-agents/package.json Defines the new package metadata, exports, dependencies, and scripts.
packages/openai-agents/tsconfig.json Adds TypeScript project config for the new package.
packages/openai-agents/src/index.ts Exposes worker/client-side public API.
packages/openai-agents/src/workflow.ts Exposes workflow-safe public API.
packages/openai-agents/src/client/trace-interceptor.ts Adds client-side trace/config propagation.
packages/openai-agents/src/common/agent-sink-types.ts Defines workflow-to-host agent tracing sink wire types.
packages/openai-agents/src/common/base-tracing-processor.ts Maps OpenAI Agents tracing events to OTel spans.
packages/openai-agents/src/common/errors.ts Adds Temporal failure unwrapping helper.
packages/openai-agents/src/common/headers.ts Adds trace/config/OTel header helpers.
packages/openai-agents/src/common/mcp-types.ts Defines shared MCP activity names and types.
packages/openai-agents/src/common/model-activity-options.ts Defines model activity configuration types.
packages/openai-agents/src/common/otel-internals.ts Adds OTel internal provider/id-generator helpers.
packages/openai-agents/src/common/otel-sink-types.ts Defines workflow-to-host OTel sink wire types.
packages/openai-agents/src/common/serialized-model.ts Defines JSON-safe model request/response wire shapes.
packages/openai-agents/src/common/trace-context.ts Restores and isolates OpenAI Agents trace context.
packages/openai-agents/src/common/tracing-bridge.ts Bridges OpenAI Agents IDs/span data to OTel.
packages/openai-agents/src/tracer-provider.ts Adds replay-safe OTel tracer provider.
packages/openai-agents/src/worker/activities.ts Adds model invocation activity implementation.
packages/openai-agents/src/worker/activity-tracing.ts Adds activity-side tracing processor registration.
packages/openai-agents/src/worker/agent-sink-bridge.ts Bridges workflow agent tracing sink events to host processors.
packages/openai-agents/src/worker/heartbeat.ts Adds adaptive activity heartbeat helper.
packages/openai-agents/src/worker/mcp-provider.ts Adds stateless MCP provider activity registration.
packages/openai-agents/src/worker/otel-sink-bridge.ts Bridges workflow OTel sink events to host processors.
packages/openai-agents/src/worker/plugin.ts Adds worker plugin integration and sink/interceptor wiring.
packages/openai-agents/src/worker/stateful-mcp-provider.ts Adds stateful MCP provider with dedicated per-run worker.
packages/openai-agents/src/worker/trace-interceptor.ts Adds activity inbound trace interceptor.
packages/openai-agents/src/workflow/activity-backed-model.ts Delegates model calls from workflows to activities.
packages/openai-agents/src/workflow/agent-sink-processor.ts Emits workflow agent tracing events through sinks.
packages/openai-agents/src/workflow/convert-agent.ts Converts agent graphs to activity-backed models.
packages/openai-agents/src/workflow/load-polyfills.ts Adds OpenAI Agents workflow polyfills.
packages/openai-agents/src/workflow/mcp-client.ts Adds workflow-side stateless MCP client.
packages/openai-agents/src/workflow/otel-context-manager.ts Adds workflow OTel context manager.
packages/openai-agents/src/workflow/otel-sink-processor.ts Emits workflow OTel spans through sinks.
packages/openai-agents/src/workflow/placeholder-model-provider.ts Adds safety placeholder model provider.
packages/openai-agents/src/workflow/plugin-config-store.ts Stores per-workflow plugin config.
packages/openai-agents/src/workflow/prng.ts Adds PRNG/span ID helpers for handler tracing.
packages/openai-agents/src/workflow/runner.ts Adds Temporal OpenAI runner implementation.
packages/openai-agents/src/workflow/span-helpers.ts Adds temporal span/header injection helpers.
packages/openai-agents/src/workflow/stateful-mcp-client.ts Adds workflow-side stateful MCP client.
packages/openai-agents/src/workflow/tools.ts Adds activity-as-tool wrapper.
packages/openai-agents/src/workflow/trace-interceptor.ts Adds workflow inbound/outbound trace interceptor.
packages/openai-agents/src/workflow/tracing.ts Adds workflow tracing processor setup.
packages/openai-agents/src/__tests__/classifier.test.ts Adds model error classification tests.
packages/openai-agents/src/__tests__/client-trace-interceptor.test.ts Adds client interceptor tests.
packages/openai-agents/src/__tests__/convert-agent.test.ts Adds agent conversion tests.
packages/openai-agents/src/__tests__/errors.test.ts Adds error normalization tests.
packages/openai-agents/src/__tests__/polyfills.test.ts Adds polyfill tests.
packages/openai-agents/src/__tests__/serialized-model.test.ts Adds model serialization tests.
packages/openai-agents/src/__tests__/sink-bridge.test.ts Adds tracing sink bridge tests.
packages/test/package.json Adds test dependencies for OpenAI Agents integration tests.
packages/test/tsconfig.json Adds project reference for the new package.
packages/test/src/activities/openai-agents.ts Adds basic activity fixtures for agent tests.
packages/test/src/activities/openai-agents-comprehensive.ts Adds comprehensive activity fixtures.
packages/test/src/helpers-integration.ts Adds plugin forwarding for test workflow bundling/environment setup.
packages/test/src/stubs/openai-agents.ts Adds reusable model/provider test stubs.
packages/test/src/stubs/openai-agents-comprehensive.ts Adds comprehensive MCP/model stubs.
packages/test/src/stubs/openai-agents-fakes.ts Adds fake model response builders.
packages/test/src/test-openai-agents.ts Adds OpenAI Agents integration tests.
packages/test/src/test-otel.ts Removes obsolete lint suppression.
packages/test/src/test-workflows.ts Adds newRandom() workflow tests.
packages/test/src/workflows/index.ts Exports new random test workflows.
packages/test/src/workflows/new-random.ts Adds workflows for deterministic newRandom() behavior.
packages/test/src/workflows/openai-agents.ts Adds workflow fixtures for OpenAI Agents tests.
packages/test/src/workflows/openai-agents-tracing.ts Adds tracing workflow fixtures.
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread contrib/openai-agents/src/worker/activities.ts
Comment thread contrib/openai-agents/src/workflow/convert-agent.ts
Comment thread packages/workflow/package.json Outdated
"@temporalio/common": "workspace:*",
"@temporalio/proto": "workspace:*",
"nexus-rpc": "^0.0.2"
"@ungap/structured-clone": "^1.3.0",
Comment thread packages/openai-agents/src/worker/plugin.ts Outdated
Comment thread packages/openai-agents/src/workflow/prng.ts Outdated
Comment thread contrib/openai-agents/src/workflow.ts
Comment thread contrib/openai-agents/package.json
Comment thread packages/openai-agents/src/worker/activities.ts Outdated
Comment thread contrib/openai-agents/README.md
@xumaple xumaple marked this pull request as ready for review May 18, 2026 21:14
@xumaple xumaple requested a review from a team as a code owner May 18, 2026 21:14
xumaple added 4 commits May 18, 2026 17:40
…tion

Reframe the README around upstream Agents SDK reuse:
- Top intro emphasizes that users keep writing standard agents.
- Runner section presents TemporalOpenAIRunner as a drop-in for upstream Runner.
- Tools section gains an umbrella paragraph; activityAsTool framed as the one Temporal addition.
- Tracing intro names the upstream entry points (setDefaultOpenAITracingExporter, addTraceProcessor, withCustomSpan) and states replay safety as a user-visible behavior.

Restructure Tracing to separate the two axes:
- ### Destinations (OpenAI dashboard, OpenTelemetry, Custom TracingProcessor) — where traces go.
- ### Temporal-orchestration spans — what gets emitted.
OpenAI hosted dashboard collapsed to one sentence; addTemporalSpans no longer paired with useOtelInstrumentation in the example.

Capitalize Temporal-concept keywords (Workflow, Activity, Worker, Task Queue, Run) in prose, code comments, and tables.

Remove em-dashes from prose throughout; use periods, parens, or inline phrasing instead.
Review feedback:
- Drop dead `configureBundler` override in `OpenAIAgentsPlugin` — `SimplePlugin.configureBundler` already appends `workerInterceptors.workflowModules`, and the bundler de-dupes via `[...new Set(...)]`, so the override was redundant.
- Hash query/update-ID inputs into the span ID via FNV-1a-32 (three salted hashes → 96 bits) instead of stripping non-hex characters. Stripping caused user-supplied IDs like `abc` and `a-b-c` to collide.
- Start the adaptive heartbeat before resolving the model. Slow `ModelProvider.getModel` implementations (e.g. providers that fetch tokens during resolution) could otherwise exceed `heartbeatTimeout` before any heartbeat fires.

Rebase fixups:
- `Activity.info().workflowExecution` is now optional on the type; assert non-null in the stateful-MCP per-Run helpers (these Activities only run from Workflows).
- Re-import `TestWorkflowEnvironment` as a value in `helpers-integration.ts` (the inlined `createFromExistingServer` call uses it as a value, not just a type).
- Update `packages/test/tsconfig.json` reference to `../../contrib/ai-sdk` (main relocated `ai-sdk` from `packages/` to `contrib/`).
…nterceptors-opentelemetry

Follows the same restructuring main applied to `ai-sdk` and `interceptors-opentelemetry`: integration packages live under `contrib/`, not `packages/`. Pure rename + reference updates; no source changes.

- `packages/openai-agents/` → `contrib/openai-agents/`
- `pnpm-workspace.yaml`: drop old path, add new
- `packages/test/tsconfig.json`: reference `../../contrib/openai-agents`
- `contrib/openai-agents/tsconfig.json`: re-relativize `references` from `../<sibling>` to `../../packages/<sibling>`
- `contrib/openai-agents/package.json`: update `repository.directory` and `homepage`
- `eslint.config.mjs`: rescope restricted-import rule to new path
- `packages/test/src/test-openai-agents.ts`: update file-comment path reference
@xumaple xumaple force-pushed the maplexu/openai-agents-plugin branch from ae2ff35 to 88adedb Compare May 18, 2026 22:04
Comment thread contrib/workflow-polyfills/src/index.ts
xumaple and others added 6 commits May 18, 2026 18:23
`createTestWorkflowEnvironment` accepts a `plugins` option as part of `LocalTestWorkflowEnvironmentOptions`, but when routing through `TEMPORAL_SERVICE_ADDRESS` it dropped them on the floor — only the local-server branch (`createLocalTestEnvironment`) was forwarding them. Pass `opts?.plugins` to `createFromExistingServer` so external-server runs honor the option too.

This lets `packages/test/src/helpers-integration.ts` go back to a thin delegation (matching main's pattern) instead of re-implementing the env-var branch with `plugins` inlined.
…b package

Per @brianstrauch on #2024: `packages/workflow/src/polyfills.ts` pulled `headers-polyfill`, `@ungap/structured-clone`, and `web-streams-polyfill` (the latter is ~140KB) into `@temporalio/workflow`, the core Workflow package. Every consumer of `@temporalio/workflow` paid the install cost for polyfills only the OpenAI Agents and AI SDK integrations actually load.

New `@temporalio/workflow-polyfills` package at `contrib/workflow-polyfills/`:
- Owns the three Web-API polyfill deps.
- Side-effect import; same module body as before, repointed to `@temporalio/workflow` exports.

`contrib/ai-sdk` and `contrib/openai-agents` depend on it directly (`workspace:*`) and update their `load-polyfills.ts` shims to `import '@temporalio/workflow-polyfills'`.

`packages/workflow/package.json` drops the three deps. Pure restructure, no behavioral change — same polyfill code runs in the same Workflow contexts.
CI fails on rebuild because:

(1) Lockfile drift: my earlier `pnpm install` re-resolved the `ts-loader` peer-dependency on `typescript` to 5.9.3 (highest matching `^5.6.3`). TypeScript 5.9 narrowed `Uint8Array<T>` and `Buffer`/`ArrayBuffer` interop, which the worker / common / client packages don't yet conform to. The `nyc-test-coverage` package transitively pulls `ts-loader`, so its `tsc --build` was the one that exposed the breakage. Restored `pnpm-lock.yaml` to main's baseline so `ts-loader`'s peer typescript pins to 5.6.3 again (same as every other consumer in the repo).

(2) Three `@typescript-eslint/consistent-type-imports` errors from this PR's own code, caught by `pnpm run lint:check` (which we hadn't run locally yet):
   - `contrib/openai-agents/src/workflow/convert-agent.ts`: `Agent` used only as a type
   - `contrib/openai-agents/src/workflow/runner.ts`: same
   - `packages/workflow/src/workflow.ts`: `RNG` (introduced with the new `newRandom()` helper) used only as a type
…ot setCurrentSpan

Concurrent query/signal/update handlers on the same Workflow were having their `temporal:handle*:*` spans mis-parented under the wrong sibling handler's span. The CI test `Comprehensive trace tree — addTemporalSpans=true` exposed this; locally it passes because the race window is tighter.

Root cause: upstream `@openai/agents-core`'s `withCustomSpan`, `setCurrentSpan`, and `resetCurrentSpan` install the current span via `als.enterWith(...)`. Per Node's `AsyncLocalStorage` semantics, `enterWith` permanently rebinds the store for the current async resource and all microtasks/continuations scheduled FROM that resource — it does NOT scope to the enclosing `als.run(store, fn)` frame. When two `handleQuery` invocations interleave their await points, the `enterWith` from query #1's `setCurrentSpan` can leak into query #2's microtask chain, so query #2's `createCustomSpan` reads the wrong active store and parents under query #1's span.

Fix: introduce `withSpanAsCurrent(span, fn)` and `withScopedSpan(span, fn)` in `common/trace-context.ts`. Both install the span as current via `als.run({ ...currentStore, span }, fn)` — a fresh async resource that ALS confines `enterWith` mutations to. Replace every workflow-side call site that mutated context via upstream's enterWith-based helpers:

- `maybeTemporalSpan` (was `withCustomSpan`)
- `maybeTemporalSpanSync` (was `setCurrentSpan` + `resetCurrentSpan`)
- `captureHeaderUnderMaybeInstantSpan` (was `setCurrentSpan` + `resetCurrentSpan`)
- `startChildWorkflowExecution`'s manual span install (was `setCurrentSpan` + `resetCurrentSpan`, with the span outliving the function for the result-promise lifetime)

No call sites remain in workflow code that use `setCurrentSpan`/`resetCurrentSpan`/`withCustomSpan`. Host-side (client interceptor, activity interceptor) still uses them — Node's real ALS in the host process handles `enterWith` correctly when handlers don't race in the same isolate.
When Server delivers a Query via the legacy single-Query WFT path
(`PollWorkflowTaskQueueResponse.query`), Core stamps `queryId =
"legacy_query"` for every such Query. Multiple Queries on the same
Workflow execution therefore feed the same input to
`spanIdFromInputId`, producing the same agent-SDK spanId. The
OtelSpanCollector dedupes by spanId and overwrites the stored span
name on `onEnd`, so two colliding Queries appear in dumped traces as a
single span carrying the first Query's parent linkage and the second
Query's name — surfaced as a trace-tree assertion mismatch on
~1-in-3 runs under `reuseV8Context: true`.

Add a `resolveQueryKey` helper that returns `input.queryId` for the
modern path (queryId is a UUID) and `${queryName}:${getTimeOfDay()}`
for the legacy path. Wall-clock nanoseconds from Core via
`getActivator().getTimeOfDay()` — the same hatch the workflow logger
uses, callable from query handlers because it lives on the Activator
instance, not in `info.unsafe` (the latter loses function-typed
properties across the worker/VM context boundary during
`mutateWorkflowInfo`). The helper is deletable in one diff once Core
assigns unique IDs on the legacy path.

Also make `OtelSpanCollector.onStart` and
`AgentSdkSpanCollector.onSpanStart` throw on dedup with mismatched
span names. Silently overwriting the name on `onEnd` is what masked
this bug.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread pnpm-workspace.yaml
- contrib/ai-sdk
- contrib/interceptors-opentelemetry
- contrib/openai-agents
- contrib/workflow-polyfills
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like ai-sdk and openai-agents both use everything inside workflow-polyfills, but will that always be the case? I wonder if there might be cases where we're overimporting.

By the way, I'm discussing (with @chris-olszewski in #2039 (comment)) the benefits of polyfills vs native classes. Maybe that change could fix any issues with the size of the polyfills.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't add a workflow-polyfills package just now. See my other comment for details.

Comment thread packages/workflow/src/workflow.ts Outdated
Comment on lines +1157 to +1158
*
* Mirrors Python's `workflow.new_random()`.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think SDK compatibility is assumed, although I suppose it's good to know that this exists in sdk-python for the purposes of reviewing this PR

Suggested change
*
* Mirrors Python's `workflow.new_random()`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't duplicate Python's new_random() API. See my other comment for details.

"path": "../nyc-test-coverage"
},
{
"path": "../../contrib/ai-sdk"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting that this wasn't being included before. Can we just include ../../contrib or is that bad practice?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are project references, not include globs — they need an explicit tsconfig.json target, which we don't have in contrib/. But also, I think it's missing maybe because ai-sdk was only very recently moved out of packages/ into the new contrib/ dir, so I guess it was just missed. In the future, AI should be very capable of following the pattern for future contrib stuff so I'm not too worried about this. Let me know if you think otherwise.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we just include ../../contrib or is that bad practice?

Not possible.

But also, I think it's missing maybe because ai-sdk was only very recently moved out of packages/ into the new contrib/ dir, so I guess it was just missed.

A contrib's tests should now be defined inside that contrib's __test__ directory. As a result, there should be no need for this reference.

attributes: s.attributes as otel.Attributes,
links: s.links.map(reconstructLink),
events: s.events.map(reconstructEvent),
duration: [s.endTime[0] - s.startTime[0], s.endTime[1] - s.startTime[1]],
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The format here is [seconds, nanoseconds] so if you did [1, 0] - [0, 1] you would get [1, -1] instead of [0, 999999999]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude's fix: use hrTimeDuration from @opentelemetry/core

@brianstrauch
Copy link
Copy Markdown
Member

Other thoughts:

… comment

- otel-sink-bridge: use hrTimeDuration from @opentelemetry/core; manual
  [s,ns] subtraction produced [1,-1] instead of [0,999999999] when
  nanoseconds borrowed across the second boundary.
- workflow.ts (newRandom): remove "Mirrors Python's workflow.new_random()"
  JSDoc line — cross-SDK parity references add no value for TS users.

Addresses review comments from brianstrauch on PR #2024.
Copy link
Copy Markdown
Member

@chris-olszewski chris-olszewski left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partial review, sorry I didn't get too far past the package level. Will pick up review this afternoon.

}
// Web-API polyfills (Headers, ReadableStream, structuredClone, crypto.randomUUID)
// eslint-disable-next-line import/no-unassigned-import
import '@temporalio/workflow-polyfills';
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This polyfill contains more than what is necessary for the AI SDK integration. IMO we should polyfill the bare minimum

require('web-streams-polyfill/polyfill');

// structuredClone polyfill
if (!('structuredClone' in globalThis)) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure sharing a few lines of polyfill are worth a new public facing package. I would rather dupe these so each package gets exactly what it needs.

Comment on lines +102 to +103
"src",
"lib"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strip out the test files from the shipped package

Suggested change
"src",
"lib"
"src",
"lib",
"!src/__tests__",
"!lib/__tests__"

"require": "./lib/workflow.js",
"default": "./lib/workflow.js"
},
"./load-polyfills": {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to expose the polyfills and require users to manually import them? Could we copy the pattern from the AI SDK and import the polyfills in entrypoints of the package?

## Install

```bash
npm install @temporalio/openai-agents @openai/agents-core @openai/agents-openai openai
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to cover the big 3

Suggested change
npm install @temporalio/openai-agents @openai/agents-core @openai/agents-openai openai
# Or `pnpm add`/`yarn add`
npm install --save @temporalio/openai-agents @openai/agents-core @openai/agents-openai openai

Comment on lines +20 to +23
// MUST be the first import in any Workflow file using this package.
// It installs EventTarget/Event/CustomEvent and a working AsyncLocalStorage
// that the agent SDK needs before any agent-SDK code is evaluated.
import '@temporalio/openai-agents/load-polyfills';
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does @openai/agents-core do stuff on import that requires our polyfills?

Comment on lines +67 to +68
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/sdk-trace-base": "^1.25.1",
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unsure if worth the squeeze, but this is forcing all users to take OTEL dependencies even if they don't enable them. For Lambda we defined a separate entrypoint that allows us to declare these as optional peer deps:

- README: add top-of-file experimental banner (mirrors
  sdk-python contrib/langsmith README).
- worker/plugin.ts: add @experimental JSDoc tag at file level
  and on the OpenAIAgentsPlugin class declaration (mirrors
  contrib/ai-sdk/src/plugin.ts wording).
xumaple and others added 7 commits May 21, 2026 16:49
When the wrapped Activity returns void/undefined, JSON.stringify(undefined)
yields the literal undefined (not the string "undefined"), violating the
Promise<string> annotation on FunctionTool.invoke. Return '' explicitly for
the void case before stringifying.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Counterpart of activityAsTool for Nexus Operations. Wraps a Nexus
Operation as an OpenAI Agents FunctionTool: when the agent invokes the
tool, the call routes through a Workflow-side Nexus client to the
configured endpoint and the stringified result feeds back to the agent.

Agent-SDK trace context propagates end-to-end through the Nexus
boundary. A new Workflow outbound interceptor for startNexusOperation
injects the agent trace header (JSON-stringified into the string-shaped
Record<string, string> Nexus headers); a new Worker-side Nexus inbound
interceptor extracts the header and restores context for the handler
invocation, so spans emitted from inside the Nexus handler (e.g. via
createCustomSpan / withCustomSpan) nest under the Workflow-side trace
tree.

- workflow/nexus-tools.ts + 6 UTs + re-exports; nexus-rpc as devDep.
- common/headers.ts: injectAgentsTraceHeaderNexus +
  extractAgentsTraceHeaderNexus for the string-valued Nexus header shape.
- workflow/trace-interceptor.ts: outbound emits
  temporal:startNexusOperation:<service>.<operation> spans (gated on
  addTemporalSpans) and injects the trace header inside the span body.
- worker/trace-interceptor.ts: OpenAIAgentsTraceNexusInboundInterceptor
  restoring agent trace context for startOperation (cancelOperation not
  currently wrapped).
- worker/plugin.ts: registers the new Nexus inbound interceptor.
- packages/test: comprehensive E2E exercises a Nexus tool turn (bare
  invocation via nexusOperationAsTool) plus a user-span-wrapped manual
  Nexus client invocation; handler self-emits getCity_handler span; both
  EXPECTED_OTEL_TEST_* trees updated for addTemporalSpans=true and =false.
- README: new Tools > Nexus operation tools subsection; feature support
  and package exports tables updated.

Ports Python's temporalio.contrib.openai_agents.workflow.nexus_operation_as_tool.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
WorkflowSafeMemorySession is a Session implementation safe to use inside
a Workflow sandbox. State lives in an internal array on the Workflow
heap; durability comes from Event History (every model Activity result
that triggered an addItems is replayed deterministically). Upstream
MemorySession depends on Node-only utilities and is rejected at runtime
via an ApplicationFailure of type UnsafeSessionError.

TemporalRunOptions widens input to accept RunState directly (matching
upstream Runner.run), enabling resume across continueAsNew. On resume,
the runner re-runs RunState.fromString against the Activity-converted
agent graph (setCurrentAgent only swaps the top-level ref) and nulls
the persisted _trace and _currentAgentSpan so the resumed run anchors
under the current Workflow's trace rather than reviving the original.

- workflow/session.ts + UTs covering round-trip, no-sessionId outside
  Workflow, distinct instances, and the MemorySession guardrail.
- workflow/runner.ts: accept RunState in input position; reject upstream
  MemorySession upfront.
- workflow/load-polyfills.ts: narrow process polyfill (Proxy that allows
  env access and throws on any other property). Upstream sessionPersistence
  reads process.env without a typeof guard.
- packages/test: comprehensive E2E exercises a 3-execution chain
  (initial -> resume -> finalRound) where AgentA calls a needsApproval
  tool, the run interrupts, state is captured via result.state.toString(),
  passed via continueAsNew, rehydrated via RunState.fromString, approved,
  and resumed to completion. Sessions demoed by two sequential runner.run
  calls sharing one WorkflowSafeMemorySession. Both EXPECTED_OTEL_TEST_*
  trees updated for the new shape.
- README: new "Run state" subsection under The runner; new "Sessions"
  section between MCP servers and Tracing; feature support and package
  exports tables updated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wraps an Agent as an OpenAI Agents FunctionTool. When the parent agent
invokes the tool, the nested agent runs through TemporalOpenAIRunner so
its model calls go through ActivityBackedModel and remain durable.

Upstream Agent.asTool() is not usable inside a Workflow: its internal
new Runner({}) requires a default model provider (which the Temporal
plugin doesn't register; model calls are routed via Activities), and
the nested agent's model is never converted to ActivityBackedModel.
Failures in upstream's nested run are silently swallowed by
defaultToolErrorFunction, surfacing as a misleading successful empty
tool output rather than a real error. This helper substitutes the
Temporal runner.

- workflow/agent-tools.ts: agentAsTool helper + extractToolOutput
  (testable post-result branch). Throws ApplicationFailure of type
  NestedAgentInterruption when the nested run pauses for approval;
  nested approval bridging is not supported, handle approvals at the
  top-level runner.
- 6 UTs covering static fields, strict always true, needsApproval/
  isEnabled, malformed JSON, outside-Workflow failure, and the
  NestedAgentInterruption throw path.
- packages/test: comprehensive E2E exercises one model turn where
  AgentA delegates to AssistantAgent (a nested Agent with weatherTool,
  addNumbersTool, and lookupCity_bare); the nested run makes three
  tool calls (getWeather; addNumbers with user_inside_inline_tool
  span; lookupCity_bare with temporal:startNexusOperation +
  getCity_handler) before returning final text. Both
  EXPECTED_OTEL_TEST_* trees show the nested agent:AssistantAgent
  subtree nesting under the parent's function:delegate_task span.
- README: new "Nested agent tools" subsection under Tools; feature
  support and package exports tables updated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extracts the ctx.cancelled.catch(() => dedicatedWorker.shutdown()) block
from inside _getActivities() into a module-scope helper. Behavior is 1:1;
the extraction makes the cancel-shutdown wiring directly unit-testable
without standing up a real Worker + NativeConnection.

- worker/stateful-mcp-provider.ts: extract registerCancelShutdown helper;
  call site inside _getActivities() now invokes it.
- __tests__/stateful-mcp-cancel.test.ts: one UT that constructs a fake
  Context with a controllable cancelled Promise + a fake worker, calls
  the helper, rejects the Promise, asserts shutdown was called exactly
  once after microtasks drain.

Addresses the "prove teardown under cancellation" gap from
sdk-comparison.md for stateful MCP.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Across 12 files, deletes 15 slop comments and trims 7 verbose
JSDocs/comments to one-line forms. No production-code changes.

Deletes:
- Restate-the-field JSDocs on NexusOperationToolDefinition,
  NexusOperationAsToolOptions, AgentAsToolOptions fields.
- Implementation chatter in tests: getActivator call-chain narration,
  AsyncLocalStorage init explainer, "Bypass the constructor" note.
- Lazy-construction prose on agentAsTool (kept only the
  NestedAgentInterruption limitation warning).
- Restate-the-shape comment on injectAgentsTraceHeaderNexus.
- Step-list narration inside bigAgentInitialScript's JSDoc (kept
  one-line summary).

Trims:
- OpenAIAgentsTraceNexusInboundInterceptor class JSDoc → one line.
- NexusOperationToolDefinition interface JSDoc → one line.
- TemporalOpenAIRunner.run JSDoc → input-shapes paragraph only.
- Two startNexusOperation inline comments → tighter single lines.
- compNexusServiceHandler JSDoc → one line.
- Two README paragraphs in "Nested agent tools".

Net delta: 12 files, +11/-91.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@@ -0,0 +1,42 @@
{
"name": "@temporalio/workflow-polyfills",
Copy link
Copy Markdown
Contributor

@mjameswh mjameswh May 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please don't introduce a new package for this just yet. For now, duplicate the polyfill usage in each package.

It is very likely that third parties will duplicate the code of our ai-sdk and openai-agents packages as a base to develop other AI integrations. And so the @temporalio/workflow-polyfills package would implicitly become a public contract, from which we'll never be able to walk back without risking breaking those third-party packages.

There's clearly a need for us to provide some of those polyfills out-of-the-box, but we'll have to think this holistically.

*
* Mirrors Python's `workflow.new_random()`.
*/
export function newRandom(): RNG {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't. We already have WIP to add a proper API for this: #1992.

@@ -0,0 +1,433 @@
// Integration tests that need a real Worker. Pure-function tests live in
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those tests belong into contrib/openai-agents/src/__tests__.

@mjameswh
Copy link
Copy Markdown
Contributor

Note that I only skimmed over the actual @temporalio/openai-agents package, as that's under the AI team's responsibility. I raised several concerns regarding things outside of that package.

@mjameswh
Copy link
Copy Markdown
Contributor

You'll need to add the contrib/openai-agents path to the .github/CODEOWNERS file.

…correctness

Host-side `InjectedSink` previously kept Maps keyed by span/trace ID. When
a Workflow's `span_start` landed on Worker A and `span_end` on Worker B
(the default multi-Worker deployment shape), Worker B's empty map → "unknown
spanId" → silent drop. Tracing data was lost on every Workflow handoff.

Architectural shifts:
- Stateless host sink: each `span_complete` event is self-contained. No Maps,
  no Sets, no per-Workflow state on the bridge. Validates against the
  canonical pattern in `@temporalio/interceptors-opentelemetry`.
- Single-path agent-SDK → OTel bridging. All emission flows through host-side
  `ActivityTracingProcessor`; the Workflow-isolate OTel provider is gone.
- Wire shape: `span_start`/`span_end` collapsed to `span_complete`. Each event
  carries `forwardToOtel: boolean` stamped from the per-Workflow plugin config
  at emit time.
- Single header `__openai_span` carrying `{ traceName, traceId, spanId,
  otelSpanId? }`. `__openai_otel_trace` deleted; matches sdk-python's design.
- `SUPPRESS_OTEL_BRIDGE` symbol marker on Trace/Span objects, applied per-
  instance via `Object.defineProperty` (non-enumerable, non-writable,
  non-configurable) when `forwardToOtel: false`. Replaces the module-level
  `bridgeOriginated` WeakSet.
- Replay gating uses `workflowInfo().unsafe.isReplayingHistoryEvents` (not
  `isReplaying`, which drops live Query/Update handler work). Sink declares
  `callDuringReplay: true`.
- Deterministic ID generation via `TemporalIdGenerator` with ALS-scoped seeds
  (`withSeededIds(traceSeed?, spanSeed?, fn)`). Replaces a FIFO seed queue
  that could desync on missed consumption.
- `clearPluginConfig` moved out of the body's `finally` (which raced the
  `onSpanEnd` microtask and gated off span emission) and into `dispose` only.
- B5 outbound header symmetry: `scheduleActivity`, `scheduleLocalActivity`,
  `startChildWorkflow`, `continueAsNew`, `signalWorkflow`, `startNexusOperation`
  always inject the header, even when no agent-SDK trace exists. Previously
  short-circuited on `!getCurrentTrace()`.

New: handles upstream `Runner.run` skipping `.end()` on the current agent span
when a run terminates via `next_step_interruption`. Workflow-side processor
tracks open spans in `openSpansByWorkflow` keyed by `workflowId/runId`;
`flushOpenSpans()` (called from the runner's `finally` and the inbound
interceptor's `finally`) dispatches synthetic `span_complete` events for
anything left open.

Removed:
- `createTracerProvider`, `ReplaySafeTracerProvider`, `CreateTracerProviderOptions`
- Workflow-isolate OTel: `TemporalTracingProcessor`, `OtelSinkSpanProcessor`,
  `WorkflowContextManager`, `otel-sink-bridge`, `otel-context-manager`,
  `otel-internals`
- Host-side state machinery: `inFlightSpans`, `inFlightTraces`,
  `bridgeOriginated`, `internalProcessors`, `__resetAgentSinkForTest`,
  `setProcessors` monkey-patch
- `__openai_otel_trace` header, `injectOtelHeader`, `extractOtelHeader`
- `sink-bridge.test.ts` and `test-openai-agents-tracing.ts` (tested the
  broken design)

Coverage:
- Comprehensive E2E asserts the new wire shape across all entry points
  (start, Signal, Query, Update, child Workflow, continueAsNew, Activity,
  Nexus) for both `addTemporalSpans` settings.
- Unit tests for the `SUPPRESS_OTEL_BRIDGE` pipeline in
  `agent-sink-bridge.test.ts` and `flushOpenSpans` in
  `agent-sink-processor.test.ts`.
- Multi-Workflow isolation under `reuseV8Context` validated by the existing
  `Stateful MCP: multi-run isolation under reuseV8Context` test in
  `test-openai-agents.ts`, which exercises the shared per-Workflow storage
  layer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants