From 468ade007c41aa4e4fa672caca661f0423328fb4 Mon Sep 17 00:00:00 2001 From: Cayman Roden Date: Sun, 5 Apr 2026 22:32:13 -0700 Subject: [PATCH 1/3] test: add negative case tests for deferral span fix Verify that ModelRetry (ToolRetryError) and unexpected exceptions still record spans as ERROR on v5 instrumentation -- only CallDeferred and ApprovalRequired get UNSET status. Addresses the missing negative cases called out in tests/AGENTS.md: 'Test both positive and negative cases for optional capabilities.' Co-Authored-By: Claude Sonnet 4.6 --- tests/test_logfire.py | 55 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/tests/test_logfire.py b/tests/test_logfire.py index 504127e6c9..c5fd62732b 100644 --- a/tests/test_logfire.py +++ b/tests/test_logfire.py @@ -3622,6 +3622,61 @@ def my_tool(x: int) -> int: ) +@pytest.mark.skipif(not logfire_installed, reason='logfire not installed') +def test_deferral_model_retry_still_errors_v5(capfire: CaptureLogfire) -> None: + """Test that ModelRetry on v5 still records the span as an error. + + The deferral fix (CallDeferred/ApprovalRequired → UNSET on v5) must not affect + ModelRetry, which wraps as ToolRetryError and should always be an error span. + """ + agent = Agent( + TestModel(), + instrument=InstrumentationSettings(version=5), + ) + + @agent.tool_plain + def my_tool(x: int) -> str: + raise ModelRetry('please try again with different input') + + with pytest.raises(UnexpectedModelBehavior): + agent.run_sync('Hello') + + tool_span = _get_tool_span(capfire) + + # ToolRetryError should still be recorded as an error on v5 — only deferrals get UNSET + assert tool_span['attributes'].get('logfire.level_num') == 17 + # No deferral attributes should be set — this is a retry, not a deferral + assert 'pydantic_ai.tool.deferral.name' not in tool_span['attributes'] + assert 'pydantic_ai.tool.deferral.metadata' not in tool_span['attributes'] + + +@pytest.mark.skipif(not logfire_installed, reason='logfire not installed') +def test_deferral_unexpected_exception_still_errors_v5(capfire: CaptureLogfire) -> None: + """Test that unexpected exceptions on v5 still record the span as an error. + + The deferral fix must not affect general exception handling — only + CallDeferred and ApprovalRequired get UNSET status on v5. + """ + agent = Agent( + TestModel(), + instrument=InstrumentationSettings(version=5), + ) + + @agent.tool_plain + def my_tool(x: int) -> str: + raise ValueError('something went wrong') + + with pytest.raises(Exception): # noqa: B017 + agent.run_sync('Hello') + + tool_span = _get_tool_span(capfire) + + # BaseException path should still record error regardless of instrumentation version + assert tool_span['attributes'].get('logfire.level_num') == 17 + # No deferral attributes should be set + assert 'pydantic_ai.tool.deferral.name' not in tool_span['attributes'] + + @pytest.mark.skipif(not logfire_installed, reason='logfire not installed') @pytest.mark.anyio async def test_agent_description(capfire: CaptureLogfire) -> None: From d1b142ed42debbfe7d61af8972c8b5487003f6fd Mon Sep 17 00:00:00 2001 From: Cayman Roden Date: Mon, 6 Apr 2026 06:16:19 -0700 Subject: [PATCH 2/3] fix: narrow pytest.raises(Exception) to pytest.raises(ValueError) The second test used the too-broad pytest.raises(Exception) which was flagged by the B017 linter rule. Since the tool explicitly raises ValueError, narrow to pytest.raises(ValueError, match=...) to validate the specific exception type and message. Co-Authored-By: Claude Sonnet 4.6 --- tests/test_logfire.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_logfire.py b/tests/test_logfire.py index c5fd62732b..c40c3a115c 100644 --- a/tests/test_logfire.py +++ b/tests/test_logfire.py @@ -3666,12 +3666,12 @@ def test_deferral_unexpected_exception_still_errors_v5(capfire: CaptureLogfire) def my_tool(x: int) -> str: raise ValueError('something went wrong') - with pytest.raises(Exception): # noqa: B017 + with pytest.raises(ValueError, match='something went wrong'): agent.run_sync('Hello') tool_span = _get_tool_span(capfire) - # BaseException path should still record error regardless of instrumentation version + # ValueError path should still record error regardless of instrumentation version assert tool_span['attributes'].get('logfire.level_num') == 17 # No deferral attributes should be set assert 'pydantic_ai.tool.deferral.name' not in tool_span['attributes'] From 5b28ba7975bfe5a6f2ab164b520bec45a49da18e Mon Sep 17 00:00:00 2001 From: Cayman Roden Date: Sat, 30 May 2026 16:07:01 -0700 Subject: [PATCH 3/3] chore: ignore local agent artifacts --- .gitignore | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.gitignore b/.gitignore index 33308f03bd..26a1ee94af 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,8 @@ node_modules/ /.cursor/ /.devcontainer/ .claude/plans/ + +# Local agent artifacts +.maintenance/ +AGENTS.md.bak-* +claude-progress.txt