Skip to content

Fail Worker creation when GoogleAdkPlugin would replace another Activity (breaking) - #161

Merged
campoy merged 2 commits into
mainfrom
fix/google-adk-same-name-activities-replaced
Oct 30, 2026
Merged

campoy merged 2 commits into
mainfrom
fix/google-adk-same-name-activities-replaced

Conversation

@campoy

@campoy campoy commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

What was changed

GoogleAdkPlugin.configureWorker now throws when one of the plugin's Activities would replace a different Activity with the same name:

TypeError: Duplicate registration of Activity 'adk-invokeModel': GoogleAdkPlugin would replace a different Activity with this name. Rename or remove the other registration, or give each GoogleAdkPlugin on this Worker the same options.
  • Before super.configureWorker, the plugin compares its Activities with the Worker's, read the way the Worker reads them (own enumerable keys with function values). TypeError and the wording follow the Worker's own Duplicate registration of nexus service check.
  • A new activity-sources.ts records, for each plugin Activity, the operation it was built for and what it was built from: the modelProvider and 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 the NativeConnection and the Worker, two instances built from one options object, and two default GoogleAdkPlugin()s still start. Another operation under a plugin name, such as adk-invokeModelStreaming registered as adk-invokeModel, is refused.
  • Same-source Activities must also carry the same setActivityOptions metadata object. The Worker decodes arguments and encodes results with its typeInfo, so a handler with other metadata is refused, in either order.
  • The model Activities read modelProvider once, 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 reads this keeps working. Because this can change what one provider returns, two instances that share one modelProvider in two options objects are refused too.
  • README: the Activity names, the new error, and how to fix a collision.

Breaking change: Workers that start today with a collision now fail in Worker.create: a caller Activity with a plugin name, two GoogleAdkPlugins 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 one GoogleAdkPlugin. 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

  1. Closes [Bug] google-adk: GoogleAdkPlugin silently replaces another Activity with the same name #159

  2. How was this tested:

    • New test configureWorker refuses to replace a different Activity with the same name in src/__tests__/plugin.test.ts. On main it fails at its first refusal (configureWorker returns instead of throwing). With this change it passes. It covers the refusals above, including differing setActivityOptions metadata on model and MCP Activities, and the setups that still start. It also runs a provider that reads this through MockActivityEnvironment.
    • Locally, in Docker (Node v24.22.0, pnpm 10.27.0): make lint passes (1 existing warning). make test with --concurrency=1: 186 passed on main, 187 with this change. check_conventions.py passes.
    • [Bug] google-adk: GoogleAdkPlugin silently replaces another Activity with the same name #159's reproduction exits 0 (bug) on 0.1.0 and main. With this change, both collisions fail with the TypeError.
    • Real Workers with a local dev server (@temporalio/worker 1.24.0, Temporal CLI 1.9.1), this change next to @temporalio/vercel-ai-sdk 1.0.0: every GoogleAdkPlugin collision fails Worker.create. These start and answer: each registration alone, the same instance on the NativeConnection and the Worker, two instances from one options object, distinct toolset names, a provider that reads this, and a bound provider. AiSdkPlugin then GoogleAdkPlugin with 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.
    • Real Worker.create with two plugins from one options object: differing setActivityOptions metadata fails in either order. The same metadata object, or none, starts and keeps the metadata.

    CI covers the other Node versions and platforms.

  3. 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:

  • Two different GoogleAdkPlugin instances are refused. OpenAIAgentsPlugin's README documents chained instances where the last one wins. Should the TypeScript plugins share one policy?
  • One modelProvider in two options objects (for example, one plugin per MCP toolset) is refused, because the plugin can't tell whether the provider reads this. Comparing the provider only would accept it, but would silently replace a provider that reads this. One plugin with every toolset still works.
  • Should this check live in SimplePlugin (sdk-typescript) for every plugin instead? Not done here: it changes every plugin at once and needs an SDK release.
  • [AI-749] fix(google-adk): support @google/adk 2.1 to 2.3 #114 also edits plugin.ts, plugin.test.ts and README.md, in other places. Whichever lands second needs a rebase.

Draft while these questions are open.

…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.
@campoy
campoy marked this pull request as ready for review October 29, 2026 00:34
@campoy
campoy requested a review from a team as a code owner October 29, 2026 00:34
@campoy
campoy marked this pull request as draft October 29, 2026 16:34
@campoy
campoy marked this pull request as ready for review October 30, 2026 07:04

@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 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.`

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: 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.

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.

#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

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: 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.

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.

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().

@campoy
campoy merged commit ff14931 into main Oct 30, 2026
27 checks passed
@campoy
campoy deleted the fix/google-adk-same-name-activities-replaced branch October 30, 2026 22:31
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] google-adk: GoogleAdkPlugin silently replaces another Activity with the same name

2 participants