Repository navigation
[Bug] strands-agents (TypeScript): a plugin Activity silently replaces a caller Activity with the same name #201
Description
What happens
StrandsPlugin (@temporalio/strands-agents) passes its Activities to SimplePlugin, whose
configureWorker merges them into the Worker's activities with { ...existing, ...param }.
When the Worker already has an Activity with one of the plugin's names, the plugin's function
replaces it. Worker.create succeeds, nothing is logged, and Workflows that call that name run
the plugin's handler instead of the caller's.
The plugin registers invokeModel and invokeModelStreaming, and <server>-listTools and
<server>-callTool for each MCP client in mcpClients. SimplePlugin documents plugin
Activities as "appended", and the Temporal docs tell callers to register their own Activities in
the Worker's activities. Neither reserves these names.
Reproduction
One file, no server. It calls the plugin's configureWorker, as Worker.create does. It exits 1
when a caller Activity is replaced.
// repro.cjs
// A caller Activity with one of StrandsPlugin's Activity names. No server: it calls the plugin's
// configureWorker, as Worker.create does. Exits 1 when the plugin replaces a caller Activity.
const { StrandsPlugin } = require('@temporalio/strands-agents');
const notCalled = () => {
throw new Error('not called');
};
const caller = async () => 'caller handler';
const plugin = new StrandsPlugin({ models: { claude: notCalled }, mcpClients: { docs: notCalled } });
let replaced = 0;
for (const name of ['invokeModel', 'docs-callTool']) {
try {
const { activities } = plugin.configureWorker({ taskQueue: 'tq', activities: { [name]: caller } });
const kept = activities[name] === caller;
console.log(`${name}: Worker options built; the caller's handler was ${kept ? 'kept' : "replaced by the plugin's"}`);
if (!kept) replaced += 1;
} catch (err) {
console.log(`${name}: ${err.name}: ${err.message}`);
}
}
process.exitCode = replaced > 0 ? 1 : 0;With @temporalio/strands-agents 1.24.0 (npm, with @strands-agents/sdk 1.6.0) and with the
package built from this repository's main (c664a34, with its locked @strands-agents/sdk
1.19.0), both with @temporalio/* 1.24.0 and Node 24.22:
$ node repro.cjs
invokeModel: Worker options built; the caller's handler was replaced by the plugin's
docs-callTool: Worker options built; the caller's handler was replaced by the plugin's
exit=1
On a real Worker and dev server, with both versions, a caller Workflow that calls its own
invokeModel Activity next to new StrandsPlugin({ models: { claude } }) got the plugin model's
stream events (answer from model claude) instead of the caller's result. The caller's handler ran
0 times, and nothing was logged at WARN or above. Control: on a Worker without the plugin, the
same Workflow gets caller handler got {"question":"caller data"}.
Expected
The caller's Activity runs, or Worker.create refuses the duplicate name with a clear error.
The Temporal Python and Go SDKs refuse a second Activity with the same name.
Impact
A silent wrong result: the Workflow gets the plugin's answer for a call to the caller's own
Activity, with no error or warning. The trigger needs a caller Activity with one of the plugin's
names. That is uncommon, but the names are not prefixed, so a caller invokeModel is not
far-fetched.
Proposed fix
In StrandsPlugin.configureWorker, before calling super.configureWorker, throw a TypeError
when one of the plugin's Activity names is already registered with a function that no
StrandsPlugin built. Activities a StrandsPlugin built stay replaceable, so one instance on both
the NativeConnection and the Worker still starts. This matches the fixes proposed for the same
bug in GoogleAdkPlugin (#159) and OpenAIAgentsPlugin (#165).
This is a breaking change: Workers with such a collision start today and would fail in
Worker.create. Activity names and History don't change. A PR follows.
Related: #160 reports the same bug in AiSdkPlugin, which also registers invokeModel and
invokeModelStreaming. On one Worker with StrandsPlugin, whichever plugin comes last owns those
names.
Activity
campoy commentedon Oct 27, 2026
One change to the proposed fix above: #202 now also refuses a second
StrandsPluginwith different options on one Worker, as #161 does forGoogleAdkPlugin, instead of letting anyStrandsPluginActivity replace another. The same instance on theNativeConnectionand the Worker, and instances built from one options object, still start.