From d94c185ca5e313dccc321f76db58923988586b57 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Mon, 19 Oct 2026 13:45:39 -0400 Subject: [PATCH] fix(pydantic-ai): don't log deferred tool calls as errors Tools raising CallDeferred or ApprovalRequired hand the call back to the caller as DeferredToolRequests. That is control flow, not a failure, but the tool span recorded it as an error. Catch both inside the span, log the deferral kind (and CallDeferred/ApprovalRequired metadata when present) on the span, and re-raise after the span closes. Fixes SDK-581 Co-Authored-By: Claude Opus 5.5 --- ...ol_is_not_logged_as_error_and_resumes.yaml | 212 +++++++++++++++ ..._deferred_tool_is_not_logged_as_error.yaml | 108 ++++++++ ...est_tool_exception_is_logged_as_error.yaml | 108 ++++++++ ...ol_is_not_logged_as_error_and_resumes.yaml | 250 ++++++++++++++++++ ..._deferred_tool_is_not_logged_as_error.yaml | 125 +++++++++ ...est_tool_exception_is_logged_as_error.yaml | 125 +++++++++ .../test_pydantic_ai_integration.py | 108 ++++++++ .../integrations/pydantic_ai/tracing.py | 33 ++- 8 files changed, 1062 insertions(+), 7 deletions(-) create mode 100644 py/src/braintrust/integrations/pydantic_ai/cassettes/1.10.0/test_approval_required_tool_is_not_logged_as_error_and_resumes.yaml create mode 100644 py/src/braintrust/integrations/pydantic_ai/cassettes/1.10.0/test_call_deferred_tool_is_not_logged_as_error.yaml create mode 100644 py/src/braintrust/integrations/pydantic_ai/cassettes/1.10.0/test_tool_exception_is_logged_as_error.yaml create mode 100644 py/src/braintrust/integrations/pydantic_ai/cassettes/latest/test_approval_required_tool_is_not_logged_as_error_and_resumes.yaml create mode 100644 py/src/braintrust/integrations/pydantic_ai/cassettes/latest/test_call_deferred_tool_is_not_logged_as_error.yaml create mode 100644 py/src/braintrust/integrations/pydantic_ai/cassettes/latest/test_tool_exception_is_logged_as_error.yaml diff --git a/py/src/braintrust/integrations/pydantic_ai/cassettes/1.10.0/test_approval_required_tool_is_not_logged_as_error_and_resumes.yaml b/py/src/braintrust/integrations/pydantic_ai/cassettes/1.10.0/test_approval_required_tool_is_not_logged_as_error_and_resumes.yaml new file mode 100644 index 00000000..1e288ebe --- /dev/null +++ b/py/src/braintrust/integrations/pydantic_ai/cassettes/1.10.0/test_approval_required_tool_is_not_logged_as_error_and_resumes.yaml @@ -0,0 +1,212 @@ +interactions: +- request: + body: '{"model":"gpt-4o-mini","stream":false,"messages":[{"role":"user","content":"Delete + the file at /tmp/report.txt."}],"max_completion_tokens":200,"tool_choice":"auto","tools":[{"type":"function","function":{"name":"delete_file","description":"Delete + the file at the given path.","parameters":{"additionalProperties":false,"properties":{"path":{"type":"string"}},"required":["path"],"type":"object"},"strict":true}}]}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '413' + content-type: + - application/json + host: + - api.openai.com + user-agent: + - pydantic-ai/1.10.0 + x-stainless-arch: + - arm64 + x-stainless-async: + - async:asyncio + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 3.31.0 + x-stainless-read-timeout: + - '600' + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.14.7 + method: POST + uri: https://api.openai.com/v1/chat/completions + response: + body: + string: "{\n \"id\": \"chatcmpl-EalMGcqrFtm0lJOe8Fe0y3RrPE62z\",\n \"object\": + \"chat.completion\",\n \"created\": 1792430560,\n \"model\": \"gpt-4o-mini-2024-07-18\",\n + \ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\": + \"assistant\",\n \"content\": null,\n \"tool_calls\": [\n {\n + \ \"id\": \"call_VWcNyyC8HPULYv2RFhBlixdl\",\n \"type\": + \"function\",\n \"function\": {\n \"name\": \"delete_file\",\n + \ \"arguments\": \"{\\\"path\\\":\\\"/tmp/report.txt\\\"}\"\n + \ }\n }\n ],\n \"refusal\": null,\n \"annotations\": + []\n },\n \"logprobs\": null,\n \"finish_reason\": \"tool_calls\"\n + \ }\n ],\n \"usage\": {\n \"prompt_tokens\": 54,\n \"completion_tokens\": + 17,\n \"total_tokens\": 71,\n \"prompt_tokens_details\": {\n \"cached_tokens\": + 0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\": + {\n \"reasoning_tokens\": 0,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\": + 0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"service_tier\": + \"default\",\n \"system_fingerprint\": \"fp_f263dd6940\"\n}\n" + headers: + access-control-expose-headers: + - X-Request-ID + - CF-Ray + - CF-Ray + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + cf-ray: + - a4d1775a0a008dfa-SJC + connection: + - keep-alive + content-length: + - '1094' + content-type: + - application/json + date: + - Mon, 19 Oct 2026 17:22:41 GMT + openai-processing-ms: + - '442' + openai-version: + - '2020-10-01' + server: + - cloudflare + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + transfer-encoding: + - chunked + x-content-type-options: + - nosniff + x-openai-proxy-wasm: + - v0.1 + x-ratelimit-limit-requests: + - '30000' + x-ratelimit-limit-tokens: + - '150000000' + x-ratelimit-remaining-requests: + - '29999' + x-ratelimit-remaining-tokens: + - '149999990' + x-ratelimit-reset-requests: + - 2ms + x-ratelimit-reset-tokens: + - 0s + x-request-id: + - req_e3872d16804e463f8fe4c16816285bcb + status: + code: 200 + message: OK +- request: + body: '{"model":"gpt-4o-mini","stream":false,"messages":[{"role":"user","content":"Delete + the file at /tmp/report.txt."},{"role":"assistant","content":null,"tool_calls":[{"id":"call_VWcNyyC8HPULYv2RFhBlixdl","type":"function","function":{"name":"delete_file","arguments":"{\"path\":\"/tmp/report.txt\"}"}}]},{"role":"tool","tool_call_id":"call_VWcNyyC8HPULYv2RFhBlixdl","content":"Deleted + /tmp/report.txt"}],"max_completion_tokens":200,"tool_choice":"auto","tools":[{"type":"function","function":{"name":"delete_file","description":"Delete + the file at the given path.","parameters":{"additionalProperties":false,"properties":{"path":{"type":"string"}},"required":["path"],"type":"object"},"strict":true}}]}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '699' + content-type: + - application/json + host: + - api.openai.com + user-agent: + - pydantic-ai/1.10.0 + x-stainless-arch: + - arm64 + x-stainless-async: + - async:asyncio + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 3.31.0 + x-stainless-read-timeout: + - '600' + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.14.7 + method: POST + uri: https://api.openai.com/v1/chat/completions + response: + body: + string: "{\n \"id\": \"chatcmpl-EalMHvelQVK33vAvgHKtsTozM3wcA\",\n \"object\": + \"chat.completion\",\n \"created\": 1792430561,\n \"model\": \"gpt-4o-mini-2024-07-18\",\n + \ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\": + \"assistant\",\n \"content\": \"The file at /tmp/report.txt has been + successfully deleted.\",\n \"refusal\": null,\n \"annotations\": + []\n },\n \"logprobs\": null,\n \"finish_reason\": \"stop\"\n + \ }\n ],\n \"usage\": {\n \"prompt_tokens\": 84,\n \"completion_tokens\": + 13,\n \"total_tokens\": 97,\n \"prompt_tokens_details\": {\n \"cached_tokens\": + 0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\": + {\n \"reasoning_tokens\": 0,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\": + 0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"service_tier\": + \"default\",\n \"system_fingerprint\": \"fp_f263dd6940\"\n}\n" + headers: + access-control-expose-headers: + - X-Request-ID + - CF-Ray + - CF-Ray + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + cf-ray: + - a4d1775f487f8dfa-SJC + connection: + - keep-alive + content-length: + - '865' + content-type: + - application/json + date: + - Mon, 19 Oct 2026 17:22:41 GMT + openai-processing-ms: + - '429' + openai-version: + - '2020-10-01' + server: + - cloudflare + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + transfer-encoding: + - chunked + x-content-type-options: + - nosniff + x-openai-proxy-wasm: + - v0.1 + x-ratelimit-limit-requests: + - '30000' + x-ratelimit-limit-tokens: + - '150000000' + x-ratelimit-remaining-requests: + - '29998' + x-ratelimit-remaining-tokens: + - '149999910' + x-ratelimit-reset-requests: + - 4ms + x-ratelimit-reset-tokens: + - 0s + x-request-id: + - req_e84a364819374fc48df1b66741fe9990 + status: + code: 200 + message: OK +version: 1 diff --git a/py/src/braintrust/integrations/pydantic_ai/cassettes/1.10.0/test_call_deferred_tool_is_not_logged_as_error.yaml b/py/src/braintrust/integrations/pydantic_ai/cassettes/1.10.0/test_call_deferred_tool_is_not_logged_as_error.yaml new file mode 100644 index 00000000..7c1d837e --- /dev/null +++ b/py/src/braintrust/integrations/pydantic_ai/cassettes/1.10.0/test_call_deferred_tool_is_not_logged_as_error.yaml @@ -0,0 +1,108 @@ +interactions: +- request: + body: '{"model":"gpt-4o-mini","stream":false,"messages":[{"role":"user","content":"Start + the background job named ''nightly-report''."}],"max_completion_tokens":200,"tool_choice":"auto","tools":[{"type":"function","function":{"name":"run_long_job","description":"Start + a long-running background job by name.","parameters":{"additionalProperties":false,"properties":{"job_name":{"type":"string"}},"required":["job_name"],"type":"object"},"strict":true}}]}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '445' + content-type: + - application/json + host: + - api.openai.com + user-agent: + - pydantic-ai/1.10.0 + x-stainless-arch: + - arm64 + x-stainless-async: + - async:asyncio + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 3.31.0 + x-stainless-read-timeout: + - '600' + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.14.7 + method: POST + uri: https://api.openai.com/v1/chat/completions + response: + body: + string: "{\n \"id\": \"chatcmpl-EalN723vAa6VVCy5FLfkerEeW7ZXC\",\n \"object\": + \"chat.completion\",\n \"created\": 1792430613,\n \"model\": \"gpt-4o-mini-2024-07-18\",\n + \ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\": + \"assistant\",\n \"content\": null,\n \"tool_calls\": [\n {\n + \ \"id\": \"call_TZtvRUnuo3or4rvoXKNg4uFJ\",\n \"type\": + \"function\",\n \"function\": {\n \"name\": \"run_long_job\",\n + \ \"arguments\": \"{\\\"job_name\\\":\\\"nightly-report\\\"}\"\n + \ }\n }\n ],\n \"refusal\": null,\n \"annotations\": + []\n },\n \"logprobs\": null,\n \"finish_reason\": \"tool_calls\"\n + \ }\n ],\n \"usage\": {\n \"prompt_tokens\": 58,\n \"completion_tokens\": + 18,\n \"total_tokens\": 76,\n \"prompt_tokens_details\": {\n \"cached_tokens\": + 0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\": + {\n \"reasoning_tokens\": 0,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\": + 0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"service_tier\": + \"default\",\n \"system_fingerprint\": \"fp_f263dd6940\"\n}\n" + headers: + access-control-expose-headers: + - X-Request-ID + - CF-Ray + - CF-Ray + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + cf-ray: + - a4d178a59aa2fb2c-SJC + connection: + - keep-alive + content-length: + - '1098' + content-type: + - application/json + date: + - Mon, 19 Oct 2026 17:23:34 GMT + openai-processing-ms: + - '631' + openai-version: + - '2020-10-01' + server: + - cloudflare + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + transfer-encoding: + - chunked + x-content-type-options: + - nosniff + x-openai-proxy-wasm: + - v0.1 + x-ratelimit-limit-requests: + - '30000' + x-ratelimit-limit-tokens: + - '150000000' + x-ratelimit-remaining-requests: + - '29999' + x-ratelimit-remaining-tokens: + - '149999987' + x-ratelimit-reset-requests: + - 2ms + x-ratelimit-reset-tokens: + - 0s + x-request-id: + - req_ee94c2f536d44dcc93d32a56d3e66601 + status: + code: 200 + message: OK +version: 1 diff --git a/py/src/braintrust/integrations/pydantic_ai/cassettes/1.10.0/test_tool_exception_is_logged_as_error.yaml b/py/src/braintrust/integrations/pydantic_ai/cassettes/1.10.0/test_tool_exception_is_logged_as_error.yaml new file mode 100644 index 00000000..753fdea4 --- /dev/null +++ b/py/src/braintrust/integrations/pydantic_ai/cassettes/1.10.0/test_tool_exception_is_logged_as_error.yaml @@ -0,0 +1,108 @@ +interactions: +- request: + body: '{"model":"gpt-4o-mini","stream":false,"messages":[{"role":"user","content":"Publish + the flow with id ''abc''."}],"max_completion_tokens":200,"tool_choice":"auto","tools":[{"type":"function","function":{"name":"publish_flow","description":"Publish + the flow with the given id.","parameters":{"additionalProperties":false,"properties":{"flow_id":{"type":"string"}},"required":["flow_id"],"type":"object"},"strict":true}}]}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '417' + content-type: + - application/json + host: + - api.openai.com + user-agent: + - pydantic-ai/1.10.0 + x-stainless-arch: + - arm64 + x-stainless-async: + - async:asyncio + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 3.31.0 + x-stainless-read-timeout: + - '600' + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.14.7 + method: POST + uri: https://api.openai.com/v1/chat/completions + response: + body: + string: "{\n \"id\": \"chatcmpl-EalMt38bdZyC6fIBrgXCAm9MTNuQu\",\n \"object\": + \"chat.completion\",\n \"created\": 1792430599,\n \"model\": \"gpt-4o-mini-2024-07-18\",\n + \ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\": + \"assistant\",\n \"content\": null,\n \"tool_calls\": [\n {\n + \ \"id\": \"call_NkltHsziosJeIBip48LEEht8\",\n \"type\": + \"function\",\n \"function\": {\n \"name\": \"publish_flow\",\n + \ \"arguments\": \"{\\\"flow_id\\\":\\\"abc\\\"}\"\n }\n + \ }\n ],\n \"refusal\": null,\n \"annotations\": + []\n },\n \"logprobs\": null,\n \"finish_reason\": \"tool_calls\"\n + \ }\n ],\n \"usage\": {\n \"prompt_tokens\": 54,\n \"completion_tokens\": + 15,\n \"total_tokens\": 69,\n \"prompt_tokens_details\": {\n \"cached_tokens\": + 0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\": + {\n \"reasoning_tokens\": 0,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\": + 0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"service_tier\": + \"default\",\n \"system_fingerprint\": \"fp_f263dd6940\"\n}\n" + headers: + access-control-expose-headers: + - X-Request-ID + - CF-Ray + - CF-Ray + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + cf-ray: + - a4d1784beb845850-SJC + connection: + - keep-alive + content-length: + - '1086' + content-type: + - application/json + date: + - Mon, 19 Oct 2026 17:23:19 GMT + openai-processing-ms: + - '533' + openai-version: + - '2020-10-01' + server: + - cloudflare + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + transfer-encoding: + - chunked + x-content-type-options: + - nosniff + x-openai-proxy-wasm: + - v0.1 + x-ratelimit-limit-requests: + - '30000' + x-ratelimit-limit-tokens: + - '150000000' + x-ratelimit-remaining-requests: + - '29999' + x-ratelimit-remaining-tokens: + - '149999990' + x-ratelimit-reset-requests: + - 2ms + x-ratelimit-reset-tokens: + - 0s + x-request-id: + - req_2f0c160ba24b4364bb169e51948f791b + status: + code: 200 + message: OK +version: 1 diff --git a/py/src/braintrust/integrations/pydantic_ai/cassettes/latest/test_approval_required_tool_is_not_logged_as_error_and_resumes.yaml b/py/src/braintrust/integrations/pydantic_ai/cassettes/latest/test_approval_required_tool_is_not_logged_as_error_and_resumes.yaml new file mode 100644 index 00000000..b8c59506 --- /dev/null +++ b/py/src/braintrust/integrations/pydantic_ai/cassettes/latest/test_approval_required_tool_is_not_logged_as_error_and_resumes.yaml @@ -0,0 +1,250 @@ +interactions: +- request: + body: '{"model":"gpt-4o-mini","stream":false,"input":[{"role":"user","content":"Delete + the file at /tmp/report.txt."}],"max_output_tokens":200,"tool_choice":"auto","tools":[{"name":"delete_file","parameters":{"additionalProperties":false,"properties":{"path":{"type":"string"}},"required":["path"],"type":"object"},"type":"function","description":"Delete + the file at the given path.","strict":true}]}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '393' + content-type: + - application/json + host: + - api.openai.com + user-agent: + - pydantic-ai/2.55.0 + x-stainless-arch: + - arm64 + x-stainless-async: + - async:asyncio + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 3.31.0 + x-stainless-read-timeout: + - '600' + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.14.7 + method: POST + uri: https://api.openai.com/v1/responses + response: + body: + string: "{\n \"id\": \"resp_0b9842582d20c89a006ad651cd963887d0a3bc8a5f165fcd68\",\n + \ \"object\": \"response\",\n \"created_at\": 1792430541,\n \"status\": + \"completed\",\n \"access_programs\": null,\n \"background\": false,\n \"billing\": + {\n \"payer\": \"developer\"\n },\n \"completed_at\": 1792430542,\n \"error\": + null,\n \"frequency_penalty\": 0.0,\n \"incomplete_details\": null,\n \"instructions\": + null,\n \"max_output_tokens\": 200,\n \"max_tool_calls\": null,\n \"model\": + \"gpt-4o-mini-2024-07-18\",\n \"moderation\": null,\n \"output\": [\n {\n + \ \"id\": \"fc_0b9842582d20c89a006ad651ce553887d0ae247a2ae6de73fc\",\n + \ \"type\": \"function_call\",\n \"status\": \"completed\",\n \"arguments\": + \"{\\\"path\\\":\\\"/tmp/report.txt\\\"}\",\n \"call_id\": \"call_5Hd74aa3IcU68gzMco09KpGj\",\n + \ \"name\": \"delete_file\"\n }\n ],\n \"parallel_tool_calls\": true,\n + \ \"presence_penalty\": 0.0,\n \"previous_response_id\": null,\n \"prompt_cache_key\": + null,\n \"prompt_cache_retention\": \"in_memory\",\n \"reasoning\": {\n + \ \"context\": null,\n \"effort\": null,\n \"summary\": null\n },\n + \ \"safety_identifier\": null,\n \"service_tier\": \"default\",\n \"store\": + true,\n \"temperature\": 1.0,\n \"text\": {\n \"format\": {\n \"type\": + \"text\"\n },\n \"verbosity\": \"medium\"\n },\n \"tool_choice\": + \"auto\",\n \"tool_usage\": {\n \"image_gen\": {\n \"input_tokens\": + 0,\n \"input_tokens_details\": {\n \"image_tokens\": 0,\n \"text_tokens\": + 0\n },\n \"output_tokens\": 0,\n \"output_tokens_details\": + {\n \"image_tokens\": 0,\n \"text_tokens\": 0\n },\n \"total_tokens\": + 0\n },\n \"web_search\": {\n \"num_requests\": 0\n }\n },\n + \ \"tools\": [\n {\n \"type\": \"function\",\n \"description\": + \"Delete the file at the given path.\",\n \"name\": \"delete_file\",\n + \ \"output_schema\": null,\n \"parameters\": {\n \"additionalProperties\": + false,\n \"properties\": {\n \"path\": {\n \"type\": + \"string\"\n }\n },\n \"required\": [\n \"path\"\n + \ ],\n \"type\": \"object\"\n },\n \"strict\": true\n + \ }\n ],\n \"top_logprobs\": 0,\n \"top_p\": 1.0,\n \"truncation\": + \"disabled\",\n \"usage\": {\n \"input_tokens\": 48,\n \"input_tokens_details\": + {\n \"cache_write_tokens\": 0,\n \"cached_tokens\": 0\n },\n + \ \"output_tokens\": 18,\n \"output_tokens_details\": {\n \"reasoning_tokens\": + 0\n },\n \"total_tokens\": 66\n },\n \"user\": null,\n \"metadata\": + {}\n}" + headers: + access-control-expose-headers: + - X-Request-ID + - CF-Ray + - CF-Ray + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + cf-ray: + - a4d176e47b3f7e56-SJC + connection: + - keep-alive + content-length: + - '2369' + content-type: + - application/json + date: + - Mon, 19 Oct 2026 17:22:22 GMT + openai-processing-ms: + - '972' + openai-version: + - '2020-10-01' + server: + - cloudflare + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + transfer-encoding: + - chunked + x-content-type-options: + - nosniff + x-ratelimit-limit-requests: + - '30000' + x-ratelimit-limit-tokens: + - '150000000' + x-ratelimit-remaining-requests: + - '29999' + x-ratelimit-remaining-tokens: + - '149999735' + x-ratelimit-reset-requests: + - 2ms + x-ratelimit-reset-tokens: + - 0s + x-request-id: + - req_33ec8dab4c90417c8427f8417cd0ef4d + status: + code: 200 + message: OK +- request: + body: '{"model":"gpt-4o-mini","stream":false,"input":[{"role":"user","content":"Delete + the file at /tmp/report.txt."},{"name":"delete_file","arguments":"{\"path\":\"/tmp/report.txt\"}","call_id":"call_5Hd74aa3IcU68gzMco09KpGj","type":"function_call"},{"type":"function_call_output","call_id":"call_5Hd74aa3IcU68gzMco09KpGj","output":"Deleted + /tmp/report.txt"}],"max_output_tokens":200,"tool_choice":"auto","tools":[{"name":"delete_file","parameters":{"additionalProperties":false,"properties":{"path":{"type":"string"}},"required":["path"],"type":"object"},"type":"function","description":"Delete + the file at the given path.","strict":true}]}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '635' + content-type: + - application/json + host: + - api.openai.com + user-agent: + - pydantic-ai/2.55.0 + x-stainless-arch: + - arm64 + x-stainless-async: + - async:asyncio + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 3.31.0 + x-stainless-read-timeout: + - '600' + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.14.7 + method: POST + uri: https://api.openai.com/v1/responses + response: + body: + string: "{\n \"id\": \"resp_0d4894281447a12b006ad651ceb6c887d08aa52e3d4f479609\",\n + \ \"object\": \"response\",\n \"created_at\": 1792430542,\n \"status\": + \"completed\",\n \"access_programs\": null,\n \"background\": false,\n \"billing\": + {\n \"payer\": \"developer\"\n },\n \"completed_at\": 1792430543,\n \"error\": + null,\n \"frequency_penalty\": 0.0,\n \"incomplete_details\": null,\n \"instructions\": + null,\n \"max_output_tokens\": 200,\n \"max_tool_calls\": null,\n \"model\": + \"gpt-4o-mini-2024-07-18\",\n \"moderation\": null,\n \"output\": [\n {\n + \ \"id\": \"msg_0d4894281447a12b006ad651cf2a5487d09efeece673667f39\",\n + \ \"type\": \"message\",\n \"status\": \"completed\",\n \"content\": + [\n {\n \"type\": \"output_text\",\n \"annotations\": + [],\n \"logprobs\": [],\n \"text\": \"The file at `/tmp/report.txt` + has been successfully deleted.\"\n }\n ],\n \"role\": \"assistant\"\n + \ }\n ],\n \"parallel_tool_calls\": true,\n \"presence_penalty\": 0.0,\n + \ \"previous_response_id\": null,\n \"prompt_cache_key\": null,\n \"prompt_cache_retention\": + \"in_memory\",\n \"reasoning\": {\n \"context\": null,\n \"effort\": + null,\n \"summary\": null\n },\n \"safety_identifier\": null,\n \"service_tier\": + \"default\",\n \"store\": true,\n \"temperature\": 1.0,\n \"text\": {\n + \ \"format\": {\n \"type\": \"text\"\n },\n \"verbosity\": \"medium\"\n + \ },\n \"tool_choice\": \"auto\",\n \"tool_usage\": {\n \"image_gen\": + {\n \"input_tokens\": 0,\n \"input_tokens_details\": {\n \"image_tokens\": + 0,\n \"text_tokens\": 0\n },\n \"output_tokens\": 0,\n \"output_tokens_details\": + {\n \"image_tokens\": 0,\n \"text_tokens\": 0\n },\n \"total_tokens\": + 0\n },\n \"web_search\": {\n \"num_requests\": 0\n }\n },\n + \ \"tools\": [\n {\n \"type\": \"function\",\n \"description\": + \"Delete the file at the given path.\",\n \"name\": \"delete_file\",\n + \ \"output_schema\": null,\n \"parameters\": {\n \"additionalProperties\": + false,\n \"properties\": {\n \"path\": {\n \"type\": + \"string\"\n }\n },\n \"required\": [\n \"path\"\n + \ ],\n \"type\": \"object\"\n },\n \"strict\": true\n + \ }\n ],\n \"top_logprobs\": 0,\n \"top_p\": 1.0,\n \"truncation\": + \"disabled\",\n \"usage\": {\n \"input_tokens\": 76,\n \"input_tokens_details\": + {\n \"cache_write_tokens\": 0,\n \"cached_tokens\": 0\n },\n + \ \"output_tokens\": 15,\n \"output_tokens_details\": {\n \"reasoning_tokens\": + 0\n },\n \"total_tokens\": 91\n },\n \"user\": null,\n \"metadata\": + {}\n}" + headers: + access-control-expose-headers: + - X-Request-ID + - CF-Ray + - CF-Ray + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + cf-ray: + - a4d176eb68427e56-SJC + connection: + - keep-alive + content-length: + - '2476' + content-type: + - application/json + date: + - Mon, 19 Oct 2026 17:22:23 GMT + openai-processing-ms: + - '753' + openai-version: + - '2020-10-01' + server: + - cloudflare + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + transfer-encoding: + - chunked + x-content-type-options: + - nosniff + x-ratelimit-limit-requests: + - '30000' + x-ratelimit-limit-tokens: + - '150000000' + x-ratelimit-remaining-requests: + - '29998' + x-ratelimit-remaining-tokens: + - '149999427' + x-ratelimit-reset-requests: + - 4ms + x-ratelimit-reset-tokens: + - 0s + x-request-id: + - req_77ecbf107a4948749f8c05fcd25966a6 + status: + code: 200 + message: OK +version: 1 diff --git a/py/src/braintrust/integrations/pydantic_ai/cassettes/latest/test_call_deferred_tool_is_not_logged_as_error.yaml b/py/src/braintrust/integrations/pydantic_ai/cassettes/latest/test_call_deferred_tool_is_not_logged_as_error.yaml new file mode 100644 index 00000000..45b6ee8f --- /dev/null +++ b/py/src/braintrust/integrations/pydantic_ai/cassettes/latest/test_call_deferred_tool_is_not_logged_as_error.yaml @@ -0,0 +1,125 @@ +interactions: +- request: + body: '{"model":"gpt-4o-mini","stream":false,"input":[{"role":"user","content":"Start + the background job named ''nightly-report''."}],"max_output_tokens":200,"tool_choice":"auto","tools":[{"name":"run_long_job","parameters":{"additionalProperties":false,"properties":{"job_name":{"type":"string"}},"required":["job_name"],"type":"object"},"type":"function","description":"Start + a long-running background job by name.","strict":true}]}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '425' + content-type: + - application/json + host: + - api.openai.com + user-agent: + - pydantic-ai/2.55.0 + x-stainless-arch: + - arm64 + x-stainless-async: + - async:asyncio + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 3.31.0 + x-stainless-read-timeout: + - '600' + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.14.7 + method: POST + uri: https://api.openai.com/v1/responses + response: + body: + string: "{\n \"id\": \"resp_0c42a124c3330e76006ad651a4a6f887d08c5105d9a4e0ac6a\",\n + \ \"object\": \"response\",\n \"created_at\": 1792430500,\n \"status\": + \"completed\",\n \"access_programs\": null,\n \"background\": false,\n \"billing\": + {\n \"payer\": \"developer\"\n },\n \"completed_at\": 1792430501,\n \"error\": + null,\n \"frequency_penalty\": 0.0,\n \"incomplete_details\": null,\n \"instructions\": + null,\n \"max_output_tokens\": 200,\n \"max_tool_calls\": null,\n \"model\": + \"gpt-4o-mini-2024-07-18\",\n \"moderation\": null,\n \"output\": [\n {\n + \ \"id\": \"fc_0c42a124c3330e76006ad651a55af087d0bbfcf6aba0325eea\",\n + \ \"type\": \"function_call\",\n \"status\": \"completed\",\n \"arguments\": + \"{\\\"job_name\\\":\\\"nightly-report\\\"}\",\n \"call_id\": \"call_kktGKL1q2akaetyzvE7FpnKU\",\n + \ \"name\": \"run_long_job\"\n }\n ],\n \"parallel_tool_calls\": + true,\n \"presence_penalty\": 0.0,\n \"previous_response_id\": null,\n \"prompt_cache_key\": + null,\n \"prompt_cache_retention\": \"in_memory\",\n \"reasoning\": {\n + \ \"context\": null,\n \"effort\": null,\n \"summary\": null\n },\n + \ \"safety_identifier\": null,\n \"service_tier\": \"default\",\n \"store\": + true,\n \"temperature\": 1.0,\n \"text\": {\n \"format\": {\n \"type\": + \"text\"\n },\n \"verbosity\": \"medium\"\n },\n \"tool_choice\": + \"auto\",\n \"tool_usage\": {\n \"image_gen\": {\n \"input_tokens\": + 0,\n \"input_tokens_details\": {\n \"image_tokens\": 0,\n \"text_tokens\": + 0\n },\n \"output_tokens\": 0,\n \"output_tokens_details\": + {\n \"image_tokens\": 0,\n \"text_tokens\": 0\n },\n \"total_tokens\": + 0\n },\n \"web_search\": {\n \"num_requests\": 0\n }\n },\n + \ \"tools\": [\n {\n \"type\": \"function\",\n \"description\": + \"Start a long-running background job by name.\",\n \"name\": \"run_long_job\",\n + \ \"output_schema\": null,\n \"parameters\": {\n \"additionalProperties\": + false,\n \"properties\": {\n \"job_name\": {\n \"type\": + \"string\"\n }\n },\n \"required\": [\n \"job_name\"\n + \ ],\n \"type\": \"object\"\n },\n \"strict\": true\n + \ }\n ],\n \"top_logprobs\": 0,\n \"top_p\": 1.0,\n \"truncation\": + \"disabled\",\n \"usage\": {\n \"input_tokens\": 52,\n \"input_tokens_details\": + {\n \"cache_write_tokens\": 0,\n \"cached_tokens\": 0\n },\n + \ \"output_tokens\": 19,\n \"output_tokens_details\": {\n \"reasoning_tokens\": + 0\n },\n \"total_tokens\": 71\n },\n \"user\": null,\n \"metadata\": + {}\n}" + headers: + access-control-expose-headers: + - X-Request-ID + - CF-Ray + - CF-Ray + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + cf-ray: + - a4d175e37ebcc8de-SJC + connection: + - keep-alive + content-length: + - '2392' + content-type: + - application/json + date: + - Mon, 19 Oct 2026 17:21:41 GMT + openai-processing-ms: + - '981' + openai-version: + - '2020-10-01' + server: + - cloudflare + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + transfer-encoding: + - chunked + x-content-type-options: + - nosniff + x-ratelimit-limit-requests: + - '30000' + x-ratelimit-limit-tokens: + - '150000000' + x-ratelimit-remaining-requests: + - '29999' + x-ratelimit-remaining-tokens: + - '149999732' + x-ratelimit-reset-requests: + - 2ms + x-ratelimit-reset-tokens: + - 0s + x-request-id: + - req_7a3b640b3205457e84944c1ab53cc37c + status: + code: 200 + message: OK +version: 1 diff --git a/py/src/braintrust/integrations/pydantic_ai/cassettes/latest/test_tool_exception_is_logged_as_error.yaml b/py/src/braintrust/integrations/pydantic_ai/cassettes/latest/test_tool_exception_is_logged_as_error.yaml new file mode 100644 index 00000000..de1e7ca0 --- /dev/null +++ b/py/src/braintrust/integrations/pydantic_ai/cassettes/latest/test_tool_exception_is_logged_as_error.yaml @@ -0,0 +1,125 @@ +interactions: +- request: + body: '{"model":"gpt-4o-mini","stream":false,"input":[{"role":"user","content":"Publish + the flow with id ''abc''."}],"max_output_tokens":200,"tool_choice":"auto","tools":[{"name":"publish_flow","parameters":{"additionalProperties":false,"properties":{"flow_id":{"type":"string"}},"required":["flow_id"],"type":"object"},"type":"function","description":"Publish + the flow with the given id.","strict":true}]}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '397' + content-type: + - application/json + host: + - api.openai.com + user-agent: + - pydantic-ai/2.55.0 + x-stainless-arch: + - arm64 + x-stainless-async: + - async:asyncio + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 3.31.0 + x-stainless-read-timeout: + - '600' + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.14.7 + method: POST + uri: https://api.openai.com/v1/responses + response: + body: + string: "{\n \"id\": \"resp_08b3b6880c09c7c9006ad651a70d3087d08e4625b3729da592\",\n + \ \"object\": \"response\",\n \"created_at\": 1792430503,\n \"status\": + \"completed\",\n \"access_programs\": null,\n \"background\": false,\n \"billing\": + {\n \"payer\": \"developer\"\n },\n \"completed_at\": 1792430503,\n \"error\": + null,\n \"frequency_penalty\": 0.0,\n \"incomplete_details\": null,\n \"instructions\": + null,\n \"max_output_tokens\": 200,\n \"max_tool_calls\": null,\n \"model\": + \"gpt-4o-mini-2024-07-18\",\n \"moderation\": null,\n \"output\": [\n {\n + \ \"id\": \"fc_08b3b6880c09c7c9006ad651a79b7c87d0adb76c4caa42b469\",\n + \ \"type\": \"function_call\",\n \"status\": \"completed\",\n \"arguments\": + \"{\\\"flow_id\\\":\\\"abc\\\"}\",\n \"call_id\": \"call_4RdltplmSlPUY0JxuESW7xOb\",\n + \ \"name\": \"publish_flow\"\n }\n ],\n \"parallel_tool_calls\": + true,\n \"presence_penalty\": 0.0,\n \"previous_response_id\": null,\n \"prompt_cache_key\": + null,\n \"prompt_cache_retention\": \"in_memory\",\n \"reasoning\": {\n + \ \"context\": null,\n \"effort\": null,\n \"summary\": null\n },\n + \ \"safety_identifier\": null,\n \"service_tier\": \"default\",\n \"store\": + true,\n \"temperature\": 1.0,\n \"text\": {\n \"format\": {\n \"type\": + \"text\"\n },\n \"verbosity\": \"medium\"\n },\n \"tool_choice\": + \"auto\",\n \"tool_usage\": {\n \"image_gen\": {\n \"input_tokens\": + 0,\n \"input_tokens_details\": {\n \"image_tokens\": 0,\n \"text_tokens\": + 0\n },\n \"output_tokens\": 0,\n \"output_tokens_details\": + {\n \"image_tokens\": 0,\n \"text_tokens\": 0\n },\n \"total_tokens\": + 0\n },\n \"web_search\": {\n \"num_requests\": 0\n }\n },\n + \ \"tools\": [\n {\n \"type\": \"function\",\n \"description\": + \"Publish the flow with the given id.\",\n \"name\": \"publish_flow\",\n + \ \"output_schema\": null,\n \"parameters\": {\n \"additionalProperties\": + false,\n \"properties\": {\n \"flow_id\": {\n \"type\": + \"string\"\n }\n },\n \"required\": [\n \"flow_id\"\n + \ ],\n \"type\": \"object\"\n },\n \"strict\": true\n + \ }\n ],\n \"top_logprobs\": 0,\n \"top_p\": 1.0,\n \"truncation\": + \"disabled\",\n \"usage\": {\n \"input_tokens\": 48,\n \"input_tokens_details\": + {\n \"cache_write_tokens\": 0,\n \"cached_tokens\": 0\n },\n + \ \"output_tokens\": 16,\n \"output_tokens_details\": {\n \"reasoning_tokens\": + 0\n },\n \"total_tokens\": 64\n },\n \"user\": null,\n \"metadata\": + {}\n}" + headers: + access-control-expose-headers: + - X-Request-ID + - CF-Ray + - CF-Ray + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + cf-ray: + - a4d175f3ae3fed3d-SJC + connection: + - keep-alive + content-length: + - '2369' + content-type: + - application/json + date: + - Mon, 19 Oct 2026 17:21:43 GMT + openai-processing-ms: + - '706' + openai-version: + - '2020-10-01' + server: + - cloudflare + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + transfer-encoding: + - chunked + x-content-type-options: + - nosniff + x-ratelimit-limit-requests: + - '30000' + x-ratelimit-limit-tokens: + - '150000000' + x-ratelimit-remaining-requests: + - '29998' + x-ratelimit-remaining-tokens: + - '149999455' + x-ratelimit-reset-requests: + - 4ms + x-ratelimit-reset-tokens: + - 0s + x-request-id: + - req_23724340fc69450995af74ad7b3a84e2 + status: + code: 200 + message: OK +version: 1 diff --git a/py/src/braintrust/integrations/pydantic_ai/test_pydantic_ai_integration.py b/py/src/braintrust/integrations/pydantic_ai/test_pydantic_ai_integration.py index 8cc7d49b..fa194d7a 100644 --- a/py/src/braintrust/integrations/pydantic_ai/test_pydantic_ai_integration.py +++ b/py/src/braintrust/integrations/pydantic_ai/test_pydantic_ai_integration.py @@ -1617,6 +1617,114 @@ def calculate(operation: str, a: float, b: float) -> float: ) +def _single_tool_span(spans, name): + tool_spans = [s for s in spans if s["span_attributes"]["name"] == name] + assert len(tool_spans) == 1, f"Expected exactly 1 {name} span, got {len(tool_spans)}" + assert tool_spans[0]["span_attributes"]["type"] == SpanTypeAttribute.TOOL + return tool_spans[0] + + +def _assert_deferred_tool_span(span, tool_call_id, kind): + assert span.get("error") is None + assert span.get("output") is None + assert span["metadata"]["tool_call_id"] == tool_call_id + assert span["metadata"]["deferred"] == kind + assert span["metrics"]["duration"] >= 0 + + +@pytest.mark.vcr +@pytest.mark.asyncio +async def test_call_deferred_tool_is_not_logged_as_error(memory_logger): + """A tool raising CallDeferred hands the call back to the caller; that is not a tool failure.""" + from pydantic_ai import DeferredToolRequests + from pydantic_ai.exceptions import CallDeferred + + assert not memory_logger.pop() + + # CallDeferred(metadata=...) is only available in newer pydantic_ai versions. + supports_metadata = hasattr(CallDeferred(), "metadata") + + agent = Agent(MODEL, model_settings=ModelSettings(max_tokens=200), output_type=[str, DeferredToolRequests]) + + @agent.tool_plain + def run_long_job(job_name: str) -> str: + """Start a long-running background job by name.""" + if supports_metadata: + raise CallDeferred(metadata={"job_id": "job-123"}) + raise CallDeferred() + + result = await agent.run("Start the background job named 'nightly-report'.") + + assert isinstance(result.output, DeferredToolRequests) + assert [call.tool_name for call in result.output.calls] == ["run_long_job"] + + tool_span = _single_tool_span(memory_logger.pop(), "run_long_job") + _assert_deferred_tool_span(tool_span, result.output.calls[0].tool_call_id, "external") + if supports_metadata: + assert tool_span["metadata"]["deferred_metadata"] == {"job_id": "job-123"} + + +@pytest.mark.vcr +@pytest.mark.asyncio +async def test_approval_required_tool_is_not_logged_as_error_and_resumes(memory_logger): + """ApprovalRequired defers the call without an error span; the approved re-run logs normal output.""" + from pydantic_ai import DeferredToolRequests, DeferredToolResults, RunContext + from pydantic_ai.exceptions import ApprovalRequired + + assert not memory_logger.pop() + + agent = Agent(MODEL, model_settings=ModelSettings(max_tokens=200), output_type=[str, DeferredToolRequests]) + + @agent.tool + def delete_file(ctx: RunContext[None], path: str) -> str: + """Delete the file at the given path.""" + if not ctx.tool_call_approved: + raise ApprovalRequired() + return f"Deleted {path}" + + first = await agent.run("Delete the file at /tmp/report.txt.") + + assert isinstance(first.output, DeferredToolRequests) + assert [call.tool_name for call in first.output.approvals] == ["delete_file"] + tool_call_id = first.output.approvals[0].tool_call_id + + _assert_deferred_tool_span(_single_tool_span(memory_logger.pop(), "delete_file"), tool_call_id, "unapproved") + + second = await agent.run( + message_history=first.all_messages(), + deferred_tool_results=DeferredToolResults(approvals={tool_call_id: True}), + ) + + assert isinstance(second.output, str) + + approved_span = _single_tool_span(memory_logger.pop(), "delete_file") + assert approved_span.get("error") is None + assert approved_span["output"] == "Deleted /tmp/report.txt" + assert approved_span["metadata"]["tool_call_id"] == tool_call_id + assert "deferred" not in approved_span["metadata"] + + +@pytest.mark.vcr +@pytest.mark.asyncio +async def test_tool_exception_is_logged_as_error(memory_logger): + """Genuine tool failures must still be logged as errors on the tool span.""" + assert not memory_logger.pop() + + agent = Agent(MODEL, model_settings=ModelSettings(max_tokens=200)) + + @agent.tool_plain + def publish_flow(flow_id: str) -> str: + """Publish the flow with the given id.""" + raise RuntimeError("publish failed") + + with pytest.raises(RuntimeError, match="publish failed"): + await agent.run("Publish the flow with id 'abc'.") + + tool_span = _single_tool_span(memory_logger.pop(), "publish_flow") + assert "publish failed" in tool_span["error"] + assert "deferred" not in tool_span["metadata"] + + def test_agent_tool_metadata_extraction(memory_logger): """Test that agent tools are properly extracted with full schemas in INPUT (not metadata). diff --git a/py/src/braintrust/integrations/pydantic_ai/tracing.py b/py/src/braintrust/integrations/pydantic_ai/tracing.py index 3c7a1bc0..9eca9918 100644 --- a/py/src/braintrust/integrations/pydantic_ai/tracing.py +++ b/py/src/braintrust/integrations/pydantic_ai/tracing.py @@ -8,7 +8,7 @@ from contextlib import AbstractAsyncContextManager from typing import Any -from braintrust.integrations.utils import _materialize_attachment +from braintrust.integrations.utils import _materialize_attachment, _timing_metrics from braintrust.logger import _internal_get_global_state, current_span from braintrust.logger import start_span as _bt_start_span @@ -984,16 +984,35 @@ async def _trace_tool_execution(wrapped: Any, args: Any, kwargs: Any): metadata = {"tool_call_id": tool_call_id} if tool_call_id else None + from pydantic_ai.exceptions import ApprovalRequired, CallDeferred + _mark_tool_span_emitted() + deferral: CallDeferred | ApprovalRequired | None = None with start_span(name=tool_name, type=SpanTypeAttribute.TOOL, input=input_data, metadata=metadata) as tool_span: start_time = time.time() - result = await wrapped(*args, **kwargs) + try: + result = await wrapped(*args, **kwargs) + except (CallDeferred, ApprovalRequired) as exc: + # The tool handed the call back to the caller as DeferredToolRequests. + # That is control flow, not a tool failure, so the span must not record an error. + deferral = exc end_time = time.time() - tool_span.log( - output=result, - metrics={"start": start_time, "end": end_time, "duration": end_time - start_time}, - ) - return result + metrics = _timing_metrics(start_time, end_time) + if deferral is None: + tool_span.log(output=result, metrics=metrics) + else: + # Same kind names pydantic_ai uses for deferred tool calls. + deferred_metadata = {"deferred": "unapproved" if isinstance(deferral, ApprovalRequired) else "external"} + if getattr(deferral, "metadata", None): + deferred_metadata["deferred_metadata"] = deferral.metadata + tool_span.log(metadata=deferred_metadata, metrics=metrics) + if deferral is not None: + try: + raise deferral + finally: + # Break the exception -> traceback -> frame -> deferral reference cycle. + deferral = None + return result async def _tool_manager_call_function_tool_wrapper(wrapped: Any, instance: Any, args: Any, kwargs: Any):