Repository navigation
Breaking: fail Worker creation when an OpenAI Agents plugin Activity would replace another Activity - #166
Breaking: fail Worker creation when an OpenAI Agents plugin Activity would replace another Activity#166campoy wants to merge 3 commits into
Conversation
…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.
90c0970 to
6cbe37a
Compare
DABH
left a comment
There was a problem hiding this comment.
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>(); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.' |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| - 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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
What was changed
OpenAIAgentsPluginnow refuses to replace a different Activity with one of its names, or to have one of its Activities replaced:super.configureWorker, the plugin checks each of its Activity names against the Worker'sactivities. An own enumerable entry whose value is a different function is refused, unless both were built from the same sources. A module-levelWeakMaprecords each Activity's name and sources:modelProviderandhostedToolCredentialsfor the model Activities, the provider object for a provider's Activities. So the same instance on theNativeConnectionand 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.runWorkerrefuses an Activity that a plugin applied afterOpenAIAgentsPluginreplaced.Worker.createapplies the Worker's plugins, then theNativeConnection's, soconfigureWorkercan't see those. It shuts the native Worker down before it polls, letsrun()release the connection, and throwsDuplicate registration of Activity '<name>': a plugin applied after OpenAIAgentsPlugin replaced its Activity with this name. ....OpenAIAgentsPlugin provider 'docs' would replace ...) and adds "Rename the provider" to the fixes.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 twoOpenAIAgentsPlugininstances 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 failsWorker.create; a later plugin's failsworker.run(). Rename or remove the other registration, put every provider in oneOpenAIAgentsPlugin, 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
SimplePluginmerges 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 secondOpenAIAgentsPlugin, or any later plugin, replace the plugin's Activities.Checklist
Closes [Bug] openai-agents: a plugin Activity silently replaces a caller Activity with the same name #165
How was this tested:
configureWorker refuses to replace a different Activity with the same nameinsrc/__tests__/plugin.test.ts: refuses a callerinvokeModelActivityanddocs-stateless-call-toolwith 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. Withplugin.tsfrommainit fails at its firstt.throws.runWorker refuses an Activity a later plugin replaced, and starts the Worker at onceinsrc/__tests__/test-openai-agents-comprehensive.ts, on a local dev server: with a laterSimplePluginthat registersinvokeModelActivity,worker.run()throws theTypeError, the Worker endsSTOPPED, andconnection.close()succeeds.runUntilstill stops a Worker after a callback that throws at once, and with two instances from one options object.make lintpasses (8 existing warnings, none in files this PR touches),make testpasses (247 tests),make buildpacks, andscripts/ci/check_conventions.pypasses.Worker.create(@temporalio/worker1.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 theTypeError. One instance on both sides, and two instances from one options object, start. A callerinvokeModelActivity, or another plugin's registered beforeOpenAIAgentsPlugin, failsWorker.create. Another plugin's registered after it passesWorker.create; therunWorkertest covers that case.main, and with the change both names fail with theTypeError. On real Workers with a local dev server, a callerinvokeModelActivity,docs-stateless-call-tool,docs-stateful-server-sessionandbox-sandbox-client-createeach failWorker.create, and no handler runs.CI covers the other Node versions and platforms.
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.