fix(aws-strands): forward plugins to per-thread agents#2141
Conversation
The StrandsAgent adapter builds a fresh strands.Agent per thread_id from a template Agent, copying init params via _extract_agent_kwargs. plugins is an Agent.__init__ param but Strands consumes the list during init and retains only a _plugin_registry -- the original objects are not stored as self.plugins/self._plugins, so they cannot be auto-extracted. The template Agent never serves a request, so a plugin registered there never runs its init_agent / before-invocation hooks on the per-thread agents that do. This silently breaks plugins whose behavior lives in those hooks -- most visibly AgentSkills: its skills activation tool leaks through (tools ARE copied) but reports 'skill not found' because skill loading happens in init_agent, which never runs. Fix mirrors the existing hooks handling: add an explicit StrandsAgent( plugins=...) kwarg, add 'plugins' to _AGUI_EXPLICIT_PARAMS so it is not auto-extracted, and forward it to every per-thread StrandsAgentCore (omitted when falsy, same rule as hooks). Adds tests/test_template_plugins_preservation.py (mirrors the hooks tests): forwarding, falsy-omission, and a real-core check that plugin.init_agent fires once per thread. All fail on pre-fix code, pass after.
BenTaylorDev
left a comment
There was a problem hiding this comment.
Mirrors the existing hooks forwarding cleanly — plugins excluded from auto-extraction (correct, since Strands consumes them into _plugin_registry), explicit kwarg, per-thread forward with the same falsy-omission rule.
Verified locally (Python 3.13, real deps): removing just the forwarding fails the two behavior tests, including the real-core one ("init_agent() … got 0, 2 expected") — so it's genuinely wiring plugins per thread, not just plumbing a kwarg. Full aws-strands suite: 160 passed / 6 skipped / 1 failed, and I confirmed the one failure (test_template_param_round_trips[interventions]) reproduces on main — it's the pre-existing newer-Strands-param gap you noted, unrelated to this change.
One minor note (non-blocking): list(self._plugins) forwards the same plugin objects to every per-thread agent, same as hooks — fine since init_agent re-runs per thread, just something to watch if a plugin ever carries mutable per-run state.
I bless the code with a glad heart.
Python Preview PackagesVersion
Install with uvAdd the TestPyPI index to your [[tool.uv.index]]
name = "testpypi"
url = "https://test.pypi.org/simple/"
explicit = trueThen install the packages you need: # Core SDK
uv add 'ag-ui-protocol==0.0.0.dev1783548658' --index testpypi
# Integrations (each already depends on the matching ag-ui-protocol preview)
uv add 'ag-ui-langgraph==0.0.0.dev1783548658' --index testpypi
uv add 'ag-ui-crewai==0.0.0.dev1783548658' --index testpypi
# NOTE: ag-ui-agent-spec depends on pyagentspec (git-only, not on PyPI).
# You will need to install pyagentspec separately from its git repo.
uv add 'ag-ui-agent-spec==0.0.0.dev1783548658' --index testpypi
uv add 'ag_ui_adk==0.0.0.dev1783548658' --index testpypi
uv add 'ag_ui_strands==0.0.0.dev1783548658' --index testpypiInstall with pippip install \
--index-url https://test.pypi.org/simple/ \
--extra-index-url https://pypi.org/simple/ \
ag-ui-protocol==0.0.0.dev1783548658
Commit: 2f64b38 |
@ag-ui/a2a-middleware
@ag-ui/a2ui-middleware
@ag-ui/event-throttle-middleware
@ag-ui/mcp-apps-middleware
@ag-ui/mcp-middleware
@ag-ui/a2a
@ag-ui/adk
@ag-ui/ag2
@ag-ui/agno
@ag-ui/aws-strands
@ag-ui/claude-agent-sdk
@ag-ui/crewai
@ag-ui/langchain
@ag-ui/langgraph
@ag-ui/llamaindex
@ag-ui/mastra
@ag-ui/pydantic-ai
@ag-ui/vercel-ai-sdk
@ag-ui/watsonx
@ag-ui/a2ui-toolkit
create-ag-ui-app
@ag-ui/client
@ag-ui/core
@ag-ui/encoder
@ag-ui/proto
commit: |
|
Addendum — verified the tool-registering-plugin case, since the tests use a no-tool init-counting plugin and the headline scenario (
So there's no double-registration failure even in the "plugins left on both template and StrandsAgent" case. One doc-only note (not a code issue): the fix is opt-in — plugins must be passed to |
|
Heads up — the Root cause: Fix: bump the For transparency: my approval above was on the strength of the fix's logic + a local run, but I ran against |
…s.plugins CI job aws-strands-python failed at collection: uv.lock pinned strands-agents==1.18.0, which predates the strands.plugins module. test_template_plugins_preservation.py imports strands.plugins.Plugin, so the whole suite errored out. Re-locked to strands-agents 1.47.0 (satisfies existing >=1.15.0 floor), which provides strands.plugins, strands.vended_plugins.skills.AgentSkills, and the Agent(plugins=...) kwarg. pyproject floors left unchanged.
Fixes #2129
Problem
StrandsAgentbuilds a freshstrands.Agentperthread_idfrom a templateAgent, copying constructor params via
_extract_agent_kwargs.pluginsis avalid
Agent.__init__parameter, but Strands consumes the plugin listduring init (registering each plugin's tools and hooks) and retains only a
_plugin_registry— the original plugin objects are not stored asself.plugins/self._plugins, so_extract_agent_kwargscannot recoverthem from the template.
The template Agent never serves a request, so a plugin registered on it never
runs its
init_agent/ before-invocation hooks on the per-thread agents thatactually do. This silently breaks any plugin whose behavior lives in those
hooks.
Most visible symptom —
AgentSkills: the plugin'sskillsactivationtool leaks through to per-thread agents (tools ARE copied), so the model sees
the tool and calls it, but every activation returns
Skill '<name>' not found. Available skills:(empty) — because skill loading happens ininit_agent,which never ran on the per-thread agent. The skill metadata is also never
injected into the per-thread system prompt.
This mirrors the exact class of bug already fixed for
hooksin this adapter.Fix
Same shape as the existing
hookshandling:plugins: list | None = Nonekwarg toStrandsAgent.__init__."plugins"to_AGUI_EXPLICIT_PARAMSso it is not auto-extracted fromthe template (it can't be — see above).
pluginsto every per-threadStrandsAgentCore, omitting thekwarg when falsy (same rule as
hooks, so we never passplugins=None/plugins=[]which a future Strands could read as "disable defaults").Usage after the fix
Tests
Adds
tests/test_template_plugins_preservation.py(mirrorstest_template_hooks_preservation.py):StrandsAgent(plugins=[...])appears in theper-thread
StrandsAgentCorekwargs;plugins=None/plugins=[]→ kwarg omitted entirely(parametrized);
strands.Agent, the plugin'sinit_agentfiresonce per thread (proves genuine wiring, not just kwarg plumbing).
All three fail on pre-fix code and pass after. Updates one comment in
test_template_agent_propagation.py(pluginsis now forwarded via explicitkwarg rather than "not forwarded").
Full existing suite: no new failures introduced by this change.
(Note:
test_template_param_round_trips[interventions]fails on currentmainindependent of this PR — a newer Strands
Agentparam the sentinel testdoesn't yet handle.)
CONTRIBUTING note
The guide asks to file an issue first for non-trivial work. Suggest opening a
short issue ("aws-strands Python: plugins dropped on per-thread agents; breaks
AgentSkills") and linking this PR, or ask a code owner to assign.
How to push (from a machine with GitHub auth)
Two artifacts in this dir:
0001-forward-plugins.patch—git format-patchoutputplugins-fix.bundle— git bundle carrying the branchOption A — apply the patch onto a fresh clone of your fork:
Option B — pull the branch from the bundle:
Verify before pushing: