sovara 0.2.4__tar.gz → 0.2.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. {sovara-0.2.4 → sovara-0.2.5}/PKG-INFO +2 -1
  2. {sovara-0.2.4 → sovara-0.2.5}/pyproject.toml +3 -2
  3. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/runner/context_manager.py +10 -0
  4. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/runner/monkey_patching/api_parser.py +25 -27
  5. sovara-0.2.5/src/sovara/runner/monkey_patching/api_parsers/claude_code_messages_api_parser.py +47 -0
  6. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/runner/monkey_patching/edit_io_filters.py +14 -0
  7. sovara-0.2.5/src/sovara/runner/monkey_patching/patches/claude_sdk_patch.py +181 -0
  8. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/runner/monkey_patching/patching_utils.py +46 -10
  9. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/runner/node_execution.py +28 -5
  10. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/runner/runtime_client.py +56 -0
  11. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/app.py +2 -0
  12. sovara-0.2.5/src/sovara/server/claude_proxy_utils.py +277 -0
  13. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/database/llm_calls.py +26 -8
  14. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/handlers/runner_handlers.py +2 -0
  15. sovara-0.2.5/src/sovara/server/routes/claude_proxy.py +426 -0
  16. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/routes/internal.py +21 -0
  17. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/services/trace_chat_service.py +3 -7
  18. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/state.py +52 -0
  19. {sovara-0.2.4 → sovara-0.2.5}/src/sovara.egg-info/PKG-INFO +2 -1
  20. {sovara-0.2.4 → sovara-0.2.5}/src/sovara.egg-info/SOURCES.txt +3 -2
  21. {sovara-0.2.4 → sovara-0.2.5}/src/sovara.egg-info/requires.txt +1 -0
  22. sovara-0.2.4/src/sovara/runner/monkey_patching/api_parsers/claude_sdk_api_parser.py +0 -83
  23. sovara-0.2.4/src/sovara/runner/monkey_patching/api_parsers/langchain_tool_api_parser.py +0 -33
  24. sovara-0.2.4/src/sovara/runner/monkey_patching/patches/claude_sdk_patch.py +0 -335
  25. {sovara-0.2.4 → sovara-0.2.5}/LICENSE +0 -0
  26. {sovara-0.2.4 → sovara-0.2.5}/README.md +0 -0
  27. {sovara-0.2.4 → sovara-0.2.5}/docs/release/PYPI_DESC.md +0 -0
  28. {sovara-0.2.4 → sovara-0.2.5}/setup.cfg +0 -0
  29. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/__init__.py +0 -0
  30. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/assets/SKILL.md +0 -0
  31. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/assets/__init__.py +0 -0
  32. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/assets/web_app/assets/AttachmentPreview-CFrNMdfC.css +0 -0
  33. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/assets/web_app/assets/AttachmentPreview-h01XSt_Q.js +0 -0
  34. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/assets/web_app/assets/DocumentPreviewModal-B9FumoYT.js +0 -0
  35. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/assets/web_app/assets/LayoutBreadcrumb-D8Jj9PNg.js +0 -0
  36. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/assets/web_app/assets/LayoutBreadcrumb-DopPRSia.css +0 -0
  37. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/assets/web_app/assets/OrgRoute-BcO5LcH1.js +0 -0
  38. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/assets/web_app/assets/OrgRoute-DIln18S1.css +0 -0
  39. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/assets/web_app/assets/PriorsRoute-BSsFm0pQ.css +0 -0
  40. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/assets/web_app/assets/PriorsRoute-CaMBZzP_.js +0 -0
  41. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/assets/web_app/assets/ProjectRoute-BCA5W9t8.js +0 -0
  42. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/assets/web_app/assets/ProjectRoute-DtQqJb1n.css +0 -0
  43. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/assets/web_app/assets/RunRoute-BWs7EUiD.css +0 -0
  44. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/assets/web_app/assets/RunRoute-rUSpBhsI.js +0 -0
  45. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/assets/web_app/assets/SettingsRoute-BtETGx6c.js +0 -0
  46. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/assets/web_app/assets/SettingsRoute-C4VU70zq.css +0 -0
  47. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/assets/web_app/assets/api-1mbBSn7v.css +0 -0
  48. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/assets/web_app/assets/api-tVJBLHUH.js +0 -0
  49. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/assets/web_app/assets/arrow_lr-ByI2ndPB.png +0 -0
  50. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/assets/web_app/assets/arrow_spiral_tr_bl-XHIpf338.png +0 -0
  51. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/assets/web_app/assets/codicon-BYm2YbZ6.ttf +0 -0
  52. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/assets/web_app/assets/eye-DljehhmM.js +0 -0
  53. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/assets/web_app/assets/index-BygN1cJK.js +0 -0
  54. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/assets/web_app/assets/index-veNT10PX.css +0 -0
  55. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/assets/web_app/assets/pdf.worker.min-qwK7q_zL.mjs +0 -0
  56. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/assets/web_app/assets/plus-CkqVCne8.js +0 -0
  57. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/assets/web_app/assets/prismTheme-Bx8cYzkm.js +0 -0
  58. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/assets/web_app/assets/sovara_wordmark-CjkpVR0a.png +0 -0
  59. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/assets/web_app/assets/tierState-Bq39H0kz.js +0 -0
  60. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/assets/web_app/assets/trash-2-DYeReTGL.js +0 -0
  61. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/assets/web_app/favicon.png +0 -0
  62. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/assets/web_app/index.html +0 -0
  63. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/assets/web_app/marketplace_icon.png +0 -0
  64. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/cli/__init__.py +0 -0
  65. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/cli/so_config.py +0 -0
  66. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/cli/so_record.py +0 -0
  67. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/cli/so_server.py +0 -0
  68. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/cli/so_tool.py +0 -0
  69. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/common/__init__.py +0 -0
  70. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/common/config.py +0 -0
  71. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/common/constants.py +0 -0
  72. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/common/custom_metrics.py +0 -0
  73. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/common/lifecycle.py +0 -0
  74. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/common/log_redaction.py +0 -0
  75. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/common/logger.py +0 -0
  76. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/common/project.py +0 -0
  77. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/common/runtime.py +0 -0
  78. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/common/user.py +0 -0
  79. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/common/utils.py +0 -0
  80. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/runner/__init__.py +0 -0
  81. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/runner/agent_runner.py +0 -0
  82. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/runner/monkey_patching/__init__.py +0 -0
  83. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/runner/monkey_patching/api_parsers/__init__.py +0 -0
  84. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/runner/monkey_patching/api_parsers/aiohttp_api_parser.py +0 -0
  85. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/runner/monkey_patching/api_parsers/httpx_api_parser.py +0 -0
  86. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/runner/monkey_patching/api_parsers/mcp_api_parser.py +0 -0
  87. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/runner/monkey_patching/api_parsers/requests_api_parser.py +0 -0
  88. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/runner/monkey_patching/api_parsers/sse_utils.py +0 -0
  89. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/runner/monkey_patching/api_parsers/urllib3_api_parser.py +0 -0
  90. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/runner/monkey_patching/apply_monkey_patches.py +0 -0
  91. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/runner/monkey_patching/async_streaming.py +0 -0
  92. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/runner/monkey_patching/endpoint_whitelist.py +0 -0
  93. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/runner/monkey_patching/node_naming.py +0 -0
  94. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/runner/monkey_patching/patches/__init__.py +0 -0
  95. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/runner/monkey_patching/patches/aiohttp_patch.py +0 -0
  96. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/runner/monkey_patching/patches/httpx_patch.py +0 -0
  97. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/runner/monkey_patching/patches/langchain_patch.py +0 -0
  98. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/runner/monkey_patching/patches/mcp_patches.py +0 -0
  99. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/runner/monkey_patching/patches/randomness_patch.py +0 -0
  100. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/runner/monkey_patching/patches/requests_patch.py +0 -0
  101. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/runner/monkey_patching/patches/urllib3_patch.py +0 -0
  102. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/runner/priors.py +0 -0
  103. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/runner/priors_pipeline.py +0 -0
  104. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/runner/run_log_capture.py +0 -0
  105. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/runner/string_matching.py +0 -0
  106. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/__init__.py +0 -0
  107. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/database/__init__.py +0 -0
  108. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/database/_shared.py +0 -0
  109. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/database/manager.py +0 -0
  110. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/database/priors.py +0 -0
  111. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/database/projects.py +0 -0
  112. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/database/runs.py +0 -0
  113. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/database/sqlite/__init__.py +0 -0
  114. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/database/sqlite/connection.py +0 -0
  115. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/database/sqlite/llm_calls.py +0 -0
  116. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/database/sqlite/priors.py +0 -0
  117. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/database/sqlite/projects.py +0 -0
  118. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/database/sqlite/runs.py +0 -0
  119. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/database/sqlite/schema.py +0 -0
  120. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/database/sqlite/users.py +0 -0
  121. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/database/users.py +0 -0
  122. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/graph_analysis/inference_server.py +0 -0
  123. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/graph_analysis/trace_chat/__init__.py +0 -0
  124. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/graph_analysis/trace_chat/cancel.py +0 -0
  125. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/graph_analysis/trace_chat/main.py +0 -0
  126. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/graph_analysis/trace_chat/tools/__init__.py +0 -0
  127. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/graph_analysis/trace_chat/tools/ask_step.py +0 -0
  128. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/graph_analysis/trace_chat/tools/edit_content.py +0 -0
  129. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/graph_analysis/trace_chat/tools/get_step_overview.py +0 -0
  130. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/graph_analysis/trace_chat/tools/get_step_snapshot.py +0 -0
  131. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/graph_analysis/trace_chat/tools/get_trace_overview.py +0 -0
  132. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/graph_analysis/trace_chat/tools/search.py +0 -0
  133. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/graph_analysis/trace_chat/tools/summarize_trace.py +0 -0
  134. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/graph_analysis/trace_chat/tools/verify.py +0 -0
  135. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/graph_analysis/trace_chat/utils/__init__.py +0 -0
  136. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/graph_analysis/trace_chat/utils/content_utils.py +0 -0
  137. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/graph_analysis/trace_chat/utils/context.py +0 -0
  138. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/graph_analysis/trace_chat/utils/edit_persist.py +0 -0
  139. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/graph_analysis/trace_chat/utils/step_content_view.py +0 -0
  140. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/graph_analysis/trace_chat/utils/step_ids.py +0 -0
  141. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/graph_analysis/trace_chat/utils/trace.py +0 -0
  142. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/graph_analysis/trace_chat/utils/verdict_persist.py +0 -0
  143. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/graph_models.py +0 -0
  144. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/handlers/__init__.py +0 -0
  145. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/handlers/cli_handlers.py +0 -0
  146. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/handlers/handler_utils.py +0 -0
  147. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/handlers/ui_handlers.py +0 -0
  148. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/llm_backend.py +0 -0
  149. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/llm_provider_catalog.py +0 -0
  150. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/llm_settings.py +0 -0
  151. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/llm_settings_verification.py +0 -0
  152. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/package_version.py +0 -0
  153. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/prior_display.py +0 -0
  154. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/priors_backend/__init__.py +0 -0
  155. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/priors_backend/constants.py +0 -0
  156. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/priors_backend/events.py +0 -0
  157. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/priors_backend/llm/__init__.py +0 -0
  158. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/priors_backend/llm/lesson_restructurer.py +0 -0
  159. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/priors_backend/llm/lesson_retriever.py +0 -0
  160. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/priors_backend/llm/lesson_summarizer.py +0 -0
  161. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/priors_backend/llm/lesson_validator.py +0 -0
  162. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/priors_backend/llm_client.py +0 -0
  163. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/priors_backend/logger.py +0 -0
  164. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/priors_backend/routes.py +0 -0
  165. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/priors_backend/storage.py +0 -0
  166. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/priors_scope.py +0 -0
  167. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/routes/__init__.py +0 -0
  168. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/routes/events.py +0 -0
  169. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/routes/runner.py +0 -0
  170. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/routes/ui.py +0 -0
  171. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/routes/ui_common.py +0 -0
  172. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/routes/ui_projects.py +0 -0
  173. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/routes/ui_runs.py +0 -0
  174. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/routes/ui_trace_chat.py +0 -0
  175. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/services/__init__.py +0 -0
  176. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/third_party_model_catalog.py +0 -0
  177. {sovara-0.2.4 → sovara-0.2.5}/src/sovara/server/web_app_assets.py +0 -0
  178. {sovara-0.2.4 → sovara-0.2.5}/src/sovara.egg-info/dependency_links.txt +0 -0
  179. {sovara-0.2.4 → sovara-0.2.5}/src/sovara.egg-info/entry_points.txt +0 -0
  180. {sovara-0.2.4 → sovara-0.2.5}/src/sovara.egg-info/top_level.txt +0 -0
  181. {sovara-0.2.4 → sovara-0.2.5}/tests/test_graph_payloads.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sovara
3
- Version: 0.2.4
3
+ Version: 0.2.5
4
4
  Summary: A development tool for LLM call visualization and debugging
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
@@ -43,6 +43,7 @@ Requires-Dist: openai; extra == "examples"
43
43
  Requires-Dist: anthropic; extra == "examples"
44
44
  Requires-Dist: openai-agents; extra == "examples"
45
45
  Requires-Dist: google-genai; extra == "examples"
46
+ Requires-Dist: claude-agent-sdk; extra == "examples"
46
47
  Provides-Extra: docs
47
48
  Requires-Dist: mkdocs-material>=9.5; extra == "docs"
48
49
  Requires-Dist: mkdocs-minify-plugin; extra == "docs"
@@ -26,7 +26,7 @@ line-length = 100
26
26
 
27
27
  [project]
28
28
  name = "sovara"
29
- version = "0.2.4"
29
+ version = "0.2.5"
30
30
  description = "A development tool for LLM call visualization and debugging"
31
31
  readme = "docs/release/PYPI_DESC.md"
32
32
  requires-python = ">=3.10"
@@ -76,7 +76,8 @@ examples = [
76
76
  "openai",
77
77
  "anthropic",
78
78
  "openai-agents",
79
- "google-genai"
79
+ "google-genai",
80
+ "claude-agent-sdk"
80
81
  ]
81
82
 
82
83
  docs = [
@@ -97,6 +97,16 @@ def sovara_launch(run_name="Workflow run"):
97
97
  clear_run_timer(run_id)
98
98
 
99
99
 
100
+ @contextmanager
101
+ def bind_run_id(run_id: str | None):
102
+ """Temporarily bind the active run id inside the current context."""
103
+ token = current_run_id.set(run_id)
104
+ try:
105
+ yield
106
+ finally:
107
+ current_run_id.reset(token)
108
+
109
+
100
110
  def log(**metrics):
101
111
  run_id = warn_if_inactive_run("log()")
102
112
  if run_id is None:
@@ -27,19 +27,12 @@ from sovara.runner.monkey_patching.api_parsers.requests_api_parser import (
27
27
  json_str_to_api_obj_requests,
28
28
  json_str_to_original_inp_dict_requests,
29
29
  )
30
- from sovara.runner.monkey_patching.api_parsers.claude_sdk_api_parser import (
31
- build_to_show_claude_sdk_input,
32
- build_to_show_claude_sdk_output,
33
- func_kwargs_to_json_str_claude_sdk,
34
- api_obj_to_json_str_claude_sdk,
35
- json_str_to_api_obj_claude_sdk,
36
- json_str_to_original_inp_dict_claude_sdk,
37
- )
38
- from sovara.runner.monkey_patching.api_parsers.langchain_tool_api_parser import (
39
- func_kwargs_to_json_str_langchain_tool,
40
- api_obj_to_json_str_langchain_tool,
41
- json_str_to_api_obj_langchain_tool,
42
- json_str_to_original_inp_dict_langchain_tool,
30
+ from sovara.runner.monkey_patching.api_parsers.claude_code_messages_api_parser import (
31
+ build_to_show_claude_code_messages_output,
32
+ func_kwargs_to_json_str_claude_code_messages,
33
+ api_obj_to_json_str_claude_code_messages,
34
+ json_str_to_api_obj_claude_code_messages,
35
+ json_str_to_original_inp_dict_claude_code_messages,
43
36
  )
44
37
  from sovara.runner.monkey_patching.api_parsers.urllib3_api_parser import (
45
38
  func_kwargs_to_json_str_urllib3,
@@ -235,9 +228,11 @@ def func_kwargs_to_json_str(input_dict: Dict[str, Any], api_type: str) -> str:
235
228
  elif api_type == "MCP.ClientSession.send_request":
236
229
  complete_json_str = func_kwargs_to_json_str_mcp(input_dict)
237
230
  elif api_type == "claude_agent_sdk.parse_message":
238
- complete_json_str = func_kwargs_to_json_str_claude_sdk(input_dict)
231
+ complete_json_str = json.dumps(input_dict)
232
+ elif api_type == "claude_code.messages":
233
+ complete_json_str = func_kwargs_to_json_str_claude_code_messages(input_dict)
239
234
  elif api_type == "langchain_core.tools.BaseTool.run":
240
- complete_json_str = func_kwargs_to_json_str_langchain_tool(input_dict)
235
+ complete_json_str = json.dumps(input_dict)
241
236
  elif api_type == "urllib3.HTTPConnectionPool.urlopen":
242
237
  complete_json_str = func_kwargs_to_json_str_urllib3(input_dict)
243
238
  else:
@@ -247,10 +242,7 @@ def func_kwargs_to_json_str(input_dict: Dict[str, Any], api_type: str) -> str:
247
242
  raw_dict = json.loads(complete_json_str)
248
243
 
249
244
  # Filter the dict to create the display version
250
- if api_type == "claude_agent_sdk.parse_message":
251
- to_show_dict = build_to_show_claude_sdk_input(raw_dict)
252
- else:
253
- to_show_dict = filter_dict(raw_dict)
245
+ to_show_dict = filter_dict(raw_dict)
254
246
 
255
247
  # Construct the wrapped format
256
248
  complete_dict = {"raw": raw_dict, "to_show": to_show_dict}
@@ -294,9 +286,11 @@ def json_str_to_original_inp_dict(json_str: str, input_dict: dict, api_type: str
294
286
  elif api_type == "MCP.ClientSession.send_request":
295
287
  return json_str_to_original_inp_dict_mcp(merged_json_str, input_dict)
296
288
  elif api_type == "claude_agent_sdk.parse_message":
297
- return json_str_to_original_inp_dict_claude_sdk(merged_json_str, input_dict)
289
+ return json.loads(merged_json_str)
290
+ elif api_type == "claude_code.messages":
291
+ return json_str_to_original_inp_dict_claude_code_messages(merged_json_str, input_dict)
298
292
  elif api_type == "langchain_core.tools.BaseTool.run":
299
- return json_str_to_original_inp_dict_langchain_tool(merged_json_str, input_dict)
293
+ return json.loads(merged_json_str)
300
294
  elif api_type == "urllib3.HTTPConnectionPool.urlopen":
301
295
  return json_str_to_original_inp_dict_urllib3(merged_json_str, input_dict)
302
296
  else:
@@ -342,9 +336,11 @@ def api_obj_to_json_str(response_obj: Any, api_type: str) -> str:
342
336
  elif api_type == "MCP.ClientSession.send_request":
343
337
  complete_json_str = api_obj_to_json_str_mcp(response_obj)
344
338
  elif api_type == "claude_agent_sdk.parse_message":
345
- complete_json_str = api_obj_to_json_str_claude_sdk(response_obj)
339
+ complete_json_str = json.dumps(response_obj)
340
+ elif api_type == "claude_code.messages":
341
+ complete_json_str = api_obj_to_json_str_claude_code_messages(response_obj)
346
342
  elif api_type == "langchain_core.tools.BaseTool.run":
347
- complete_json_str = api_obj_to_json_str_langchain_tool(response_obj)
343
+ complete_json_str = json.dumps(response_obj)
348
344
  elif api_type == "urllib3.HTTPConnectionPool.urlopen":
349
345
  complete_json_str = api_obj_to_json_str_urllib3(response_obj)
350
346
  else:
@@ -353,8 +349,8 @@ def api_obj_to_json_str(response_obj: Any, api_type: str) -> str:
353
349
  # Parse the JSON string to get the raw dict
354
350
  raw_dict = json.loads(complete_json_str)
355
351
  # Filter the content dict
356
- if api_type == "claude_agent_sdk.parse_message":
357
- to_show_dict = build_to_show_claude_sdk_output(raw_dict)
352
+ if api_type == "claude_code.messages":
353
+ to_show_dict = build_to_show_claude_code_messages_output(raw_dict)
358
354
  else:
359
355
  to_show_dict = filter_dict(raw_dict)
360
356
  # Create to_show with filtered content
@@ -394,9 +390,11 @@ def json_str_to_api_obj(new_output_text: str, api_type: str) -> Any:
394
390
  elif api_type == "MCP.ClientSession.send_request":
395
391
  return json_str_to_api_obj_mcp(merged_json_str)
396
392
  elif api_type == "claude_agent_sdk.parse_message":
397
- return json_str_to_api_obj_claude_sdk(merged_json_str)
393
+ return json.loads(merged_json_str)
394
+ elif api_type == "claude_code.messages":
395
+ return json_str_to_api_obj_claude_code_messages(merged_json_str)
398
396
  elif api_type == "langchain_core.tools.BaseTool.run":
399
- return json_str_to_api_obj_langchain_tool(merged_json_str)
397
+ return json.loads(merged_json_str)
400
398
  elif api_type == "urllib3.HTTPConnectionPool.urlopen":
401
399
  return json_str_to_api_obj_urllib3(merged_json_str)
402
400
  else:
@@ -0,0 +1,47 @@
1
+ """API parser helpers for ``claude_code.messages`` proxy calls."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import Any
7
+
8
+ def build_to_show_claude_code_messages_input(raw_input: dict[str, Any]) -> dict[str, Any]:
9
+ body = raw_input.get("body")
10
+ if body is None:
11
+ return {}
12
+ return {"body": body}
13
+
14
+
15
+ def build_to_show_claude_code_messages_output(raw_output: dict[str, Any]) -> dict[str, Any]:
16
+ visible_output = {
17
+ key: value
18
+ for key, value in raw_output.items()
19
+ if not str(key).startswith("_")
20
+ }
21
+ from sovara.runner.monkey_patching.api_parser import filter_dict
22
+
23
+ return filter_dict(visible_output)
24
+
25
+
26
+ def func_kwargs_to_json_str_claude_code_messages(input_dict: dict[str, Any]) -> str:
27
+ return json.dumps({
28
+ "url": input_dict.get("url", ""),
29
+ "body": input_dict.get("body"),
30
+ })
31
+
32
+
33
+ def api_obj_to_json_str_claude_code_messages(obj: Any) -> str:
34
+ if not isinstance(obj, dict):
35
+ obj = {"value": obj}
36
+ return json.dumps(obj)
37
+
38
+
39
+ def json_str_to_api_obj_claude_code_messages(json_str: str) -> Any:
40
+ return json.loads(json_str)
41
+
42
+
43
+ def json_str_to_original_inp_dict_claude_code_messages(json_str: str, input_dict: dict) -> dict:
44
+ payload = json.loads(json_str)
45
+ input_dict["url"] = payload.get("url", input_dict.get("url", ""))
46
+ input_dict["body"] = payload.get("body")
47
+ return input_dict
@@ -9,6 +9,16 @@ COMPILED_EDIT_IO_EXCLUDE_PATTERNS = [
9
9
  re.compile(r"^max_tokens$"),
10
10
  re.compile(r"^stream$"),
11
11
  re.compile(r"^temperature$"),
12
+ re.compile(r"^metadata\.user_id$"),
13
+ re.compile(r"^thinking(\.|$)"),
14
+ re.compile(r"^context_management(\.|$)"),
15
+ re.compile(r"^output_config\.(?!effort$).+"),
16
+ re.compile(r"^body\.metadata\.user_id$"),
17
+ re.compile(r"^body\.thinking(\.|$)"),
18
+ re.compile(r"^body\.context_management(\.|$)"),
19
+ re.compile(r"^body\.output_config\.(?!effort$).+"),
20
+ re.compile(r"^body\.stream$"),
21
+ re.compile(r"^body\.messages\.\d+\.content\.\d+\.signature$"),
12
22
  # Bedrock request tuning fields
13
23
  re.compile(r"^body\.inferenceConfig\.(maxTokens|temperature)$"),
14
24
  # content.* fields
@@ -30,7 +40,10 @@ COMPILED_EDIT_IO_EXCLUDE_PATTERNS = [
30
40
  re.compile(r"^content\.max_output_tokens$"),
31
41
  re.compile(r"^content\.max_tool_calls$"),
32
42
  re.compile(r"^content\.createTime$"),
43
+ re.compile(r"^content\.frequency_penalty$"),
44
+ re.compile(r"^content\.incomplete_details(\.|$)"),
33
45
  re.compile(r"^content\.parallel_tool_calls$"),
46
+ re.compile(r"^content\.presence_penalty$"),
34
47
  re.compile(r"^content\.previous_response_id$"),
35
48
  re.compile(r"^content\.prompt_cache"),
36
49
  re.compile(r"^content\.reasoning\.(effort|summary)$"),
@@ -46,6 +59,7 @@ COMPILED_EDIT_IO_EXCLUDE_PATTERNS = [
46
59
  re.compile(r"^content\.responseId$"),
47
60
  re.compile(r"^content\.metrics(\.|$)"),
48
61
  re.compile(r"^content\.stopReason$"),
62
+ re.compile(r"^content\.\d+\.signature$"),
49
63
  # content.content.* fields
50
64
  re.compile(r"^content\.content\.\d+\.(type|id|signature)$"),
51
65
  re.compile(r"^content\.content\.\d+\.content\.\d+\.type$"),
@@ -0,0 +1,181 @@
1
+ """Patch Claude Agent SDK to route model traffic through the Sovara proxy."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import replace
6
+ from functools import wraps
7
+ import os
8
+ import uuid
9
+ from typing import Any
10
+
11
+ from sovara.common.constants import SOVARA_SERVER_URL
12
+ from sovara.common.logger import logger
13
+ from sovara.runner.context_manager import get_run_id
14
+ from sovara.runner.monkey_patching.patching_utils import capture_stack_trace
15
+ from sovara.runner.runtime_client import runtime_client
16
+
17
+ _DEFAULT_ANTHROPIC_BASE_URL = "https://api.anthropic.com"
18
+ _UNSUPPORTED_CUSTOM_TRANSPORT_MESSAGE = (
19
+ "Custom Claude Agent SDK transports are not supported yet under so-record. "
20
+ "Please reach out to support@sovara-labs.com."
21
+ )
22
+ _PROXY_LOOP_MESSAGE = (
23
+ "Claude Agent SDK ANTHROPIC_BASE_URL points at the Sovara Claude proxy itself. "
24
+ "Nested Claude proxy configuration is not supported under so-record."
25
+ )
26
+
27
+
28
+ def _replace_options_env(options: Any, env: dict[str, str]):
29
+ try:
30
+ return replace(options, env=env)
31
+ except Exception:
32
+ options.env = env
33
+ return options
34
+
35
+
36
+ def _merged_transport_env(options_env: dict[str, str] | None) -> dict[str, str]:
37
+ inherited_env = {key: value for key, value in os.environ.items() if key != "CLAUDECODE"}
38
+ return {
39
+ **inherited_env,
40
+ "CLAUDE_CODE_ENTRYPOINT": "sdk-py",
41
+ **(options_env or {}),
42
+ }
43
+
44
+
45
+ def _proxy_base_url(run_id: str, client_id: str) -> str:
46
+ return f"{SOVARA_SERVER_URL}/claude-proxy/runs/{run_id}/clients/{client_id}"
47
+
48
+
49
+ def _is_sovara_proxy_base_url(base_url: str) -> bool:
50
+ return base_url.rstrip("/").startswith(f"{SOVARA_SERVER_URL.rstrip('/')}/claude-proxy/")
51
+
52
+
53
+ def _ensure_supported_transport(transport: Any) -> None:
54
+ if transport is not None and get_run_id():
55
+ raise RuntimeError(_UNSUPPORTED_CUSTOM_TRANSPORT_MESSAGE)
56
+
57
+
58
+ def _normalize_tool_result_content(content: Any) -> Any:
59
+ if content is None:
60
+ return ""
61
+ return content
62
+
63
+
64
+ def claude_sdk_patch():
65
+ """Apply Claude Agent SDK patches when running under ``so-record``."""
66
+ try:
67
+ from claude_agent_sdk._internal import client as internal_client
68
+ from claude_agent_sdk._internal import message_parser
69
+ from claude_agent_sdk._internal.transport.subprocess_cli import SubprocessCLITransport
70
+ from claude_agent_sdk import client as sdk_client
71
+ from claude_agent_sdk.client import ClaudeSDKClient
72
+ except ImportError:
73
+ logger.info("claude_agent_sdk not installed, skipping SDK patches")
74
+ return
75
+
76
+ try:
77
+ from claude_agent_sdk.types import AssistantMessage, ToolResultBlock, ToolUseBlock, UserMessage
78
+ except ImportError:
79
+ logger.info("claude_agent_sdk types unavailable, skipping SDK patches")
80
+ return
81
+
82
+ original_parse = message_parser.parse_message
83
+ original_process_query = internal_client.InternalClient.process_query
84
+ original_client_connect = ClaudeSDKClient.connect
85
+ original_transport_connect = SubprocessCLITransport.connect
86
+
87
+ @wraps(original_parse)
88
+ def patched_parse_message(data: dict) -> Any:
89
+ message = original_parse(data)
90
+ run_id = get_run_id()
91
+ if not run_id:
92
+ return message
93
+
94
+ stack_trace = capture_stack_trace()
95
+ try:
96
+ if isinstance(message, AssistantMessage):
97
+ for block in getattr(message, "content", []) or []:
98
+ if isinstance(block, ToolUseBlock):
99
+ runtime_client.register_claude_sdk_tool_use(
100
+ run_id,
101
+ block.id,
102
+ block.name,
103
+ getattr(block, "input", None),
104
+ stack_trace=stack_trace,
105
+ )
106
+ elif isinstance(message, UserMessage):
107
+ content = getattr(message, "content", None)
108
+ if isinstance(content, list):
109
+ for block in content:
110
+ if isinstance(block, ToolResultBlock):
111
+ runtime_client.finalize_claude_sdk_tool_result(
112
+ run_id,
113
+ block.tool_use_id,
114
+ _normalize_tool_result_content(getattr(block, "content", None)),
115
+ is_error=bool(getattr(block, "is_error", False)),
116
+ )
117
+ except Exception as exc:
118
+ logger.error("Error in claude_sdk_patch parse hook: %s", exc)
119
+ return message
120
+
121
+ @wraps(original_process_query)
122
+ async def patched_process_query(self, prompt, options, transport=None):
123
+ _ensure_supported_transport(transport)
124
+ async for message in original_process_query(self, prompt, options, transport=transport):
125
+ yield message
126
+
127
+ @wraps(original_client_connect)
128
+ async def patched_client_connect(self, prompt=None):
129
+ _ensure_supported_transport(getattr(self, "_custom_transport", None))
130
+ return await original_client_connect(self, prompt)
131
+
132
+ @wraps(original_transport_connect)
133
+ async def patched_transport_connect(self):
134
+ run_id = get_run_id()
135
+ if not run_id:
136
+ return await original_transport_connect(self)
137
+
138
+ original_env = dict(getattr(self._options, "env", {}) or {})
139
+ merged_env = _merged_transport_env(original_env)
140
+ if merged_env.get("SOVARA_CLAUDE_PROXY_ACTIVE") == "1":
141
+ return await original_transport_connect(self)
142
+
143
+ upstream_base_url = merged_env.get("ANTHROPIC_BASE_URL") or _DEFAULT_ANTHROPIC_BASE_URL
144
+ if _is_sovara_proxy_base_url(upstream_base_url):
145
+ raise RuntimeError(_PROXY_LOOP_MESSAGE)
146
+ client_id = getattr(self, "_sovara_claude_proxy_client_id", None)
147
+ if not client_id:
148
+ client_id = uuid.uuid4().hex
149
+ self._sovara_claude_proxy_client_id = client_id
150
+
151
+ runtime_client.register_claude_proxy_client(
152
+ run_id,
153
+ client_id,
154
+ upstream_base_url,
155
+ stack_trace=capture_stack_trace(),
156
+ )
157
+
158
+ proxy_env = {
159
+ **merged_env,
160
+ "ANTHROPIC_BASE_URL": _proxy_base_url(run_id, client_id),
161
+ "SOVARA_CLAUDE_PROXY_ACTIVE": "1",
162
+ }
163
+ self._options = _replace_options_env(self._options, proxy_env)
164
+ try:
165
+ return await original_transport_connect(self)
166
+ finally:
167
+ self._options = _replace_options_env(self._options, original_env)
168
+
169
+ message_parser.parse_message = patched_parse_message
170
+ internal_client.parse_message = patched_parse_message
171
+ sdk_client.parse_message = patched_parse_message
172
+ internal_client.InternalClient.process_query = patched_process_query
173
+ ClaudeSDKClient.connect = patched_client_connect
174
+ SubprocessCLITransport.connect = patched_transport_connect
175
+
176
+ logger.info("claude_agent_sdk patch applied")
177
+
178
+
179
+ def clear_sdk_run_data(run_id: str):
180
+ """Retained for compatibility with older cleanup paths."""
181
+ del run_id
@@ -64,6 +64,12 @@ def _extract_model_from_body(input_dict: Dict[str, Any], api_type: str) -> Optio
64
64
  return "Redacted Reasoning"
65
65
  return input_dict.get("tool_name") or input_dict.get("model")
66
66
 
67
+ elif api_type == "claude_code.messages":
68
+ body = input_dict.get("body")
69
+ if isinstance(body, dict):
70
+ return body.get("model")
71
+ return None
72
+
67
73
  elif api_type == "langchain_core.tools.BaseTool.run":
68
74
  return input_dict.get("tool_name")
69
75
 
@@ -188,6 +194,10 @@ def _extract_request_url_and_path(input_dict: Dict[str, Any], api_type: str) ->
188
194
  return url, parsed.path
189
195
  if api_type == "urllib3.HTTPConnectionPool.urlopen":
190
196
  return input_dict.get("full_url"), input_dict.get("url")
197
+ if api_type == "claude_code.messages":
198
+ url = str(input_dict.get("url") or "")
199
+ parsed = urlparse(url)
200
+ return url, parsed.path
191
201
  except (AttributeError, KeyError, TypeError):
192
202
  return None, None
193
203
 
@@ -209,6 +219,9 @@ def get_node_kind(input_dict: Dict[str, Any], api_type: str) -> str:
209
219
  if api_type == "claude_agent_sdk.parse_message":
210
220
  return "tool" if input_dict.get("tool_name") else "llm"
211
221
 
222
+ if api_type == "claude_code.messages":
223
+ return "llm"
224
+
212
225
  if api_type == "langchain_core.tools.BaseTool.run":
213
226
  return "tool"
214
227
 
@@ -339,6 +352,36 @@ def send_graph_node_and_edges(
339
352
  latency_seconds=None,
340
353
  ):
341
354
  """Send graph node and edge updates to the server."""
355
+ run_id = get_run_id()
356
+ node_msg = build_add_node_message(
357
+ run_id=run_id,
358
+ node_id=node_id,
359
+ input_dict=input_dict,
360
+ output_obj=output_obj,
361
+ source_node_ids=source_node_ids,
362
+ api_type=api_type,
363
+ stack_trace=stack_trace,
364
+ latency_seconds=latency_seconds,
365
+ )
366
+
367
+ try:
368
+ http_post("/runner/add-node", node_msg)
369
+ except Exception as e:
370
+ logger.error(f"Failed to send add_node: {e}")
371
+
372
+
373
+ def build_add_node_message(
374
+ *,
375
+ run_id,
376
+ node_id,
377
+ input_dict,
378
+ output_obj,
379
+ source_node_ids,
380
+ api_type,
381
+ stack_trace,
382
+ latency_seconds=None,
383
+ ):
384
+ """Build the canonical add-node payload for graph updates."""
342
385
  # Import here to avoid circular import
343
386
  from sovara.runner.monkey_patching.api_parser import func_kwargs_to_json_str, api_obj_to_json_str
344
387
  from sovara.runner.node_execution import extract_node_error
@@ -351,7 +394,6 @@ def send_graph_node_and_edges(
351
394
  raw_node_name = get_node_name(input_dict, api_type)
352
395
  label = get_node_label(input_dict, api_type)
353
396
  node_kind = get_node_kind(input_dict, api_type)
354
- run_id = get_run_id()
355
397
  error_payload = extract_node_error(output_obj, api_type)
356
398
  node_status = "error" if error_payload else "success"
357
399
  error_message = error_payload.get("message") if isinstance(error_payload, dict) else None
@@ -378,10 +420,9 @@ def send_graph_node_and_edges(
378
420
  if node_a != node_b and node_b in _graph_reachable_set[run_id][node_a]:
379
421
  if output_contained_in_input(run_id, node_a, node_b):
380
422
  nodes_to_remove.add(node_a)
381
- source_node_ids = [n for n in source_node_ids if n not in nodes_to_remove]
423
+ filtered_source_node_ids = [n for n in source_node_ids if n not in nodes_to_remove]
382
424
 
383
- # Send node
384
- node_msg = {
425
+ return {
385
426
  "type": "add_node",
386
427
  "run_id": run_id,
387
428
  "node": {
@@ -396,10 +437,5 @@ def send_graph_node_and_edges(
396
437
  "node_kind": node_kind,
397
438
  "raw_node_name": raw_node_name,
398
439
  },
399
- "incoming_edges": source_node_ids,
440
+ "incoming_edges": filtered_source_node_ids,
400
441
  }
401
-
402
- try:
403
- http_post("/runner/add-node", node_msg)
404
- except Exception as e:
405
- logger.error(f"Failed to send add_node: {e}")
@@ -68,11 +68,9 @@ def record_prepared_call_exception(
68
68
  if build_exception_output_func is None:
69
69
  build_exception_output_func = build_exception_output
70
70
 
71
- record_exception_func(
72
- prepared_call=prepared_call,
73
- exc=exc,
74
- api_type=api_type,
75
- )
71
+ # Support recorders that use either ``prepared_call`` or ``cache_result``
72
+ # for the first argument name.
73
+ record_exception_func(prepared_call, exc, api_type)
76
74
  prepared_call.output = build_exception_output_func(exc, api_type, prepared_call.input_dict)
77
75
  send_graph_node_and_edges_func(
78
76
  node_id=prepared_call.node_uuid,
@@ -194,6 +192,23 @@ def extract_node_error(output_obj: Any, api_type: str) -> dict[str, Any] | None:
194
192
 
195
193
  payload = _jsonable_payload(output_obj)
196
194
 
195
+ if api_type == "claude_code.messages":
196
+ if isinstance(payload, dict):
197
+ status = payload.get("_status_code")
198
+ if isinstance(status, int) and status >= 400:
199
+ details = {
200
+ key: value
201
+ for key, value in payload.items()
202
+ if not str(key).startswith("_")
203
+ }
204
+ return _http_error_payload(
205
+ api_type=api_type,
206
+ status=status,
207
+ reason=None,
208
+ details=details,
209
+ )
210
+ return None
211
+
197
212
  if api_type == "MCP.ClientSession.send_request":
198
213
  if isinstance(payload, dict) and (payload.get("isError") is True or payload.get("is_error") is True):
199
214
  return _structured_error_payload(
@@ -288,4 +303,12 @@ def build_exception_output(exc: Exception, api_type: str, input_dict: dict[str,
288
303
  request_url=input_dict.get("full_url"),
289
304
  )
290
305
 
306
+ if api_type == "claude_code.messages":
307
+ return {
308
+ **error_payload,
309
+ "_stream": bool(input_dict.get("body", {}).get("stream")),
310
+ "_status_code": 599,
311
+ "_headers": {"content-type": "application/json"},
312
+ }
313
+
291
314
  return error_payload
@@ -63,12 +63,67 @@ class RuntimeClient:
63
63
  def get_run_scope(self, run_id: str) -> dict[str, Any] | None:
64
64
  return http_post("/internal/runner/run-scope", {"run_id": run_id}).get("run")
65
65
 
66
+ def register_claude_proxy_client(
67
+ self,
68
+ run_id: str,
69
+ client_id: str,
70
+ upstream_base_url: str,
71
+ stack_trace: str | None = None,
72
+ ) -> None:
73
+ http_post(
74
+ "/internal/runner/claude-proxy/register-client",
75
+ {
76
+ "run_id": run_id,
77
+ "client_id": client_id,
78
+ "upstream_base_url": upstream_base_url,
79
+ "stack_trace": stack_trace,
80
+ },
81
+ )
82
+
66
83
  def add_prior_applied(self, prior_id: str, run_id: str, node_uuid: str | None = None) -> None:
67
84
  http_post(
68
85
  "/internal/runner/priors/track-applied",
69
86
  {"prior_id": prior_id, "run_id": run_id, "node_uuid": node_uuid},
70
87
  )
71
88
 
89
+ def register_claude_sdk_tool_use(
90
+ self,
91
+ run_id: str,
92
+ tool_use_id: str,
93
+ tool_name: str,
94
+ tool_input: Any,
95
+ *,
96
+ stack_trace: str | None = None,
97
+ ) -> None:
98
+ http_post(
99
+ "/internal/runner/claude-sdk/tool-use",
100
+ {
101
+ "run_id": run_id,
102
+ "tool_use_id": tool_use_id,
103
+ "tool_name": tool_name,
104
+ "tool_input": tool_input,
105
+ "stack_trace": stack_trace,
106
+ },
107
+ )
108
+
109
+ def finalize_claude_sdk_tool_result(
110
+ self,
111
+ run_id: str,
112
+ tool_use_id: str,
113
+ tool_result: Any,
114
+ *,
115
+ is_error: bool = False,
116
+ ) -> None:
117
+ http_post(
118
+ "/internal/runner/claude-sdk/tool-result",
119
+ {
120
+ "run_id": run_id,
121
+ "tool_use_id": tool_use_id,
122
+ "tool_result": tool_result,
123
+ "is_error": is_error,
124
+ },
125
+ )
126
+
72
127
  def upsert_prior_retrieval(
73
128
  self,
74
129
  run_id: str,
@@ -127,6 +182,7 @@ class RuntimeClient:
127
182
  "aiohttp.ClientSession._request",
128
183
  "requests.Session.send",
129
184
  "urllib3.HTTPConnectionPool.urlopen",
185
+ "claude_code.messages",
130
186
  }
131
187
  )
132
188
 
@@ -212,12 +212,14 @@ def create_app() -> FastAPI:
212
212
  from sovara.server.routes.ui import router as ui_router
213
213
  from sovara.server.routes.events import router as events_router
214
214
  from sovara.server.routes.internal import router as internal_router
215
+ from sovara.server.routes.claude_proxy import router as claude_proxy_router
215
216
  from sovara.server.priors_backend.routes import router as priors_router
216
217
 
217
218
  app.include_router(runner_router)
218
219
  app.include_router(ui_router)
219
220
  app.include_router(events_router)
220
221
  app.include_router(internal_router)
222
+ app.include_router(claude_proxy_router)
221
223
  app.include_router(priors_router)
222
224
 
223
225
  @app.get("/health")