Skip to content

fix(aws-strands): forward plugins per-thread and warn on agent-bound params - #2612

Merged
ranst91 merged 2 commits into
mainfrom
fix/aws-strands-python-plugins-and-agent-bound-warning
Sep 3, 2026
Merged

fix(aws-strands): forward plugins per-thread and warn on agent-bound params#2612
ranst91 merged 2 commits into
mainfrom
fix/aws-strands-python-plugins-and-agent-bound-warning

Conversation

@ranst91

@ranst91 ranst91 commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Closes the still-open half of #2129.

The bug

StrandsAgent builds a fresh strands.Agent per thread_id from the template Agent, recovering the template's constructor params through _extract_agent_kwargs. On strands-agents 1.35.0, Agent.__init__ accepts plugins and consumes the list immediately: it calls each plugin's init_agent against the agent that received it, registers that plugin's hooks and tools into that agent's registries, and keeps only a _PluginRegistry holding a weakref back to the agent.

_references_agent sees that weakref and returns _AGENT_BOUND, so plugins lands in _template_owned_params. _report_uncarried_params warns only about _unreadable_params, and template-owned params were excluded on purpose, on the reasoning that an agent-bound param is a structural property rather than a surprise.

The result is the worst of the two failure modes. A caller who sets plugins on the template gets no plugins on the agents that actually serve requests, and no warning that anything was lost. The template never serves a request, so a plugin whose whole behaviour lives in init_agent silently does nothing.

Reproduced before the fix:

a = Agent(model=None, system_prompt="BASE.", plugins=[MyPlugin()])
_resolve_template_param(a, "plugins", None)   # -> _AGENT_BOUND
_extract_agent_kwargs(a)                      # -> template_owned == ['plugins']

What changed

1. An agent-bound param that will not carry now says so.

_report_uncarried_params reports both kinds, each with its own message and both under the existing once-per-param bookkeeping, so neither can nag. They are kept separate because they ask for different reading: an unreadable param is a gap in this adapter that a later release may close, while a param the SDK wired to the agent that received it will never be carryable, so the per-thread route is the whole answer rather than a stopgap.

2. Python gets the plugins= kwarg TypeScript already has.

StrandsAgent(plugins=[...]) now forwards to every per-thread StrandsAgentCore, next to the existing hooks forwarding and under the same falsy-omission rule: an empty list is never passed, because plugins=[] is a value a future SDK could read as "register none of the defaults either". "plugins" is added to _AGUI_EXPLICIT_PARAMS so it is not also probed off the template.

Because that exclusion also takes plugins out of the generic probe, a small dedicated read (_template_plugin_names) keeps the template case reportable, mirroring the session_manager footgun detector already in the constructor. It filters out plugins Strands registers itself: every Agent is built with at least one (strands:model), and counting those would warn every caller about a setting nobody made. Plugins a caller opts into are not namespaced that way, including vended ones like AgentSkills, whose name is agent_skills.

Precedence

Three sources can name plugins at once, and all three can be present together:

  1. thread_agent_kwargs wins. It goes on last at the merge site, which is the existing rule for every param and is unchanged here.
  2. The plugins= kwarg is next.
  3. The template never carries.

The warning stays quiet whenever either of the first two supplied the param for that thread, so acting on the message makes it stop. It is still said per param and per thread, so one thread supplying everything does not buy silence for the next thread that supplies nothing.

Version floor

plugins arrived on the Strands Agent constructor in strands-agents 1.28.0. This package declares a 1.15.0 floor and runs a CI lane against it, so the new kwarg can be handed a release with nowhere to put it. Left alone it reached that constructor and Strands raised a bare TypeError from inside per-thread construction, which escapes the run generator rather than becoming a run error: the caller's first request died with an SDK traceback naming neither the argument they passed nor the version that could not take it.

Raising the floor would drop support for 1.15 through 1.27, so instead the capability is probed off the constructor signature and a caller who supplies plugins= on a release without it is refused while the wrapper is being built. That is where the mistake is knowable, and it is said once rather than on every request. The message names the installed version and what to do.

This follows the pattern the adapter already uses for session reconciliation, which probes for the exact repository methods it needs, fails in a defined way when they are absent, and documents the boundary in the README support table.

Not raised for a multi-agent orchestrator. An orchestrator builds no per-thread agent and so ignores both hooks and plugins on every release; reporting a version problem there would describe something the newer releases do not do either.

Credit

PR #2141 by @hiepcs proposed exactly the shape of part 2, including the _AGUI_EXPLICIT_PARAMS entry and the falsy-omission rule, and diagnosed the AgentSkills symptom. That PR was written against a much older tree and no longer applies, so this is written fresh, but the design of the forwarding half is theirs.

Tests

Extended the two suites that already own this behaviour.

tests/test_template_agent_propagation.py:

  • the warning fires when the template carries plugins and nothing else supplies them
  • it stays quiet on an agent whose only plugins are Strands' own, which is what keeps the message rare enough to read
  • the SDK-plugin name filter is also asserted directly
  • it stays quiet when the plugins= kwarg supplies them
  • it is said once, not once per thread
  • it names the kwarg that fixes it
  • the kwarg reaches the per-thread constructor
  • a falsy value omits the kwarg entirely (parametrized over None and [])
  • a release without the plugin system refuses the kwarg at wrap time, and still builds a wrapper when the kwarg is absent or empty
  • against the real strands.Agent, a forwarded plugin's init_agent runs once per thread and against that thread's own agent

tests/test_thread_agent_kwargs.py:

  • the hook can supply plugins
  • hook plugins win over the constructor kwarg
  • the warning stays quiet when the hook supplies them

Only the real-Agent init_agent test needs the SDK's plugin system, so it is the only one skipped at the floor. The rest exercise the adapter's own reading, reporting and forwarding, and use a synthetic plugin registry and plain sentinels, which is how the resolver tests in that file already build their cases. The three that drive the kwarg against a stub core declare the capability explicitly rather than skipping, since a stub takes any kwarg and that is what it already assumes.

test_no_constructor_param_is_dropped_silently now also asserts that template-owned params are named in a warning, which is the general form of the defect above.

Each behaviour was checked against a mutated source to confirm the tests fail without it: removing the forwarding, the template detection, the SDK-plugin filter, or the wrap-time refusal each turns the relevant tests red, and moving the kwarg after the hook merge breaks the precedence test.

Verification

  • strands-agents 1.35.0: green, 1289 passed and 1 skipped, against 1277 passed and 1 skipped on main.
  • The declared floor, strands-agents 1.15.0, which is a CI lane: green, 1280 passed and 4 skipped. Three of those skips are pre-existing; one is the plugin test above.
  • No TypeScript files were touched. The TS package already has this kwarg and its own coverage in plugins-forwarded.test.ts.

Also in this diff

The per-thread build site called _report_uncarried_params twice, once unconditionally and again when thread_agent_kwargs was None. The second call was a no-op given the once-per-param bookkeeping. Removed.

A README section now documents the per-thread rebuild, the hooks and plugins routes, and the version boundary.

…params

The adapter rebuilds a fresh strands.Agent per thread_id by reading the
template's constructor params back off the instance. Strands consumes
`plugins` during init: it runs each plugin's init_agent against the agent that
received it, registers that plugin's hooks and tools into that agent's
registries, and keeps only a _PluginRegistry holding a weakref back to the
agent. _references_agent sees that weakref and classifies `plugins` as
template-owned, and template-owned params were deliberately excluded from the
uncarried-param warning.

A caller who set plugins on the template therefore got neither the plugins nor
a word about losing them. The template never serves a request, so a plugin
whose behaviour lives in init_agent silently did nothing.

Report both kinds of uncarried param, each with its own message and both under
the existing once-per-param bookkeeping. They stay separate because they point
at different fixes: an unreadable param is an adapter gap a later release may
close, while a param the SDK wired to one agent will never be carryable.

Add the `plugins=` kwarg Python was missing, matching StrandsAgentOptions.plugins
on the TypeScript side. It forwards into every per-thread StrandsAgentCore next
to `hooks`, under the same falsy-omission rule, and "plugins" joins
_AGUI_EXPLICIT_PARAMS so it is not also probed off the template. Since that
exclusion hides it from the generic probe, a dedicated read keeps the template
case reportable, filtering out the plugins Strands registers on every Agent
itself so the warning does not reach callers who set none.

Precedence is unchanged: thread_agent_kwargs wins at the merge site, then the
explicit kwarg, and the template never carries. The warning stays quiet for a
thread whose kwargs supplied the param either way.

Also drops a duplicated _report_uncarried_params call at the build site that
was a no-op given the once-per-param bookkeeping.

The forwarding half was proposed in #2141 by @hiepcs against an older tree.

Co-authored-by: HiepBP <hiepcs@users.noreply.github.com>
@ranst91
ranst91 requested a review from a team as a code owner September 3, 2026 08:02
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Python Preview Packages

Version 0.0.0.dev1788433359 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.dev1788433359' --index testpypi

# Integrations (each already depends on the matching ag-ui-protocol preview)
uv add 'ag-ui-langgraph==0.0.0.dev1788433359' --index testpypi
uv add 'ag-ui-crewai==0.0.0.dev1788433359' --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.dev1788433359' --index testpypi
uv add 'ag_ui_adk==0.0.0.dev1788433359' --index testpypi
uv add 'ag_ui_strands==0.0.0.dev1788433359' --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.dev1788433359

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


Commit: cf37618

@pkg-pr-new

pkg-pr-new Bot commented Sep 3, 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@2612

@ag-ui/a2ui-middleware

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

@ag-ui/event-throttle-middleware

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

@ag-ui/mcp-apps-middleware

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

@ag-ui/mcp-middleware

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

@ag-ui/a2a

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

@ag-ui/adk

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

@ag-ui/ag2

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

@ag-ui/agno

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

@ag-ui/aws-strands

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

@ag-ui/claude-agent-sdk

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

@ag-ui/claude-managed-agents

pnpm add https://pkg.pr.new/ag-ui-protocol/ag-ui/@ag-ui/claude-managed-agents@2612

@ag-ui/crewai

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

@ag-ui/langchain

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

@ag-ui/langgraph

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

@ag-ui/llamaindex

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

@ag-ui/mastra

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

@ag-ui/pydantic-ai

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

@ag-ui/vercel-ai-sdk

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

@ag-ui/watsonx

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

@ag-ui/a2ui-toolkit

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

create-ag-ui-app

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

@ag-ui/client

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

@ag-ui/core

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

@ag-ui/encoder

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

@ag-ui/proto

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

commit: 277b34f

The plugins kwarg added in the previous commit reached a constructor that has
no such parameter on strands-agents below 1.28.0, the release that added
plugins to Agent. This package declares a 1.15.0 floor and runs a CI lane
against it. There, Strands raised a bare TypeError from inside per-thread
construction, and because that happens inside the run generator it escaped to
the caller rather than becoming a run error: the first request died with an SDK
traceback pointing at neither the argument nor the version that could not take
it.

Probe the capability off the Agent constructor signature and refuse at wrap
time instead. A static misconfiguration is knowable the moment the wrapper is
built, so it is answered there, with a message naming the installed version and
what to do about it. Not raised for a multi-agent orchestrator, which builds no
per-thread agent and so ignores plugins on every release; reporting a version
problem there would describe something the newer releases do not do either.

Tests previously gated the whole plugin block on the SDK having plugins, which
left twelve behaviours unasserted at the declared floor. Only one of them needs
the real plugin system: the one proving init_agent runs once per thread against
a real Agent. The rest exercise the adapter's own reading, reporting and
forwarding, so they now use a synthetic plugin registry and plain sentinels,
the way the resolver tests in that file already do, and run on every supported
release. The three that drive the kwarg against a stub core declare the
capability explicitly rather than skipping, which is what the stub already
assumes. Floor skips drop from twelve to one.

Adds direct coverage for the SDK-plugin name filter and for the new refusal,
and a README section documenting the per-thread rebuild, the hooks and plugins
routes, and the version boundary.
@ranst91
ranst91 merged commit 1c60523 into main Sep 3, 2026
55 checks passed
@ranst91
ranst91 deleted the fix/aws-strands-python-plugins-and-agent-bound-warning branch September 3, 2026 13:15
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.

2 participants