Skip to content

Commit 1651332

Browse files
author
Raju Ansari
committed
fix: address review feedback
- Preserve interrupt behavior: post-payment failure stores state and triggers interrupt so downstream consumers are notified - Remove dead code: _extract_payment_error_message (unreachable) - Remove TestExtractPaymentErrorMessage (tested removed method) - Clarify comment on retry counter increment - Add test: signing flag takes precedence over retry limit - Remove emojis from new integration test logger output
1 parent 34f9130 commit 1651332

3 files changed

Lines changed: 58 additions & 55 deletions

File tree

src/bedrock_agentcore/payments/integrations/strands/plugin.py

Lines changed: 6 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -198,20 +198,23 @@ def after_tool_call(self, event: AfterToolCallEvent) -> None:
198198

199199
# If we previously signed successfully and still got a 402, the server
200200
# rejected the payment for a non-retryable reason (e.g., insufficient balance).
201-
# Stop processing — the existing 402 result is already structured for the LLM.
201+
# Do not retry — store failure state so the agent is notified via interrupt.
202202
if self._has_successful_signing(event):
203+
error_msg = body.get("error", "unknown error") if body and isinstance(body, dict) else "unknown error"
203204
logger.warning(
204-
"Received 402 after successful signing for tool %s — post-payment failure, not retrying",
205+
"Received 402 after successful signing for tool %s — post-payment failure: %s",
205206
event.tool_use.get("name", "unknown"),
207+
error_msg,
206208
)
209+
self._store_payment_failure_state(event, PaymentError(f"Payment rejected after signing: {error_msg}"))
207210
return
208211

209212
# Check if signing retry limit has been reached
210213
if self._check_payment_retry_limit(event):
211214
logger.warning("Payment signing retry limit reached. Processing skipped.")
212215
return
213216

214-
# Increment retry count for signing attempts
217+
# Increment before attempt so limit is enforced even on exception
215218
self._increment_payment_retry_count(event)
216219

217220
# Validate tool input before processing payment
@@ -321,22 +324,6 @@ def _mark_successful_signing(self, event: AfterToolCallEvent) -> None:
321324
signed_key = f"payment_signed_{tool_use_id}"
322325
event.invocation_state[signed_key] = True
323326

324-
@staticmethod
325-
def _extract_payment_error_message(body: Optional[Dict[str, Any]]) -> str:
326-
"""Extract a human-readable error message from a 402 response body.
327-
328-
Args:
329-
body: The extracted response body (may be None)
330-
331-
Returns:
332-
Error message string, or "unknown error" if not extractable
333-
"""
334-
if body and isinstance(body, dict):
335-
error = body.get("error")
336-
if isinstance(error, str) and error:
337-
return error
338-
return "unknown error"
339-
340327
def _store_payment_failure_state(self, event: AfterToolCallEvent, exception: Exception) -> None:
341328
"""Store payment failure information in invocation state for agent to handle.
342329

tests/bedrock_agentcore/payments/integrations/strands/test_plugin.py

Lines changed: 46 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1376,34 +1376,6 @@ def test_signing_state_is_per_tool_use(self):
13761376
assert plugin._has_successful_signing(event2) is False
13771377

13781378

1379-
class TestExtractPaymentErrorMessage:
1380-
"""Tests for _extract_payment_error_message static method."""
1381-
1382-
def test_extracts_error_from_body(self):
1383-
"""Test extracting error message from body dict."""
1384-
body = {"error": "invalid_exact_evm_insufficient_balance"}
1385-
assert AgentCorePaymentsPlugin._extract_payment_error_message(body) == "invalid_exact_evm_insufficient_balance"
1386-
1387-
def test_returns_unknown_for_none_body(self):
1388-
"""Test returns 'unknown error' when body is None."""
1389-
assert AgentCorePaymentsPlugin._extract_payment_error_message(None) == "unknown error"
1390-
1391-
def test_returns_unknown_for_empty_error(self):
1392-
"""Test returns 'unknown error' when error is empty string."""
1393-
body = {"error": ""}
1394-
assert AgentCorePaymentsPlugin._extract_payment_error_message(body) == "unknown error"
1395-
1396-
def test_returns_unknown_for_missing_error_key(self):
1397-
"""Test returns 'unknown error' when error key is missing."""
1398-
body = {"statusCode": 402}
1399-
assert AgentCorePaymentsPlugin._extract_payment_error_message(body) == "unknown error"
1400-
1401-
def test_returns_unknown_for_non_string_error(self):
1402-
"""Test returns 'unknown error' when error is not a string."""
1403-
body = {"error": 42}
1404-
assert AgentCorePaymentsPlugin._extract_payment_error_message(body) == "unknown error"
1405-
1406-
14071379
class TestAfterToolCallPostPaymentFailure:
14081380
"""Tests for the post-payment failure path in after_tool_call."""
14091381

@@ -1443,12 +1415,13 @@ def test_post_payment_failure_does_not_retry_after_successful_signing(self, mock
14431415

14441416
# Should NOT retry
14451417
assert event.retry is False
1446-
# Should NOT store failure state (no interrupt cycle)
1447-
assert "payment_failure_tool-123" not in event.invocation_state
1418+
# Should store failure state so agent is notified via interrupt
1419+
assert "payment_failure_tool-123" in event.invocation_state
1420+
failure = event.invocation_state["payment_failure_tool-123"]
1421+
assert "Payment rejected after signing" in failure["exceptionMessage"]
1422+
assert "invalid_exact_evm_insufficient_balance" in failure["exceptionMessage"]
14481423
# Should NOT have called generate_payment_header
14491424
mock_pm_instance.generate_payment_header.assert_not_called()
1450-
# Result should remain unchanged (raw 402 for LLM to reason about)
1451-
assert event.result == original_result
14521425

14531426
@patch("bedrock_agentcore.payments.integrations.strands.plugin.PaymentManager")
14541427
def test_first_402_without_prior_signing_proceeds_normally(self, mock_payment_manager_class):
@@ -1558,6 +1531,47 @@ def test_signing_retry_limit_stops_processing(self, mock_payment_manager_class):
15581531
assert event.retry is False
15591532

15601533

1534+
class TestSigningFlagTakesPrecedenceOverRetryLimit:
1535+
"""Tests that _has_successful_signing check takes precedence over retry limit."""
1536+
1537+
@patch("bedrock_agentcore.payments.integrations.strands.plugin.PaymentManager")
1538+
def test_successful_signing_stops_even_when_retry_limit_not_reached(self, mock_payment_manager_class):
1539+
"""Test that post-payment failure stops processing regardless of retry counter state.
1540+
1541+
If signing succeeded (payment_signed_ flag is True), a subsequent 402 should
1542+
stop immediately — even if payment_retry_count is below MAX_PAYMENT_RETRIES.
1543+
The signing flag takes precedence over the retry counter.
1544+
"""
1545+
config = AgentCorePaymentsPluginConfig(
1546+
payment_manager_arn="arn:aws:bedrock-agentcore:us-west-2:123456789012:payment-manager/test",
1547+
user_id="test-user",
1548+
payment_instrument_id="payment-instrument-123",
1549+
payment_session_id="payment-session-456",
1550+
)
1551+
1552+
mock_pm_instance = MagicMock()
1553+
plugin = AgentCorePaymentsPlugin(config=config)
1554+
plugin.payment_manager = mock_pm_instance
1555+
1556+
# Signing succeeded but retry counter is only at 1 (below MAX_PAYMENT_RETRIES=3)
1557+
event, _ = _create_event_with_agent(
1558+
{
1559+
"result": [{"text": f"PAYMENT_REQUIRED: {json.dumps({'statusCode': 402, 'headers': {}, 'body': {}})}"}],
1560+
"tool_use": {"name": "http_request", "toolUseId": "tool-123", "input": {"headers": {}}},
1561+
"invocation_state": {"payment_signed_tool-123": True, "payment_retry_count_tool-123": 1},
1562+
"retry": False,
1563+
}
1564+
)
1565+
1566+
plugin.after_tool_call(event)
1567+
1568+
# Should NOT retry — signing flag takes precedence
1569+
assert event.retry is False
1570+
mock_pm_instance.generate_payment_header.assert_not_called()
1571+
# Should store failure state for interrupt notification
1572+
assert "payment_failure_tool-123" in event.invocation_state
1573+
1574+
15611575
class TestAfterToolCallAutoPaymentDisabled:
15621576
"""Tests for auto_payment=False in after_tool_call."""
15631577

tests_integ/payments/integrations/strands/test_plugin_integration.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -797,7 +797,7 @@ def test_full_flow_402_sign_retry_402_stops(self):
797797
mock_pm.generate_payment_header.assert_called_once()
798798
assert "X-PAYMENT" in tool_input["headers"]
799799
assert invocation_state.get("payment_signed_tool-abc") is True
800-
logger.info("Step 1: First 402 signed and retry=True")
800+
logger.info("Step 1: First 402 - signed and retry=True")
801801

802802
# Step 2: Second 402 — server rejected (insufficient balance)
803803
event2 = self._make_402_event(
@@ -813,9 +813,11 @@ def test_full_flow_402_sign_retry_402_stops(self):
813813
assert event2.retry is False
814814
# generate_payment_header should still only have been called once (from step 1)
815815
assert mock_pm.generate_payment_header.call_count == 1
816-
# Should NOT store failure state (no interrupt cycle)
817-
assert "payment_failure_tool-abc" not in invocation_state
818-
logger.info("✓ Step 2: Second 402 after signing → stopped, no retry")
816+
# Should store failure state so agent is notified via interrupt
817+
assert "payment_failure_tool-abc" in invocation_state
818+
failure = invocation_state["payment_failure_tool-abc"]
819+
assert "Payment rejected after signing" in failure["exceptionMessage"]
820+
logger.info("Step 2: Second 402 after signing - stopped, failure stored for interrupt")
819821

820822
def test_signing_failure_uses_retry_counter(self):
821823
"""Test that signing failures increment retry counter and stop at limit.

0 commit comments

Comments
 (0)