Skip to content

fix(aws-strands): forward plugins to per-thread agents#2141

Open
hiepcs wants to merge 2 commits into
ag-ui-protocol:mainfrom
hiepcs:fix/aws-strands-forward-plugins
Open

fix(aws-strands): forward plugins to per-thread agents#2141
hiepcs wants to merge 2 commits into
ag-ui-protocol:mainfrom
hiepcs:fix/aws-strands-forward-plugins

Conversation

@hiepcs

@hiepcs hiepcs commented Jul 8, 2026

Copy link
Copy Markdown

Fixes #2129

Problem

StrandsAgent builds a fresh strands.Agent per thread_id from a template
Agent, copying constructor params via _extract_agent_kwargs. plugins is a
valid Agent.__init__ parameter, but Strands consumes the plugin list
during init (registering each plugin's tools and hooks) and retains only a
_plugin_registry — the original plugin objects are not stored as
self.plugins / self._plugins, so _extract_agent_kwargs cannot recover
them 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 that
actually do. This silently breaks any plugin whose behavior lives in those
hooks.

Most visible symptom — AgentSkills: the plugin's skills activation
tool 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 in init_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 hooks in this adapter.

Fix

Same shape as the existing hooks handling:

  1. Add an explicit plugins: list | None = None kwarg to StrandsAgent.__init__.
  2. Add "plugins" to _AGUI_EXPLICIT_PARAMS so it is not auto-extracted from
    the template (it can't be — see above).
  3. Forward plugins to every per-thread StrandsAgentCore, omitting the
    kwarg when falsy (same rule as hooks, so we never pass plugins=None /
    plugins=[] which a future Strands could read as "disable defaults").

Usage after the fix

from strands import Agent, AgentSkills
from ag_ui_strands import StrandsAgent

template = Agent(model=..., system_prompt=..., tools=[...])
adapter = StrandsAgent(
    template, name="my-agent",
    plugins=[AgentSkills(skills="./skills/")],   # now forwarded per-thread
)

Tests

Adds tests/test_template_plugins_preservation.py (mirrors
test_template_hooks_preservation.py):

  • forwarding: a plugin passed to StrandsAgent(plugins=[...]) appears in the
    per-thread StrandsAgentCore kwargs;
  • falsy-omission: plugins=None / plugins=[] → kwarg omitted entirely
    (parametrized);
  • real-core: against the real strands.Agent, the plugin's init_agent fires
    once 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 (plugins is now forwarded via explicit
kwarg rather than "not forwarded").

Full existing suite: no new failures introduced by this change.
(Note: test_template_param_round_trips[interventions] fails on current main
independent of this PR — a newer Strands Agent param the sentinel test
doesn'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.patchgit format-patch output
  • plugins-fix.bundle — git bundle carrying the branch

Option A — apply the patch onto a fresh clone of your fork:

git clone git@github.com:<you>/ag-ui.git && cd ag-ui
git checkout -b fix/aws-strands-forward-plugins
git am /path/to/0001-forward-plugins.patch
git push -u origin fix/aws-strands-forward-plugins
gh pr create --repo ag-ui-protocol/ag-ui --title "fix(aws-strands): forward plugins to per-thread agents" --body-file PR.md

Option B — pull the branch from the bundle:

cd ag-ui   # your fork clone
git fetch /path/to/plugins-fix.bundle fix/aws-strands-forward-plugins:fix/aws-strands-forward-plugins
git push -u origin fix/aws-strands-forward-plugins

Verify before pushing:

cd integrations/aws-strands/python
uv venv --python 3.12 .venv && source .venv/bin/activate
uv pip install -e . pytest pytest-asyncio
python -m pytest tests/test_template_plugins_preservation.py -q   # 4 passed

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.
@hiepcs
hiepcs requested a review from a team as a code owner July 8, 2026 22:10
BenTaylorDev
BenTaylorDev previously approved these changes Jul 14, 2026

@BenTaylorDev BenTaylorDev left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@github-actions

Copy link
Copy Markdown
Contributor

Python Preview Packages

Version 0.0.0.dev1783548658 published to TestPyPI.

Warning: These packages are built from contributor code that may not yet have been vetted for correctness or security. Install at your own risk and do not use in production.

Install with uv

Add the TestPyPI index to your pyproject.toml:

[[tool.uv.index]]
name = "testpypi"
url = "https://test.pypi.org/simple/"
explicit = true

Then 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 testpypi

Install with pip

pip install \
  --index-url https://test.pypi.org/simple/ \
  --extra-index-url https://pypi.org/simple/ \
  ag-ui-protocol==0.0.0.dev1783548658

Use --extra-index-url https://pypi.org/simple/ so pip can resolve
transitive dependencies (pydantic, fastapi, etc.) from real PyPI.


Commit: 2f64b38

@pkg-pr-new

pkg-pr-new Bot commented Jul 14, 2026

Copy link
Copy Markdown

Open in StackBlitz

@ag-ui/a2a-middleware

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/a2a-middleware@2141

@ag-ui/a2ui-middleware

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/a2ui-middleware@2141

@ag-ui/event-throttle-middleware

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/event-throttle-middleware@2141

@ag-ui/mcp-apps-middleware

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/mcp-apps-middleware@2141

@ag-ui/mcp-middleware

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/mcp-middleware@2141

@ag-ui/a2a

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/a2a@2141

@ag-ui/adk

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/adk@2141

@ag-ui/ag2

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/ag2@2141

@ag-ui/agno

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/agno@2141

@ag-ui/aws-strands

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/aws-strands@2141

@ag-ui/claude-agent-sdk

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/claude-agent-sdk@2141

@ag-ui/crewai

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/crewai@2141

@ag-ui/langchain

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/langchain@2141

@ag-ui/langgraph

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/langgraph@2141

@ag-ui/llamaindex

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/llamaindex@2141

@ag-ui/mastra

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/mastra@2141

@ag-ui/pydantic-ai

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/pydantic-ai@2141

@ag-ui/vercel-ai-sdk

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/vercel-ai-sdk@2141

@ag-ui/watsonx

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/watsonx@2141

@ag-ui/a2ui-toolkit

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/a2ui-toolkit@2141

create-ag-ui-app

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/create-ag-ui-app@2141

@ag-ui/client

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/client@2141

@ag-ui/core

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/core@2141

@ag-ui/encoder

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/encoder@2141

@ag-ui/proto

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/proto@2141

commit: 9a6589f

@BenTaylorDev

Copy link
Copy Markdown
Contributor

Addendum — verified the tool-registering-plugin case, since the tests use a no-tool init-counting plugin and the headline scenario (AgentSkills) does register a tool.

self._tools is copied from the template's full tool_registry.registry, so if a plugin's tool is present there, the per-thread agent could receive it both ways — copied via tools=self._tools and re-registered by the forwarded plugin. I checked whether that double-registers or errors, with real AgentSkills + strands.Agent:

  • Template built with the plugin registers a skills tool (confirms it can leak into self._tools).
  • Per-thread built with the copied skills tool and the forwarded plugin → constructs fine, registry = ['skills'] (one tool). Strands' registry dedupes by name — no duplicate, no error, and init_agent still runs per thread.
  • Intended usage (template without plugins, plugin only on StrandsAgent) → same, single tool.

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 StrandsAgent(plugins=[...]), not left on the template Agent(plugins=[...]), since a consumed template plugin can't be recovered (_plugin_registry, per the PR description). Worth a line in the usage docs so pattern-A users know to move them.

@BenTaylorDev

Copy link
Copy Markdown
Contributor

Heads up — the aws-strands-python CI job is red, and it's a real blocker (not the pre-existing interventions failure):

ERROR collecting tests/test_template_plugins_preservation.py
ModuleNotFoundError: No module named 'strands.plugins'

Root cause: uv.lock pins strands-agents==1.18.0, and 1.18.0 doesn't have strands.plugins (I installed it and confirmed the import fails). The new test does from strands.plugins import Plugin, so on CI the module errors out at collection and the whole suite fails. The plugins API (strands.plugins, and AgentSkills now under strands.vended_plugins.skills) landed after 1.18.0 — the latest strands has it, which is why it passes locally if you pip install the latest rather than uv sync the lock.

Fix: bump the strands-agents floor to the version that introduced strands.plugins and re-run uv lock so CI installs a plugins-capable strands. Worth double-checking the runtime plugins= kwarg is actually supported there too — on 1.18.0 Agent(plugins=[…]) may not exist at all, so the feature likely needs the newer floor regardless.

For transparency: my approval above was on the strength of the fix's logic + a local run, but I ran against pip-resolved latest strands rather than the locked 1.18.0, so I missed this — the approve should be treated as contingent on the lock bump + green CI. Happy to re-confirm once that's in.

…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.
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]: [AWS Strands] wrapper forwards caller hooks but silently drops the template agent's plugins

2 participants