Skip to content

fix(aws-strands): unify the terminal error codes and message text across both bridges - #2585

Open
ranst91 wants to merge 1 commit into
mainfrom
ran/pni-353-strands-error-code-parity
Open

fix(aws-strands): unify the terminal error codes and message text across both bridges#2585
ranst91 wants to merge 1 commit into
mainfrom
ran/pni-353-strands-error-code-parity

Conversation

@ranst91

@ranst91 ranst91 commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

What this is about

The two AWS Strands bridges put terminal RUN_ERROR frames on the wire carrying a code and a message. Clients and mock harnesses match both literally, and the two sides had drifted, so the drift broke consumers rather than merely looking untidy.

This resolves the two behaviour differences, makes shared codes carry identical text, and adds the thing that stops it recurring.

The two behaviour differences, both resolved toward failing loud

Every attachment fails to convert and no text remains. TypeScript aborted the run; Python logged a warning and carried on with an empty prompt. Python now emits MEDIA_RESOLUTION_FAILED and ends the run, with the message TypeScript already used.

One honest narrowing: the degrade path below that gate, where text does survive, behaves as it always did, and under the default configuration the replay path supplies history so the recovered text reaches the model that way rather than through the fallback assignment. What this change adds is the gate, not the degrade.

Telling our bug from someone else's. TypeScript distinguished a defect in this adapter from a failure elsewhere; Python labelled everything an SDK failure. Python now makes the same split, the rule lives in one function per language instead of at four catch sites, and a fault arriving from inside the Strands call is marked foreign before it reaches the classifier. An integrator's unserializable tool result and a client's malformed state are no longer reported as this adapter's bug. Wrapping a fault is total on both sides, so wrapping can never itself raise the type it exists to rule out.

Runtime strings changed deliberately

Every one of these is a string another system could match, so each was checked across the repository first. No test, fixture, harness, doc or dojo page matched any of the old texts. The only occurrences were the source lines themselves.

  • PARTIAL_RESUME and INVALID_PAYLOAD rendered a Python list repr on one side and a JSON array on the other. Both now render one sorted comma-joined list.
  • UNKNOWN_INTERRUPT_ID used a singular on one side and a plural on the other. One spelling now.
  • THREAD_BUSY shared an opening and a tail through a scope helper on each side. The parked-orchestrator refusal, which only Python can produce, became its own sentence: no run is in flight then, so telling the caller to wait for RUN_FINISHED was false, and answering the interrupt is the way out.
  • Both bridges now pick the article for a vowel-initial JSON Schema type, so an integer violation no longer reads "must be a integer".
  • A null forced-stop reason emitted the literal string "None" instead of the shared fallback.

Two wire changes beyond the codes, both for parity

  • A stream failure in TypeScript no longer splits by exception type. Every failure from that call now reports STRANDS_FORCE_STOP after the message and tool-call closeout, which is what Python already did; one branch previously reported STRANDS_ERROR and skipped the closeout, leaving an unclosed message envelope ahead of the error.
  • A non-mapping client state now yields no opening state snapshot on either side. TypeScript previously put an array on the wire as an index-keyed object that Python never produced.

The guard

error-codes.json sits beside ARCHITECTURE.md and lists every code each bridge can emit with the message template it owes. It is deliberately not a hand-maintained checklist, which the parent ticket warns against: each side derives the list from its own source and asserts the derivation against the file, so the file follows the source and cannot drift silently. A code only one bridge emits is legitimate and carries a note saying why, and a test enforces that the note exists.

Two things back it up. A corpus of small synthetic sources, one per shape in which an error can be emitted, proves the derivation actually sees each shape. And a mutation harness perturbs a copy of the contract and requires each assertion to redden, so a guard that quietly stops checking fails instead of passing.

It earned this three times while the branch was open, catching drift from other people's merges: two new codes, and one shared sentence reworded upstream. Each time the suite went red naming exactly what moved.

Known limits, stated rather than papered over

  • The derivation catches drift in the shapes the adapters actually use, proven above. It does not yet guarantee that a code introduced in a shape no adapter currently uses is either recorded or counted as unresolved. The specific shapes are listed in the review notes. Closing that is a rewrite of the resolution code, which regressed once during this work and was caught only because a corpus case existed, so it is follow-up rather than something to land unreviewed here.
  • Two codes arrived from other work while this was open and are the same failure under different names: THREAD_AGENT_CONFIG_ERROR and THREAD_AGENT_KWARGS_ERROR. Both are listed as they are spelled, each noting the other. Unifying them is for whoever owns the per-thread agent provider; renaming another chip's just-landed code here would collide with them.
  • The corpus header's framing about one-sided cases being shapes a language cannot spell reads loosely against two notes whose real reason turned out to be a derivation rule rather than the language.

Not regressed

The interrupt protocol's four typed error paths are untouched and on the list with their reasons, along with the codes behind durable frontend-result recovery. Nothing was renamed or deleted to buy symmetry.

Verification

Python 1049 passing, TypeScript 1123 across 66 files, typecheck clean. The red-green demonstrations for adding, removing and rewording a code were run rather than asserted, and the mutation harness re-proves that criterion on every run.

…oss both bridges

The two AWS Strands bridges had drifted on the RUN_ERROR frames they put on the
wire. Clients and mock harnesses match both the code and the message literally,
so the drift broke consumers rather than merely looking untidy.

Two behaviour differences are resolved, both toward failing loud.

When every attached image or document fails to convert and no text remains,
TypeScript aborted the run and Python logged a warning and carried on with an
empty prompt. Python now emits MEDIA_RESOLUTION_FAILED and ends the run, with
the same message TypeScript already used. The degrade below that gate, where
text does survive, is unchanged; note that under the default configuration the
replay path supplies history and the recovered text reaches the model that way
rather than through the fallback assignment.

TypeScript distinguished a defect in this adapter from a failure elsewhere and
Python labelled everything an SDK failure. Python now makes the same split. The
rule then lives in one function per language rather than at four catch sites,
and a fault arriving from inside the Strands call is marked foreign before it
reaches the classifier, so an integrator's unserializable tool result or a
client's malformed state is no longer reported as this adapter's bug. Wrapping a
fault is total on both sides: it cannot itself raise the type it exists to rule
out.

Shared codes now carry identical text. PARTIAL_RESUME and INVALID_PAYLOAD render
one sorted comma-joined list instead of a Python list repr on one side and a
JSON array on the other. UNKNOWN_INTERRUPT_ID uses one spelling. THREAD_BUSY
shares an opening and a tail through a scope helper on each side, and the
parked-orchestrator refusal, which only Python can produce, is its own sentence
because no run is in flight then and answering the interrupt rather than waiting
for RUN_FINISHED is the way out. Both bridges pick the article for a
vowel-initial JSON Schema type, so an integer violation reads correctly.

Two wire changes beyond the codes, both for parity. A stream failure in
TypeScript no longer splits by exception type: every failure from that call now
reports STRANDS_FORCE_STOP after the message and tool-call closeout, which is
what Python already did, where one branch previously reported STRANDS_ERROR and
skipped the closeout. And a non-mapping client state now yields no opening state
snapshot on either side, where TypeScript put an array on the wire as an
index-keyed object that Python never produced.

The guard against this recurring is error-codes.json beside ARCHITECTURE.md. It
is not a hand-maintained checklist. Each side derives from its own source every
code it can emit and the normalised message template for each, and asserts that
against the file, so the file follows the source and cannot drift silently. A
code only one bridge emits is legitimate and carries a note saying why, which a
test enforces. A corpus of synthetic sources, one per emission shape, proves the
derivation sees each shape, and a mutation harness perturbs a copy of the
contract and requires each assertion to redden, so a guard that stops checking
fails rather than passing quietly.

It has already earned this three times while the branch was open, catching two
codes and one reworded sentence that arrived on main from other work.
@ranst91
ranst91 requested a review from a team as a code owner August 31, 2026 09:45
@github-actions

Copy link
Copy Markdown
Contributor

Python Preview Packages

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

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

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


Commit: 1b1418d

@pkg-pr-new

pkg-pr-new Bot commented Aug 31, 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@2585

@ag-ui/a2ui-middleware

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

@ag-ui/event-throttle-middleware

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

@ag-ui/mcp-apps-middleware

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

@ag-ui/mcp-middleware

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

@ag-ui/a2a

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

@ag-ui/adk

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

@ag-ui/ag2

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

@ag-ui/agno

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

@ag-ui/aws-strands

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

@ag-ui/claude-agent-sdk

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

@ag-ui/claude-managed-agents

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

@ag-ui/crewai

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

@ag-ui/langchain

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

@ag-ui/langgraph

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

@ag-ui/llamaindex

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

@ag-ui/mastra

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

@ag-ui/pydantic-ai

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

@ag-ui/vercel-ai-sdk

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

@ag-ui/watsonx

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

@ag-ui/a2ui-toolkit

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

create-ag-ui-app

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

@ag-ui/client

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

@ag-ui/core

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

@ag-ui/encoder

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

@ag-ui/proto

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

commit: 23716e7

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.

1 participant