Skip to content

Breaking: fail Worker creation when an OpenAI Agents plugin Activity would replace another Activity - #166

Open
campoy wants to merge 3 commits into
mainfrom
fix/openai-agents-typescript-same-name-activities-replaced
Open

campoy wants to merge 3 commits into
mainfrom
fix/openai-agents-typescript-same-name-activities-replaced

Conversation

@campoy

@campoy campoy commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

What was changed

OpenAIAgentsPlugin now refuses to replace a different Activity with one of its names, or to have one of its Activities replaced:

TypeError: Duplicate registration of Activity '<name>': OpenAIAgentsPlugin would replace a different Activity with this name. Rename or remove the other Activity, run the other integration on its own Worker and task queue, or use one OpenAIAgentsPlugin instance on this Worker.
  • Before super.configureWorker, the plugin checks each of its Activity names against the Worker's activities. An own enumerable entry whose value is a different function is refused, unless both were built from the same sources. A module-level WeakMap records each Activity's name and sources: modelProvider and hostedToolCredentials for the model Activities, the provider object for a provider's Activities. So the same instance on the NativeConnection and the Worker, and two instances built from one options object, still start. Another instance with a different provider or credentials resolver is refused, as Fail Worker creation when GoogleAdkPlugin would replace another Activity (breaking) #161, Refuse a different Activity under an AiSdkPlugin Activity name (breaking) #162 and Breaking: fail Worker creation when a StrandsPlugin Activity would replace another Activity #202 do for their plugins.
  • runWorker refuses an Activity that a plugin applied after OpenAIAgentsPlugin replaced. Worker.create applies the Worker's plugins, then the NativeConnection's, so configureWorker can't see those. It shuts the native Worker down before it polls, lets run() release the connection, and throws Duplicate registration of Activity '<name>': a plugin applied after OpenAIAgentsPlugin replaced its Activity with this name. ....
  • For a provider Activity, the message names the provider (OpenAIAgentsPlugin provider 'docs' would replace ...) and adds "Rename the provider" to the fixes.
  • README: names the model Activities, says that provider Activity names start with the provider's name, which call fails when, and when two instances count as one.

Breaking change: Workers that start today with a collision now fail: a caller Activity named invokeModelActivity, invokeModelStreamActivity, or like a provider Activity (<name>-stateless-..., <name>-stateful-..., <name>-sandbox-...), another plugin's different Activity with one of these names, or two OpenAIAgentsPlugin instances built from different sources (with a prebuilt Workflow bundle these start today, and one instance's Activities silently serve every call). A registration before the plugin fails Worker.create; a later plugin's fails worker.run(). Rename or remove the other registration, put every provider in one OpenAIAgentsPlugin, or build both instances from one options object. Workers without a clash behave as before. The title starts with "Breaking:" because release notes come from commit subjects.

Rollback: this changes Worker configuration, not Workflow code. Activity names, inputs, results and History don't change, so recorded histories replay unchanged with this change and without it.

Why?

See #165. The SDK's SimplePlugin merges plugin Activities with { ...existing, ...param }, so a caller Activity with one of the plugin's names was replaced without a warning, and Workflows calling it ran the plugin's handler. The same merge let a second OpenAIAgentsPlugin, or any later plugin, replace the plugin's Activities.

Checklist

  1. Closes [Bug] openai-agents: a plugin Activity silently replaces a caller Activity with the same name #165

  2. How was this tested:

    • configureWorker refuses to replace a different Activity with the same name in src/__tests__/plugin.test.ts: refuses a caller invokeModelActivity and docs-stateless-call-tool with the exact messages, keeps caller Activities with other names, accepts the same instance twice and a second instance from the same options, and refuses a second instance with another model provider. With plugin.ts from main it fails at its first t.throws.
    • runWorker refuses an Activity a later plugin replaced, and starts the Worker at once in src/__tests__/test-openai-agents-comprehensive.ts, on a local dev server: with a later SimplePlugin that registers invokeModelActivity, worker.run() throws the TypeError, the Worker ends STOPPED, and connection.close() succeeds. runUntil still stops a Worker after a callback that throws at once, and with two instances from one options object.
    • Locally, in Docker (Node v24.22.0, pnpm 10.27.0, Linux arm64), at 64c6e89: make lint passes (8 existing warnings, none in files this PR touches), make test passes (247 tests), make build packs, and scripts/ci/check_conventions.py passes.
    • A probe through the real Worker.create (@temporalio/worker 1.24.0, no server; a last connection plugin captures the compiled options), at 64c6e89: instance A on the connection with instance B on the Worker, built with another provider, fails with the TypeError. One instance on both sides, and two instances from one options object, start. A caller invokeModelActivity, or another plugin's registered before OpenAIAgentsPlugin, fails Worker.create. Another plugin's registered after it passes Worker.create; the runWorker test covers that case.
    • At the first revision (6cbe37a): [Bug] openai-agents: a plugin Activity silently replaces a caller Activity with the same name #165's reproduction exits 0 (bug) on 1.24.0 and on main, and with the change both names fail with the TypeError. On real Workers with a local dev server, a caller invokeModelActivity, docs-stateless-call-tool, docs-stateful-server-session and box-sandbox-client-create each fail Worker.create, and no handler runs.

    CI covers the other Node versions and platforms.

  3. Any docs updates needed?
    The README paragraphs, included here.

Related: #161 (GoogleAdkPlugin), #162 (AiSdkPlugin) and #202 (StrandsPlugin) fix the same bug with the same policy and error wording. The PRs don't depend on each other.

…would replace another Activity

OpenAIAgentsPlugin merged its Activities over the Worker's `activities`
option. A caller Activity named `invokeModelActivity`,
`invokeModelStreamActivity`, or like a provider Activity
(`<name>-stateless-...`, `<name>-stateful-...`, `<name>-sandbox-...`) was
dropped with no error, and Workflows calling it ran the plugin's handler.

configureWorker now throws a TypeError for such a name. Activities that an
OpenAIAgentsPlugin built are still replaced, so one instance on both the
NativeConnection and the Worker still starts, and chained instances keep
the last one's Activities, as the README describes.

Breaking change: Worker.create now fails when another Activity on the
Worker has the name of an OpenAI Agents plugin Activity. Rename or remove
that Activity. Activity names, inputs and History do not change.
@campoy
campoy force-pushed the fix/openai-agents-typescript-same-name-activities-replaced branch from 90c0970 to 6cbe37a Compare October 30, 2026 08:57
@campoy
campoy marked this pull request as ready for review October 30, 2026 15:09
@campoy
campoy requested a review from a team as a code owner October 30, 2026 15:09

@DABH DABH left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Review at head 6cbe37a. On a worktree at the head with pnpm 10.27.0: make lint clean (8 pre-existing warnings), make test 244 of 244, including the new plugin test. Findings were checked with probes that call the real Worker.create (@temporalio/worker 1.24.0) with Worker and connection plugin lists and a final plugin that captures the options the SDK would compile. Checked and fine: a caller invokeModelActivity throws the new TypeError; the same instance on the connection and the Worker starts; the dedicated per-run Worker that a stateful MCP provider creates on a NativeConnection carrying the plugin starts too, because the plugin only adds its own Activities beside <name>-stateful-list-tools and -call-tool, which are not plugin names; replay Workers need no check (ReplayWorkerOptions omits activities and SimplePlugin.configureReplayWorker merges none). Refuted: clashes between provider kinds inside one plugin (no MCP or sandbox suffix ends with another kind's suffix), and a split WeakSet from a dual CommonJS/ES module install (the package ships one CommonJS build). The branch conflicts with #215 in plugin.ts (both add a private field and an assignment after super); keeping both sides builds, and the plugin and trace-interceptor tests pass. Not verified end to end: a model call through a running Worker in the two-instance case; the probe shows it by function identity in the compiled options.

Severity: P1 = wrong result or stuck run in realistic use; P2 = correctness or robustness in an edge case; P3 = docs, tests, cleanup.

Where the problems cluster: the check is correct for what it targets: a caller Activity, or an earlier plugin's, under one of the plugin's names. What remains silent is replacement between two OpenAIAgentsPlugin instances, which this PR keeps on purpose and lists as its open question. Because Worker.create runs NativeConnection plugins after Worker plugins, the instance that wins is the connection's. #161 refuses the same case for GoogleAdkPlugin, so the four TypeScript PRs (#161, #162, #202 and this one) currently disagree.

Commenting rather than approving: the check itself is right and CI is green, but the cross-instance policy below should be settled to match #161 before this lands, and the branch needs a rebase over #215.

// Every Activity function an OpenAIAgentsPlugin builds. configureWorker lets these be replaced, so
// one instance can be on both the NativeConnection and the Worker, and chained instances keep the
// last one's Activities.
const pluginActivityFunctions = new WeakSet<object>();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

P2: a second OpenAIAgentsPlugin instance still replaces the first one's Activities silently, and because Worker.create runs NativeConnection plugins after Worker plugins, the connection's instance wins, not the Worker's. Worker.create builds its list as (options.plugins ?? []).concat(options.connection?.plugins ?? []) (@temporalio/worker 1.24.0, lib/worker.js:196). Probe through the real Worker.create: instance A { modelProvider: providerA } on the connection, instance B { modelProvider: providerB, hostedToolCredentials, mcpServerProviders: [docs] } on the Worker. The compiled options run the plugins in the order B, A, and invokeModelActivity is A's function. Every model call uses providerA, and B's hostedToolCredentials resolver is never called; docs-stateless-call-tool stays B's. There is no error or log, the same silent wrong handler #165 describes, and the test at plugin.test.ts:42-44 locks it in with a different modelProvider. #161 refuses this for GoogleAdkPlugin and allows only the same instance twice or Activities built from the same sources. Fix, as one policy across the four TypeScript PRs: replace the WeakSet with a WeakMap from each Activity function to its sources (options.modelProvider and options.hostedToolCredentials for the two model Activities, the provider object for a provider's Activities). Allow replacement only when the existing function's sources match, element by element, this instance's sources for that name. One instance on the connection and the Worker, and two instances sharing their providers, keep starting. A different model provider, credentials resolver or server factory is refused. Flip the test's last case to t.throws and add a same-sources case. If last-wins stays on purpose, the README should say the connection's instance wins, and the PR body's 'Draft while this question is open' should go. P2 because it needs two instances with different options on one Worker, which per the PR body only starts with a prebuilt Workflow bundle.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Changed in 64c6e89 to match #161, #162 and #202. A WeakMap now records each Activity's name and sources: modelProvider and hostedToolCredentials for the model Activities, the provider object for a provider's Activities. One Activity may replace another only when both match by identity.

Your case (A with providerA on the connection, B with providerB, credentials and docs on the Worker) now fails Worker.create with the TypeError on invokeModelActivity. The same instance on both sides, and two instances from one options object, still start. The test's last case now expects the TypeError, and there is a same-options case. The PR body no longer lists this as open.

I also merged main in eadab02 for the #215 conflict, keeping both fields.

if (typeof fn === 'function' && !pluginActivityFunctions.has(fn)) {
throw new TypeError(
`Duplicate registration of Activity '${name}': OpenAIAgentsPlugin would replace a different Activity ` +
'with this name. Rename or remove the other registration.'

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

P3: for a provider Activity, the error does not name the provider or suggest renaming it, which is usually the easier fix. A caller docs-stateless-call-tool gets Duplicate registration of Activity 'docs-stateless-call-tool': OpenAIAgentsPlugin would replace a different Activity with this name. Rename or remove the other registration. The reader has to work out that the name comes from StatelessMCPServerProvider('docs'). Fix: record the provider name per Activity while building allActivities and, for those names, append , or rename the MCP server or sandbox client provider 'docs'. Non-blocking.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 64c6e89. For a provider Activity, the error now names the provider and suggests renaming it:

Duplicate registration of Activity 'docs-stateless-call-tool': OpenAIAgentsPlugin provider 'docs' would replace a different Activity with this name. Rename the provider, rename or remove the other Activity, run the other integration on its own Worker and task queue, or use one OpenAIAgentsPlugin instance on this Worker.

The rest of the wording is shared with #161, #162 and #202.

Comment thread typescript/openai-agents/README.md Outdated
- Adds Workflow, Activity, and Nexus interceptors for trace propagation.
- Installs Workflow bundle polyfills required by the OpenAI Agents SDK.

The model Activities are named `invokeModelActivity` and `invokeModelStreamActivity`. Provider Activity names start with the provider's name, such as `<name>-stateless-call-tool`. If another Activity on the Worker already has one of these names, `Worker.create` fails with a `TypeError` instead of replacing it. Activities from another `OpenAIAgentsPlugin` instance are still replaced.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

P3: 'already has one of these names' is accurate, but readers will take it to cover every clash. An Activity added after this plugin still replaces the plugin's silently. Probe through Worker.create: plugins [OpenAIAgentsPlugin, new SimplePlugin({ activities: { invokeModelActivity } })] start, with the other plugin's function serving invokeModelActivity; the reverse order throws. NativeConnection plugins always run after the Worker's plugins. Fix: say the check covers Activities registered before the plugin runs (the Worker's own activities and earlier plugins), and state which instance wins between two OpenAIAgentsPlugin instances, depending on the decision above. A check in SimplePlugin's merge would cover both orders for every plugin and may be worth an SDK issue. Non-blocking.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Covered in 64c6e89 with a check, the same one #162 now has. runWorker compares the Worker's compiled Activities with the plugin's. If a plugin applied later replaced one with a different function, it shuts the native Worker down before it polls, lets run() release the connection, and throws Duplicate registration of Activity '<name>': a plugin applied after OpenAIAgentsPlugin replaced its Activity with this name. ....

Your [OpenAIAgentsPlugin, new SimplePlugin({ activities: { invokeModelActivity } })] case now fails at worker.run(). A new real-Worker test checks the error, the STOPPED state, that connection.close() succeeds, and that runUntil still stops a Worker after its callback.

The README now says which call fails when: Worker.create for Activities registered before the plugin, worker.run() for a plugin applied after it, with the connection's plugins applied after the Worker's. Agreed that a check in SimplePlugin's merge would cover every plugin; I'll leave that to a separate SDK issue.

…replacements

configureWorker let any Activity an OpenAIAgentsPlugin built be replaced,
so a second instance with another model provider, credentials resolver or
MCP server or sandbox client provider replaced the first one's Activities
silently. Since Worker.create applies the NativeConnection's plugins after
the Worker's, the connection's instance won.

Each Activity now records its name and what it was built from: the
modelProvider and hostedToolCredentials for the model Activities, the
provider for a provider's Activities. An Activity may replace another only
when both entries match, so the same instance on the NativeConnection and
the Worker, and two instances built from one options object, still start.

A plugin applied after OpenAIAgentsPlugin could also replace its
Activities, which configureWorker cannot see. runWorker now refuses that
before the Worker polls: it shuts the native Worker down, lets run() release
the connection, and throws.

The error names the provider for a provider Activity and uses the wording
shared with the Google ADK, Vercel AI SDK and Strands plugins.

This branch has not been deployed

No deployments
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.

[Bug] openai-agents: a plugin Activity silently replaces a caller Activity with the same name

2 participants