Repository navigation
Fail Worker creation when GoogleAdkPlugin would replace another Activity (breaking) - #161
Conversation
…ity (breaking)
The SDK's plugin merge keeps the last Activity of each name. A caller
Activity named adk-invokeModel, adk-invokeModelStreaming or
<toolset>-listTools/-callTool/-listResources/-readResource, a second
GoogleAdkPlugin with a different modelProvider or toolset factory, or
another integration's MCP Activity with the same server name was
replaced without a warning, and the Worker ran the wrong handler.
GoogleAdkPlugin.configureWorker now throws a TypeError
("Duplicate registration of Activity '<name>'") when one of its
Activities would replace a different function. Each plugin Activity
records the operation it was built for and its modelProvider or toolset
factory, so another operation registered under the same name is
refused, while the same plugin instance registered twice (for example
on the NativeConnection and the Worker), or two instances built from
the same options object, still start.
The model Activities now read modelProvider once, when the plugin is
constructed, so the provider they call is the one recorded for the
check. Changing the options object after construction no longer changes
the provider. They still call it as a method of the options object, as
before, so a provider that reads `this` keeps working; that object is
recorded with the provider, so two instances with one provider in two
different options objects are refused.
Breaking change: Workers with such a collision used to start and now
fail in Worker.create. Rename or remove the other Activity or MCP
server, or put every model provider and toolset in one GoogleAdkPlugin.
Activity names, inputs and results do not change, so recorded histories
replay unchanged.
DABH
left a comment
There was a problem hiding this comment.
Review at head 56615d9 (two commits over 5e91c26). The branch merges cleanly with current main, and CI's pull_request run tested the merge commit after #114 had landed. On a worktree at the head with pnpm 10.27.0: make lint clean (1 pre-existing warning), make test 187 of 187. Findings were verified with probes against the built package and with real Worker.create calls on a local dev server: the same instance on the NativeConnection and the Worker starts and runs singleModelCall end to end; two instances built from one options object start; a caller Activity, two plugins with different providers, one plugin per toolset sharing one provider, and a helper that builds the plugin twice all reject with the TypeError; Worker.runReplayHistory with two conflicting plugins replays the recorded history. Checked and not a problem: replay Workers (constructReplayWorker calls only configureReplayWorker, which never touches Activities, worker.js:397-401); prebuilt bundles (withWorker already bundles with bundleWorkflowCode({ plugins }) and then passes the same plugins to Worker.create); hook order (Worker plugins run before connection plugins, worker.js:196, so a connection-registered instance sees everything the Worker registered); cross-integration names (a shared MCP server name with AiSdkPlugin would hand one integration's handler the other's arguments, so refusing is right); and the metadata comparison, which reads activityDefinitionOptions the way @temporalio/common does. Nothing material was left unverified.
Severity: P1 = wrong result or stuck run in realistic use; P2 = correctness or robustness in an edge case; P3 = docs, tests, cleanup.
On the open questions. (1) Shared policy: #166 keeps last-wins chaining for OpenAIAgentsPlugin because its README documents it. This plugin has no such contract and its silent replacement is exactly #159, so refusing here, as #162 and #202 do, is the right call for a Public Preview package. (2) One modelProvider in two options objects: the receiver rule is conservative but consistent with still calling the provider with this set to the options object. Keeping it is fine if the error says what to do (first inline comment). (3) A SimplePlugin-level check is the better long-term home; until then the copies of this helper in #161, #162 and #202 should keep one wording. (4) #114 has landed and the merge is clean. The body still ends with 'Draft while these questions are open' although the PR is marked ready.
Approving: the check does what the PR claims on every path exercised, CI is green, and the two remaining items are wording.
| if (other === undefined || other === fn || sameSources(other, fn)) continue; | ||
| throw new TypeError( | ||
| `Duplicate registration of Activity '${name}': ${plugin} would replace a different Activity with this name. ` + | ||
| `Rename or remove the other registration, or give each ${plugin} on this Worker the same options.` |
There was a problem hiding this comment.
P3: the error tells the user to 'give each GoogleAdkPlugin on this Worker the same options', which reads as value equality and does not name the fix for the two likeliest two-instance layouts. Probed on the head with real Worker.create: a helper makeAdk = () => new GoogleAdkPlugin({ modelProvider: p, mcpToolsets: { weather } }) called once for the NativeConnection and once for the Worker (identical values, distinct objects) fails with this message, and so does one plugin per toolset sharing one provider ([new GoogleAdkPlugin({ modelProvider: p, mcpToolsets: { weather } }), new GoogleAdkPlugin({ modelProvider: p, mcpToolsets: { news } })]), where the colliding name is adk-invokeModel, which the user cannot rename. In both cases the user already has 'the same options' by value; what works is reusing one instance or one options object, or putting every toolset in one plugin, and only the README says that. activitySources.has(other) already tells the cases apart (another GoogleAdkPlugin's functions are in this module's map; a caller Activity or another integration's are not), so the message can name the fix. For example, for another GoogleAdkPlugin: Duplicate registration of Activity '<name>': another GoogleAdkPlugin on this Worker registers it with a different modelProvider, toolset factory, options object or Activity metadata. Construct one GoogleAdkPlugin with every model provider and MCP toolset and pass that instance wherever it is needed. Otherwise: Duplicate registration of Activity '<name>': it is already registered by WorkerOptions.activities or an earlier plugin, and GoogleAdkPlugin would replace it. Rename that Activity or MCP server. Keep the Duplicate registration of Activity '<name>' prefix the README quotes. #162 and #202 carry the same wording, so the same change applies there.
There was a problem hiding this comment.
#161 merged before this was addressed, so the fix is in #339. The message there is the same as #162 and #202: ... Rename or remove the other Activity, run the other integration on its own Worker and task queue, or use one GoogleAdkPlugin instance on this Worker. The README now says when two instances count as the same.
| `<name>-listResources` and `<name>-readResource` for each MCP toolset. If the | ||
| Worker already has a different Activity with one of these names (your own | ||
| Activity, another integration's MCP Activity, or a second `GoogleAdkPlugin` | ||
| with a different `modelProvider` or toolset factory), `Worker.create` fails |
There was a problem hiding this comment.
P3: the list of refused setups is narrower than the rule, and 'Earlier versions kept the last one' is still true for a plugin listed after this one. (a) The rule includes the provider's options object (activities.ts:153; the JSDoc at plugin.ts:619-620 says so), so the same modelProvider in two options objects is refused, and so is a plugin without a modelProvider next to one with it ([new GoogleAdkPlugin({ mcpToolsets }), new GoogleAdkPlugin({ modelProvider })], which on main ended up with the provider plugin's model Activities). Both read as supported here, because neither has 'a different modelProvider or toolset factory'. (b) The check only sees Activities registered before this plugin's configureWorker runs: plugins: [new GoogleAdkPlugin(), new SimplePlugin({ name: 'mine', activities: { 'adk-invokeModel': mine } })] starts and registers mine (probe), and AiSdkPlugin after this plugin does the same until #162 lands. Suggested wording: 'Two GoogleAdkPlugins on one Worker must be the same instance or be built from one options object; two without a modelProvider also work when every shared toolset name uses the same factory function.' and 'A plugin listed after GoogleAdkPlugin can still replace these Activities unless it runs the same check.' Also, the 'unless setActivityOptions ...' clause cannot apply to the same instance (its functions are identical), so it belongs to the second case only.
There was a problem hiding this comment.
Addressed in #339, since #161 merged first. (a) The README now lists the setups the rule refuses, including one modelProvider in two options objects and a plugin without a modelProvider next to one with it. (b) runWorker now refuses a plugin applied after GoogleAdkPlugin, so a later replacement fails worker.run() before the Worker polls. The README says which registrations fail at Worker.create and which at worker.run().
What was changed
GoogleAdkPlugin.configureWorkernow throws when one of the plugin's Activities would replace a different Activity with the same name:super.configureWorker, the plugin compares its Activities with the Worker's, read the way the Worker reads them (own enumerable keys with function values).TypeErrorand the wording follow the Worker's ownDuplicate registration of nexus servicecheck.activity-sources.tsrecords, for each plugin Activity, the operation it was built for and what it was built from: themodelProviderand its options object for the model Activities, the toolset factory for the MCP Activities. Activities with the same operation and sources may replace each other. So the same instance on theNativeConnectionand the Worker, two instances built from one options object, and two defaultGoogleAdkPlugin()s still start. Another operation under a plugin name, such asadk-invokeModelStreamingregistered asadk-invokeModel, is refused.setActivityOptionsmetadata object. The Worker decodes arguments and encodes results with itstypeInfo, so a handler with other metadata is refused, in either order.modelProvideronce, at construction, so they call the provider that was recorded. They still call it as a method of the options object, as before, so a provider that readsthiskeeps working. Becausethiscan change what one provider returns, two instances that share onemodelProviderin two options objects are refused too.Breaking change: Workers that start today with a collision now fail in
Worker.create: a caller Activity with a plugin name, twoGoogleAdkPlugins with different options or different Activity metadata, or another integration's Activity with the same name registered first. Rename or remove the other registration, or put every model provider and toolset in oneGoogleAdkPlugin. Changing an options object after construction no longer changes the plugin's provider. The commit subject says so, because release notes come from it.Rollback: this changes Worker configuration, not Workflow code. Activity names, inputs and results don't change, so recorded histories replay unchanged with this change and with 0.1.0.
Why?
See #159. The SDK's plugin merge keeps the last Activity of each name, so a caller Activity, another
GoogleAdkPlugin's handlers, or another integration's MCP Activity was replaced without a warning, and Workflows reached the wrong handler.Checklist
Closes [Bug] google-adk: GoogleAdkPlugin silently replaces another Activity with the same name #159
How was this tested:
configureWorker refuses to replace a different Activity with the same nameinsrc/__tests__/plugin.test.ts. Onmainit fails at its first refusal (configureWorkerreturns instead of throwing). With this change it passes. It covers the refusals above, including differingsetActivityOptionsmetadata on model and MCP Activities, and the setups that still start. It also runs a provider that readsthisthroughMockActivityEnvironment.make lintpasses (1 existing warning).make testwith--concurrency=1: 186 passed onmain, 187 with this change.check_conventions.pypasses.main. With this change, both collisions fail with theTypeError.@temporalio/worker1.24.0, Temporal CLI 1.9.1), this change next to@temporalio/vercel-ai-sdk1.0.0: everyGoogleAdkPlugincollision failsWorker.create. These start and answer: each registration alone, the same instance on theNativeConnectionand the Worker, two instances from one options object, distinct toolset names, a provider that readsthis, and a bound provider.AiSdkPluginthenGoogleAdkPluginwith one MCP server name now fails. The other order still starts; [Bug] vercel-ai-sdk: AiSdkPlugin silently replaces another Activity with the same name #160 covers it.Worker.createwith two plugins from one options object: differingsetActivityOptionsmetadata fails in either order. The same metadata object, or none, starts and keeps the metadata.CI covers the other Node versions and platforms.
Any docs updates needed?
The README section, included here. The docs.temporal.io guide doesn't list the plugin's Activity names; a line can follow the release.
Related: #160 reports the same bug in
AiSdkPlugin. Its fix is a separate PR; the two don't depend on each other. With both, a shared MCP server name fails in either order.Open questions:
GoogleAdkPlugininstances are refused.OpenAIAgentsPlugin's README documents chained instances where the last one wins. Should the TypeScript plugins share one policy?modelProviderin two options objects (for example, one plugin per MCP toolset) is refused, because the plugin can't tell whether the provider readsthis. Comparing the provider only would accept it, but would silently replace a provider that readsthis. One plugin with every toolset still works.SimplePlugin(sdk-typescript) for every plugin instead? Not done here: it changes every plugin at once and needs an SDK release.plugin.ts,plugin.test.tsandREADME.md, in other places. Whichever lands second needs a rebase.Draft while these questions are open.