feat(openai-agents): add @temporalio/openai-agents package#2024
feat(openai-agents): add @temporalio/openai-agents package#2024xumaple wants to merge 20 commits into
Conversation
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
…-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.
f173bf2 to
4507fe0
Compare
There was a problem hiding this comment.
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.
| "@temporalio/common": "workspace:*", | ||
| "@temporalio/proto": "workspace:*", | ||
| "nexus-rpc": "^0.0.2" | ||
| "@ungap/structured-clone": "^1.3.0", |
…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
ae2ff35 to
88adedb
Compare
`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>
| - contrib/ai-sdk | ||
| - contrib/interceptors-opentelemetry | ||
| - contrib/openai-agents | ||
| - contrib/workflow-polyfills |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Don't add a workflow-polyfills package just now. See my other comment for details.
| * | ||
| * Mirrors Python's `workflow.new_random()`. |
There was a problem hiding this comment.
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
| * | |
| * Mirrors Python's `workflow.new_random()`. |
There was a problem hiding this comment.
Don't duplicate Python's new_random() API. See my other comment for details.
| "path": "../nyc-test-coverage" | ||
| }, | ||
| { | ||
| "path": "../../contrib/ai-sdk" |
There was a problem hiding this comment.
Interesting that this wasn't being included before. Can we just include ../../contrib or is that bad practice?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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]], |
There was a problem hiding this comment.
The format here is [seconds, nanoseconds] so if you did [1, 0] - [0, 1] you would get [1, -1] instead of [0, 999999999]
There was a problem hiding this comment.
Claude's fix: use hrTimeDuration from @opentelemetry/core
|
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.
chris-olszewski
left a comment
There was a problem hiding this comment.
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'; |
There was a problem hiding this comment.
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)) { |
There was a problem hiding this comment.
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.
| "src", | ||
| "lib" |
There was a problem hiding this comment.
Strip out the test files from the shipped package
| "src", | |
| "lib" | |
| "src", | |
| "lib", | |
| "!src/__tests__", | |
| "!lib/__tests__" | |
| "require": "./lib/workflow.js", | ||
| "default": "./lib/workflow.js" | ||
| }, | ||
| "./load-polyfills": { |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Just to cover the big 3
| 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 |
| // 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'; |
There was a problem hiding this comment.
Does @openai/agents-core do stuff on import that requires our polyfills?
| "@opentelemetry/api": "^1.9.0", | ||
| "@opentelemetry/sdk-trace-base": "^1.25.1", |
There was a problem hiding this comment.
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).
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", | |||
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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 | |||
There was a problem hiding this comment.
Those tests belong into contrib/openai-agents/src/__tests__.
|
Note that I only skimmed over the actual |
|
You'll need to add the |
…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>
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 realModelProviderlives.API
Also ships
StatelessMCPServerProvider/StatefulMCPServerProvider, a replay-safe tracer provider, and anOpenAIAgentsTraceClientInterceptorfor end-to-end trace propagation.Correctness-critical behavior
Handoffobjects (no mutation); preservesonHandoff,inputType,isEnabled; cycle detection for cyclic handoff graphs.APIErrorfrom the OpenAI SDK, it inspectserror.statusanderror.headers, honorsx-should-retryandretry-after, and maps the result toModelInvocationErrorsubtypes (.RateLimit,.Authentication,.BadRequest,.ServerError,.Timeout,.Conflict).toolsarray (with a hint to usetool()oractivityAsTool()instead), and recurses into handoff agents' tools. Inlinetool()-built FunctionTools are supported and run in the Workflow sandbox; they must be deterministic.TemporalFailures buried in the cause chain are unwrapped rather than re-wrapped.Headers,ReadableStream,structuredClone,crypto.randomUUID(deterministic viauuid4()),EventTarget/Event/CustomEvent(workflow-safe, isolated listener errors).Tests
contrib/openai-agents/src/__tests__/covering classifier, agent conversion, polyfills, serialized model, errors, sink bridge, and client trace interceptor.packages/test/src/test-openai-agents.tsexercising the full plugin path withFakeModelProvider/GeneratorFakeModelProvider.