feat(sdk): Strands Agents adapter — Tier-1 injectable-model family#435
Draft
nir-singher-band wants to merge 2 commits into
Draft
feat(sdk): Strands Agents adapter — Tier-1 injectable-model family#435nir-singher-band wants to merge 2 commits into
nir-singher-band wants to merge 2 commits into
Conversation
Add StrandsAdapter (SimpleAdapter[StrandsMessages]) + StrandsHistoryConverter for AWS Strands Agents (strands-agents>=1.40,<2), mirroring the pydantic-ai adapter: platform tools as native @tool functions (per-turn tools handle via invocation_state), CustomToolDef bridged through a native AgentTool, L6 tool_call/tool_result via Before/AfterToolCall hooks, per-turn TurnUsage from accumulated metrics, terminal detection via is_terminal_success. A Strands Agent is stateful (owns messages, throws on concurrent invocation), so the adapter builds prompt+tools once in on_started and constructs a lightweight per-turn Agent over band-owned per-room history — the same fresh-runner-per-message shape as the google_adk adapter. Registered across the strands/dev extras, lazy adapter import, the baseline matrix (core lane via Dep.OPENAI), and the conformance config registries; Tier-1 injection spike proves scripted-model -> real loop -> typed dispatch with ordered/paired L6 events and a negative control. ✌️
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a Strands Agents adapter (
StrandsAdapter) + history converter for AWS's Strands Agents SDK (pinnedstrands-agents>=1.40,<2), modeled on the pydantic-ai adapter — same integration family (INJECTABLE_MODEL_OBJECT: the framework owns the loop, the model object is injectable at the publicAgent(model=...)seam).src/band/adapters/strands.py): platform tools as native@toolfunctions (descriptions fromget_tool_description), contact tools gated onCapability.CONTACTS, memory tools onCapability.MEMORY;CustomToolDeftuples bridged through a nativeAgentToolsubclass validating viaexecute_custom_tool; L6tool_call/tool_resultevents viaBefore/AfterToolCallEventhooks (ordered, paired, correlated bytoolUseId); per-turnTurnUsagefrommetrics.accumulated_usage→emit_usage; terminal detection viais_terminal_success+band_tool_errored, with_report_erroron a turn that never called a terminal Band tool.src/band/converters/strands.py): platform history → Converse-shapedMessages (toolUse/toolResultblocks, batched; own text preserved as assistant turns for rehydration).strandsextra (+ indev), lazy import inband.adapters,Adapter.STRANDS+@adapterbuilder (OpenAI provider,corelane derived fromDep.OPENAI— noci_lanes.pyedit needed, the lane partition is registry-derived), conformance config registries (adapter/converter/output-adapter).examples/strands/01_basic_agent.py,02_custom_tools.py(PEP-723,band-sdk[strands]).Step-0 findings (verified against installed strands-agents 1.47.0)
agent.invoke_async(prompt, invocation_state={...})reaches tool bodies via@tool(context=True)→tool_context.invocation_state, and every hook event viaevent.invocation_state. Verified empirically; theself._active_toolsfallback was not needed.Modelstream shape — subclassstrands.models.Model(must also implement abstractstructured_output). One tool call:messageStart{role:assistant}→contentBlockStart{start:{toolUse:{toolUseId,name}}}→contentBlockDelta{delta:{toolUse:{input:<json-string>}}}→contentBlockStop→messageStop{stopReason:"tool_use"}; text turn:delta:{text}+stopReason:"end_turn"; per-call usage on a trailingmetadata{usage}event. Strands ships no public test model, so the spike implements the minimalModelsubclass (still the public provider ABC).strands.models.openai.OpenAIModel(client_args={"api_key":...}, model_id=...). There is no"openai:model"string shorthand: a bare string toAgent(model=...)is treated as a Bedrock model id. Builder + examples use the explicitOpenAIModel(the design's ⚠ alternative).BeforeToolCallEvent.tool_use{name,toolUseId,input},AfterToolCallEvent.result: ToolResult{status,content,toolUseId}(+.exception), async callbacks supported. Usage lives onAgentResult.metrics.accumulated_usage(inputTokens/outputTokens/cacheReadInputTokens/cacheWriteInputTokens) — not onAfterModelCallEvent— and accumulates across turns on a shared Agent.agent.messagesis a public read/write list of ConverseMessagedicts; pre-seeding viaAgent(messages=...)and post-run read-back both verified — Band owns per-room history (no Strands sessions).Design adjustment (D1), flagged rather than silently diverged: a Strands
Agentis stateful (ownsmessages, per-Agent metrics) and raisesConcurrencyExceptionon concurrent invocation (default mode), while the Band runtime runs one asyncio task per room — a single Agent shared across rooms would break. The adapter builds the heavy pieces (prompt, tool set) once inon_startedand constructs a lightweight per-turnAgentover the room's band-owned history — the same fresh-runner-per-message shapegoogle_adk.pyalready uses. Bonus: fresh per-turn metrics makeaccumulated_usageexactly the turn's usage (no snapshot deltas). Everything else follows the design decisions verbatim.Injection registry note: the Tier-1
InjectionBindingregistry (tests/framework_conformance/injection_registry.py) lives on the unmerged taxonomy branch (int-826-827-tier1-injection-taxonomy; PR #368 was closed) and does not exist ondev. The spike is standalone (matching how the taxonomy branch keeps spikes standalone), and the exactInjectionBinding(...)record for strands —family=INJECTABLE_MODEL_OBJECT,tier1_status=HONEST_TODAY,observation_paths={TYPED_METHODS},model_seam_kind=PUBLIC_TEST_MODEL,drift_risk=HIGH,version_pin="strands-agents>=1.40,<2",seam="band.adapters.strands:StrandsAdapter._build_agent"— is recorded in the spike's module docstring, ready to paste when the registry lands.Verification gate
uv sync --extra strandsresolves with no conflicts (coexists with the fulldevextra incl. parlant/pydantic-ai);uv run python -c "import band; from band.adapters import StrandsAdapter"→ OK; in a strands-free env:OK: plain band-sdk does not import/require strands.uv run pytest tests/framework_conformance -q→476 passed, 43 skipped(incl. the 4 strands injection-spike tests; skips are pre-existing env-gated cases).uv run pytest tests/adapters/test_strands_adapter.py -q→17 passed.assert_registry_covers_discovered()+assert_every_adapter_has_a_ci_home()pass; strands lands in thecorelane:('core', ['agno', 'anthropic', 'claude_sdk', 'langgraph', 'pydantic_ai', 'strands']). (pytest tests/e2e/baseline -k "registry or discovered"skips locally by design — that tree is live-gated; same guards run intests/framework_conformance/test_lane_scheduling.py, green.)ruff check .→All checks passed!;ruff formatapplied;pyrefly check src/band/adapters/strands.py src/band/converters/strands.py→0 errors(the project-wide pyrefly run is inert inside a.claude/worktreescheckout — pre-existing path quirk, CI unaffected).OpenAIModel(model_id=...).uv run pytest tests/ --ignore=tests/integration --ignore=tests/e2e -q→3545 passed, 60 skipped.Live validation (manual, app.band.ai): registered a fresh
strands-exampleagent, ranexamples/strands/01_basic_agent.pyagainst the local branch, and drove it as a user in roomac584ec0-0f6b-4481-9361-fc8d23402b21:band_send_message(haiku received, no error events, clean turn logs).StrandsHistoryConverter(own prior reply as an assistant turn) and correctly answered a follow-up that referenced its pre-restart reply ("make a joke about that line").Not run locally: the automated baseline matrix (
E2E_TESTS_ENABLED=true … -k strands). The cell is registered fail-loud in thecorelane, so the e2e workflow exercises L0–L6.Relates INT-971
🤖 Generated with Claude Code