calfkit 0.3.2__tar.gz → 0.3.3__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 (273) hide show
  1. calfkit-0.3.3/.release-please-manifest.json +3 -0
  2. {calfkit-0.3.2 → calfkit-0.3.3}/CHANGELOG.md +7 -0
  3. {calfkit-0.3.2 → calfkit-0.3.3}/PKG-INFO +1 -1
  4. calfkit-0.3.3/calfkit/exceptions.py +60 -0
  5. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/models/state.py +85 -20
  6. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/nodes/agent.py +153 -10
  7. calfkit-0.3.3/calfkit/nodes/tool.py +200 -0
  8. {calfkit-0.3.2 → calfkit-0.3.3}/pyproject.toml +1 -1
  9. calfkit-0.3.3/tests/test_tool_errors.py +1492 -0
  10. calfkit-0.3.2/.release-please-manifest.json +0 -3
  11. calfkit-0.3.2/calfkit/exceptions.py +0 -2
  12. calfkit-0.3.2/calfkit/nodes/tool.py +0 -105
  13. {calfkit-0.3.2 → calfkit-0.3.3}/.github/CODEOWNERS +0 -0
  14. {calfkit-0.3.2 → calfkit-0.3.3}/.github/dependabot.yml +0 -0
  15. {calfkit-0.3.2 → calfkit-0.3.3}/.github/workflows/build.yml +0 -0
  16. {calfkit-0.3.2 → calfkit-0.3.3}/.github/workflows/code-checks.yml +0 -0
  17. {calfkit-0.3.2 → calfkit-0.3.3}/.github/workflows/release.yml +0 -0
  18. {calfkit-0.3.2 → calfkit-0.3.3}/.github/workflows/security.yml +0 -0
  19. {calfkit-0.3.2 → calfkit-0.3.3}/.github/workflows/test.yml +0 -0
  20. {calfkit-0.3.2 → calfkit-0.3.3}/.gitignore +0 -0
  21. {calfkit-0.3.2 → calfkit-0.3.3}/LICENSE +0 -0
  22. {calfkit-0.3.2 → calfkit-0.3.3}/Makefile +0 -0
  23. {calfkit-0.3.2 → calfkit-0.3.3}/README.md +0 -0
  24. {calfkit-0.3.2 → calfkit-0.3.3}/ROADMAP.md +0 -0
  25. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/__init__.py +0 -0
  26. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_protocol.py +0 -0
  27. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_types.py +0 -0
  28. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/__init__.py +0 -0
  29. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/LICENSE +0 -0
  30. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/__init__.py +0 -0
  31. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/__main__.py +0 -0
  32. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/_a2a.py +0 -0
  33. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/_agent_graph.py +0 -0
  34. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/_cli/__init__.py +0 -0
  35. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/_cli/web.py +0 -0
  36. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/_function_schema.py +0 -0
  37. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/_griffe.py +0 -0
  38. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/_instrumentation.py +0 -0
  39. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/_json_schema.py +0 -0
  40. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/_mcp.py +0 -0
  41. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/_otel_messages.py +0 -0
  42. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/_output.py +0 -0
  43. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/_parts_manager.py +0 -0
  44. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/_run_context.py +0 -0
  45. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/_system_prompt.py +0 -0
  46. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/_thinking_part.py +0 -0
  47. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/_tool_manager.py +0 -0
  48. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/_utils.py +0 -0
  49. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/ag_ui.py +0 -0
  50. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/agent/__init__.py +0 -0
  51. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/agent/abstract.py +0 -0
  52. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/agent/wrapper.py +0 -0
  53. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/builtin_tools.py +0 -0
  54. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/common_tools/__init__.py +0 -0
  55. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/common_tools/duckduckgo.py +0 -0
  56. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/common_tools/exa.py +0 -0
  57. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/common_tools/tavily.py +0 -0
  58. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/direct.py +0 -0
  59. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/durable_exec/__init__.py +0 -0
  60. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/durable_exec/dbos/__init__.py +0 -0
  61. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/durable_exec/dbos/_agent.py +0 -0
  62. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/durable_exec/dbos/_fastmcp_toolset.py +0 -0
  63. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/durable_exec/dbos/_mcp.py +0 -0
  64. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/durable_exec/dbos/_mcp_server.py +0 -0
  65. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/durable_exec/dbos/_model.py +0 -0
  66. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/durable_exec/dbos/_utils.py +0 -0
  67. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/durable_exec/prefect/__init__.py +0 -0
  68. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/durable_exec/prefect/_agent.py +0 -0
  69. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/durable_exec/prefect/_cache_policies.py +0 -0
  70. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/durable_exec/prefect/_function_toolset.py +0 -0
  71. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/durable_exec/prefect/_mcp_server.py +0 -0
  72. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/durable_exec/prefect/_model.py +0 -0
  73. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/durable_exec/prefect/_toolset.py +0 -0
  74. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/durable_exec/prefect/_types.py +0 -0
  75. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/durable_exec/temporal/__init__.py +0 -0
  76. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/durable_exec/temporal/_agent.py +0 -0
  77. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/durable_exec/temporal/_dynamic_toolset.py +0 -0
  78. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/durable_exec/temporal/_fastmcp_toolset.py +0 -0
  79. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/durable_exec/temporal/_function_toolset.py +0 -0
  80. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/durable_exec/temporal/_logfire.py +0 -0
  81. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/durable_exec/temporal/_mcp.py +0 -0
  82. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/durable_exec/temporal/_mcp_server.py +0 -0
  83. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/durable_exec/temporal/_model.py +0 -0
  84. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/durable_exec/temporal/_run_context.py +0 -0
  85. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/durable_exec/temporal/_toolset.py +0 -0
  86. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/durable_exec/temporal/_workflow.py +0 -0
  87. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/embeddings/__init__.py +0 -0
  88. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/embeddings/base.py +0 -0
  89. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/embeddings/cohere.py +0 -0
  90. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/embeddings/google.py +0 -0
  91. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/embeddings/instrumented.py +0 -0
  92. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/embeddings/openai.py +0 -0
  93. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/embeddings/result.py +0 -0
  94. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/embeddings/sentence_transformers.py +0 -0
  95. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/embeddings/settings.py +0 -0
  96. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/embeddings/test.py +0 -0
  97. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/embeddings/voyageai.py +0 -0
  98. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/embeddings/wrapper.py +0 -0
  99. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/exceptions.py +0 -0
  100. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/ext/__init__.py +0 -0
  101. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/ext/aci.py +0 -0
  102. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/ext/langchain.py +0 -0
  103. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/format_prompt.py +0 -0
  104. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/mcp.py +0 -0
  105. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/messages.py +0 -0
  106. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/models/__init__.py +0 -0
  107. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/models/anthropic.py +0 -0
  108. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/models/bedrock.py +0 -0
  109. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/models/cerebras.py +0 -0
  110. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/models/cohere.py +0 -0
  111. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/models/fallback.py +0 -0
  112. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/models/function.py +0 -0
  113. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/models/gemini.py +0 -0
  114. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/models/google.py +0 -0
  115. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/models/groq.py +0 -0
  116. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/models/huggingface.py +0 -0
  117. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/models/instrumented.py +0 -0
  118. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/models/mcp_sampling.py +0 -0
  119. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/models/mistral.py +0 -0
  120. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/models/openai.py +0 -0
  121. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/models/openrouter.py +0 -0
  122. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/models/outlines.py +0 -0
  123. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/models/test.py +0 -0
  124. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/models/wrapper.py +0 -0
  125. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/models/xai.py +0 -0
  126. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/output.py +0 -0
  127. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/profiles/__init__.py +0 -0
  128. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/profiles/amazon.py +0 -0
  129. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/profiles/anthropic.py +0 -0
  130. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/profiles/cohere.py +0 -0
  131. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/profiles/deepseek.py +0 -0
  132. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/profiles/google.py +0 -0
  133. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/profiles/grok.py +0 -0
  134. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/profiles/groq.py +0 -0
  135. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/profiles/harmony.py +0 -0
  136. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/profiles/meta.py +0 -0
  137. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/profiles/mistral.py +0 -0
  138. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/profiles/moonshotai.py +0 -0
  139. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/profiles/openai.py +0 -0
  140. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/profiles/qwen.py +0 -0
  141. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/profiles/zai.py +0 -0
  142. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/providers/__init__.py +0 -0
  143. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/providers/alibaba.py +0 -0
  144. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/providers/anthropic.py +0 -0
  145. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/providers/azure.py +0 -0
  146. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/providers/bedrock.py +0 -0
  147. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/providers/cerebras.py +0 -0
  148. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/providers/cohere.py +0 -0
  149. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/providers/deepseek.py +0 -0
  150. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/providers/fireworks.py +0 -0
  151. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/providers/gateway.py +0 -0
  152. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/providers/github.py +0 -0
  153. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/providers/google.py +0 -0
  154. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/providers/google_gla.py +0 -0
  155. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/providers/google_vertex.py +0 -0
  156. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/providers/grok.py +0 -0
  157. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/providers/groq.py +0 -0
  158. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/providers/heroku.py +0 -0
  159. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/providers/huggingface.py +0 -0
  160. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/providers/litellm.py +0 -0
  161. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/providers/mistral.py +0 -0
  162. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/providers/moonshotai.py +0 -0
  163. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/providers/nebius.py +0 -0
  164. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/providers/ollama.py +0 -0
  165. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/providers/openai.py +0 -0
  166. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/providers/openrouter.py +0 -0
  167. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/providers/outlines.py +0 -0
  168. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/providers/ovhcloud.py +0 -0
  169. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/providers/sambanova.py +0 -0
  170. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/providers/sentence_transformers.py +0 -0
  171. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/providers/together.py +0 -0
  172. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/providers/vercel.py +0 -0
  173. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/providers/voyageai.py +0 -0
  174. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/providers/xai.py +0 -0
  175. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/py.typed +0 -0
  176. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/result.py +0 -0
  177. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/retries.py +0 -0
  178. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/run.py +0 -0
  179. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/settings.py +0 -0
  180. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/tools.py +0 -0
  181. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/toolsets/__init__.py +0 -0
  182. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/toolsets/_dynamic.py +0 -0
  183. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/toolsets/abstract.py +0 -0
  184. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/toolsets/approval_required.py +0 -0
  185. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/toolsets/combined.py +0 -0
  186. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/toolsets/external.py +0 -0
  187. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/toolsets/fastmcp.py +0 -0
  188. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/toolsets/filtered.py +0 -0
  189. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/toolsets/function.py +0 -0
  190. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/toolsets/prefixed.py +0 -0
  191. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/toolsets/prepared.py +0 -0
  192. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/toolsets/renamed.py +0 -0
  193. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/toolsets/wrapper.py +0 -0
  194. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/ui/__init__.py +0 -0
  195. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/ui/_adapter.py +0 -0
  196. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/ui/_event_stream.py +0 -0
  197. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/ui/_messages_builder.py +0 -0
  198. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/ui/_web/__init__.py +0 -0
  199. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/ui/_web/api.py +0 -0
  200. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/ui/_web/app.py +0 -0
  201. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/ui/ag_ui/__init__.py +0 -0
  202. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/ui/ag_ui/_adapter.py +0 -0
  203. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/ui/ag_ui/_event_stream.py +0 -0
  204. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/ui/ag_ui/app.py +0 -0
  205. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/ui/vercel_ai/__init__.py +0 -0
  206. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/ui/vercel_ai/_adapter.py +0 -0
  207. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/ui/vercel_ai/_event_stream.py +0 -0
  208. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/ui/vercel_ai/_models.py +0 -0
  209. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/ui/vercel_ai/_utils.py +0 -0
  210. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/ui/vercel_ai/request_types.py +0 -0
  211. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/ui/vercel_ai/response_types.py +0 -0
  212. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/pydantic_ai/usage.py +0 -0
  213. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/_vendor/vendor.txt +0 -0
  214. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/client/__init__.py +0 -0
  215. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/client/base.py +0 -0
  216. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/client/client.py +0 -0
  217. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/client/deserialize.py +0 -0
  218. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/client/invocation_handle.py +0 -0
  219. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/client/middleware.py +0 -0
  220. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/client/node_result.py +0 -0
  221. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/client/reply_dispatcher.py +0 -0
  222. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/models/__init__.py +0 -0
  223. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/models/actions.py +0 -0
  224. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/models/envelope.py +0 -0
  225. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/models/node_schema.py +0 -0
  226. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/models/payload.py +0 -0
  227. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/models/session_context.py +0 -0
  228. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/models/tool_context.py +0 -0
  229. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/nodes/__init__.py +0 -0
  230. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/nodes/base.py +0 -0
  231. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/nodes/consumer.py +0 -0
  232. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/nodes/node.py +0 -0
  233. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/providers/__init__.py +0 -0
  234. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/providers/pydantic_ai/__init__.py +0 -0
  235. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/providers/pydantic_ai/anthropic.py +0 -0
  236. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/providers/pydantic_ai/model_client.py +0 -0
  237. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/providers/pydantic_ai/openai.py +0 -0
  238. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/worker/__init__.py +0 -0
  239. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/worker/worker.py +0 -0
  240. {calfkit-0.3.2 → calfkit-0.3.3}/calfkit/worker/worker_config.py +0 -0
  241. {calfkit-0.3.2 → calfkit-0.3.3}/codecov.yml +0 -0
  242. {calfkit-0.3.2 → calfkit-0.3.3}/docs/calfkit-v1-design.md +0 -0
  243. {calfkit-0.3.2 → calfkit-0.3.3}/docs/calfkit-v1-dx-review.md +0 -0
  244. {calfkit-0.3.2 → calfkit-0.3.3}/docs/hooks-design.md +0 -0
  245. {calfkit-0.3.2 → calfkit-0.3.3}/examples/__init__.py +0 -0
  246. {calfkit-0.3.2 → calfkit-0.3.3}/examples/deprecated/agent_dispatcher.py +0 -0
  247. {calfkit-0.3.2 → calfkit-0.3.3}/examples/deprecated/chat_node.py +0 -0
  248. {calfkit-0.3.2 → calfkit-0.3.3}/examples/deprecated/chat_repl_cli.py +0 -0
  249. {calfkit-0.3.2 → calfkit-0.3.3}/examples/deprecated/invoke_agent.py +0 -0
  250. {calfkit-0.3.2 → calfkit-0.3.3}/examples/deprecated/router_node.py +0 -0
  251. {calfkit-0.3.2 → calfkit-0.3.3}/examples/deprecated/tool_nodes.py +0 -0
  252. {calfkit-0.3.2 → calfkit-0.3.3}/examples/quickstart/agent_service.py +0 -0
  253. {calfkit-0.3.2 → calfkit-0.3.3}/examples/quickstart/invoke.py +0 -0
  254. {calfkit-0.3.2 → calfkit-0.3.3}/examples/quickstart/weather_sink.py +0 -0
  255. {calfkit-0.3.2 → calfkit-0.3.3}/examples/quickstart/weather_tool.py +0 -0
  256. {calfkit-0.3.2 → calfkit-0.3.3}/examples/rpc_worker.py +0 -0
  257. {calfkit-0.3.2 → calfkit-0.3.3}/release-please-config.json +0 -0
  258. {calfkit-0.3.2 → calfkit-0.3.3}/tests/__init__.py +0 -0
  259. {calfkit-0.3.2 → calfkit-0.3.3}/tests/conftest.py +0 -0
  260. {calfkit-0.3.2 → calfkit-0.3.3}/tests/integration/__init__.py +0 -0
  261. {calfkit-0.3.2 → calfkit-0.3.3}/tests/integration/test_agent_output_types.py +0 -0
  262. {calfkit-0.3.2 → calfkit-0.3.3}/tests/integration/test_agent_workers.py +0 -0
  263. {calfkit-0.3.2 → calfkit-0.3.3}/tests/providers.py +0 -0
  264. {calfkit-0.3.2 → calfkit-0.3.3}/tests/test_co_tenant_tool_isolation.py +0 -0
  265. {calfkit-0.3.2 → calfkit-0.3.3}/tests/test_concurrent_tool_calls.py +0 -0
  266. {calfkit-0.3.2 → calfkit-0.3.3}/tests/test_consumer.py +0 -0
  267. {calfkit-0.3.2 → calfkit-0.3.3}/tests/test_gates.py +0 -0
  268. {calfkit-0.3.2 → calfkit-0.3.3}/tests/test_headers.py +0 -0
  269. {calfkit-0.3.2 → calfkit-0.3.3}/tests/test_instructions.py +0 -0
  270. {calfkit-0.3.2 → calfkit-0.3.3}/tests/test_model_settings.py +0 -0
  271. {calfkit-0.3.2 → calfkit-0.3.3}/tests/test_overrides.py +0 -0
  272. {calfkit-0.3.2 → calfkit-0.3.3}/tests/test_serializable.py +0 -0
  273. {calfkit-0.3.2 → calfkit-0.3.3}/tests/utils.py +0 -0
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "0.3.3"
3
+ }
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.3](https://github.com/calf-ai/calfkit-sdk/compare/v0.3.2...v0.3.3) (2026-05-22)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * (agent,tool) surface tool exceptions to the agent instead of hanging ([#146](https://github.com/calf-ai/calfkit-sdk/issues/146)) ([05162cc](https://github.com/calf-ai/calfkit-sdk/commit/05162cc2512285e7dfcb64a30c7c8bca2f9fe50d))
9
+
3
10
  ## [0.3.2](https://github.com/calf-ai/calfkit-sdk/compare/v0.3.1...v0.3.2) (2026-05-21)
4
11
 
5
12
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: calfkit
3
- Version: 0.3.2
3
+ Version: 0.3.3
4
4
  Summary: Build AI workflows and agents as fully-distributed and event-driven microservices.
5
5
  Project-URL: Homepage, https://github.com/calf-ai/calf-sdk
6
6
  Project-URL: Repository, https://github.com/calf-ai/calf-sdk
@@ -0,0 +1,60 @@
1
+ from typing import Any
2
+
3
+
4
+ class DeserializationError(Exception):
5
+ """Raised when client-side output deserialization fails."""
6
+
7
+
8
+ class ToolExecutionError(Exception):
9
+ """The original traceback is not preserved across the Kafka boundary; it is
10
+ logged at the worker that ran the tool. ``exc_type`` and ``exc_message`` are
11
+ the only forensic data available at the agent.
12
+ """
13
+
14
+ def __init__(self, *, tool_name: str, tool_call_id: str, exc_type: str, exc_message: str):
15
+ self.tool_name = tool_name
16
+ self.tool_call_id = tool_call_id
17
+ self.exc_type = exc_type
18
+ self.exc_message = exc_message
19
+ super().__init__(f"Tool {tool_name!r} (call_id={tool_call_id}) raised {exc_type}: {exc_message}")
20
+
21
+ def __reduce__(self) -> tuple[Any, ...]:
22
+ # Bypass keyword-only ``__init__`` during reconstruction by routing
23
+ # through a module-level helper that uses ``__new__``. A naive
24
+ # ``(self.__class__, (), state)`` reduction would call ``__init__()``
25
+ # with no args and fail because the keyword arguments are required.
26
+ return (
27
+ _reconstruct_tool_execution_error,
28
+ (),
29
+ {
30
+ "tool_name": self.tool_name,
31
+ "tool_call_id": self.tool_call_id,
32
+ "exc_type": self.exc_type,
33
+ "exc_message": self.exc_message,
34
+ },
35
+ )
36
+
37
+ def __setstate__(self, state: dict[str, Any] | None) -> None:
38
+ # ``__reduce__`` above always provides a dict, but the supertype
39
+ # signature allows ``None`` (Liskov); treat ``None`` as a no-op so
40
+ # subclasses constructed without state don't crash.
41
+ if state is None:
42
+ return
43
+ self.tool_name = state["tool_name"]
44
+ self.tool_call_id = state["tool_call_id"]
45
+ self.exc_type = state["exc_type"]
46
+ self.exc_message = state["exc_message"]
47
+ Exception.__init__(
48
+ self,
49
+ f"Tool {self.tool_name!r} (call_id={self.tool_call_id}) raised {self.exc_type}: {self.exc_message}",
50
+ )
51
+
52
+
53
+ def _reconstruct_tool_execution_error() -> "ToolExecutionError":
54
+ """Create a bare ``ToolExecutionError`` for reconstruction via ``__setstate__``.
55
+
56
+ Bypasses the keyword-only ``__init__`` so state restoration can populate
57
+ fields directly. Must be module-level (not a lambda or local closure) so
58
+ it can be located by fully-qualified name during deserialization.
59
+ """
60
+ return ToolExecutionError.__new__(ToolExecutionError)
@@ -1,16 +1,18 @@
1
1
  import logging
2
2
  from dataclasses import dataclass, field
3
- from typing import Annotated, Any, Generic
3
+ from typing import Annotated, Any, ClassVar, Generic, Literal
4
4
 
5
- from pydantic import BaseModel, ConfigDict, Discriminator, Field
5
+ from pydantic import BaseModel, ConfigDict, Discriminator, Field, Tag, field_validator
6
6
  from typing_extensions import TypeVar
7
7
 
8
+ from calfkit._vendor.pydantic_ai.exceptions import ModelRetry
8
9
  from calfkit._vendor.pydantic_ai.messages import (
9
10
  ModelMessage,
10
11
  ModelRequest,
12
+ RetryPromptPart,
11
13
  ToolCallPart,
14
+ ToolReturn,
12
15
  )
13
- from calfkit._vendor.pydantic_ai.tools import DeferredToolCallResult as ToolCallResult
14
16
  from calfkit.models.node_schema import BaseToolNodeSchema
15
17
  from calfkit.models.payload import ContentPart
16
18
 
@@ -58,28 +60,102 @@ class CoreMessageState(BaseAgentActivityState):
58
60
  self.uncommitted_message = None
59
61
 
60
62
 
63
+ class FailedToolCall(BaseModel):
64
+ """Wire-format representation of a tool that raised on the worker side.
65
+
66
+ Stored in ``State.tool_results`` in the same slot a successful ``ToolReturn``
67
+ would occupy; consumers must handle both shapes. The ``marker_kind`` literal
68
+ is the discriminator tag consulted by ``_calf_tool_result_discriminator`` so
69
+ this value reconstructs as a typed instance (rather than a plain ``dict``)
70
+ when ``State`` round-trips through JSON across the Kafka boundary.
71
+
72
+ String fields are clamped to bounded lengths (see ``_MAX_LENGTHS``) rather
73
+ than rejected for wire-size safety: a tool that raises with an enormous
74
+ ``str(exc)`` must not itself cause construction to raise inside the worker's
75
+ ``except Exception`` block (which would prevent the failure reply from ever
76
+ being published and hang the agent).
77
+ """
78
+
79
+ model_config = ConfigDict(extra="ignore", frozen=True)
80
+ tool_name: str
81
+ tool_call_id: str = Field(min_length=1)
82
+ exc_type: str
83
+ exc_message: str
84
+ marker_kind: Literal["calfkit-tool-error"] = "calfkit-tool-error"
85
+
86
+ _MAX_LENGTHS: ClassVar[dict[str, int]] = {
87
+ "tool_name": 256,
88
+ "tool_call_id": 128,
89
+ "exc_type": 256,
90
+ "exc_message": 4096,
91
+ }
92
+
93
+ @field_validator("tool_name", "tool_call_id", "exc_type", "exc_message", mode="before")
94
+ @classmethod
95
+ def _clamp_string_fields(cls, v: Any, info: Any) -> Any:
96
+ if isinstance(v, str):
97
+ limit = cls._MAX_LENGTHS.get(info.field_name)
98
+ if limit is not None and len(v) > limit:
99
+ return v[:limit]
100
+ return v
101
+
102
+
103
+ def _calf_tool_result_discriminator(x: Any) -> str | None:
104
+ """Tag extractor for the ``CalfToolResult`` discriminated union.
105
+
106
+ Mirrors ``calfkit._vendor.pydantic_ai.tools._deferred_tool_call_result_discriminator``
107
+ with an added ``marker_kind`` branch for ``FailedToolCall``. Without this, a
108
+ serialized marker arrives as a plain ``dict`` after a Kafka hop because the
109
+ upstream discriminator only recognizes ``kind`` / ``part_kind``.
110
+
111
+ Returns only string tags; non-string values are treated as missing so the
112
+ union falls through to the ``Any`` arm rather than silently misrouting.
113
+ Keep this in sync with the upstream helper if a new pydantic-ai tag is added.
114
+ """
115
+ for key in ("marker_kind", "kind", "part_kind"):
116
+ v = x.get(key) if isinstance(x, dict) else getattr(x, key, None)
117
+ if isinstance(v, str):
118
+ return v
119
+ return None
120
+
121
+
122
+ CalfToolResult = Annotated[
123
+ Annotated[ToolReturn, Tag("tool-return")]
124
+ | Annotated[ModelRetry, Tag("model-retry")]
125
+ | Annotated[RetryPromptPart, Tag("retry-prompt")]
126
+ | Annotated[FailedToolCall, Tag("calfkit-tool-error")],
127
+ Discriminator(_calf_tool_result_discriminator),
128
+ ]
129
+ """Calfkit-side tagged union for values stored in ``State.tool_results``.
130
+
131
+ Flattens pydantic-ai's ``DeferredToolCallResult`` (``ToolReturn`` / ``ModelRetry``
132
+ / ``RetryPromptPart``) and adds calfkit's ``FailedToolCall``. Flattening (rather
133
+ than ``DeferredToolCallResult | FailedToolCall``) is required because Pydantic's
134
+ discriminated-union machinery does not compose nested ``Discriminator``
135
+ annotations reliably.
136
+ """
137
+
138
+
61
139
  class InFlightToolsState(BaseAgentActivityState):
62
140
  """State of all in-progress tool calls and results.
63
141
  Tool calls and results are in-progress when all tool calls have not been completed and committed to message history.""" # noqa: E501
64
142
 
65
143
  model_config = ConfigDict(extra="ignore")
66
144
 
67
- # Map of tool call IDS to tool calls
68
145
  tool_calls: dict[str, ToolCallPart] = Field(default_factory=dict)
69
146
 
70
- # Map of tool call IDs to tool results
71
- tool_results: dict[str, ToolCallResult | Any] = Field(default_factory=dict)
147
+ tool_results: dict[str, CalfToolResult | Any] = Field(default_factory=dict)
72
148
 
73
149
  def add_tool_call(self, tool_call: ToolCallPart) -> None:
74
150
  self.tool_calls[tool_call.tool_call_id] = tool_call
75
151
 
76
- def add_tool_result(self, tool_call_id: str, tool_result: ToolCallResult | Any) -> None:
152
+ def add_tool_result(self, tool_call_id: str, tool_result: CalfToolResult | Any) -> None:
77
153
  self.tool_results[tool_call_id] = tool_result
78
154
 
79
155
  def get_tool_call(self, tool_call_id: str) -> ToolCallPart | None:
80
156
  return self.tool_calls.get(tool_call_id)
81
157
 
82
- def get_tool_result(self, tool_call_id: str) -> ToolCallResult | Any | None:
158
+ def get_tool_result(self, tool_call_id: str) -> CalfToolResult | Any | None:
83
159
  return self.tool_results.get(tool_call_id)
84
160
 
85
161
  def all_call_ids_complete(self, *call_ids: str) -> bool:
@@ -102,17 +178,6 @@ class State(CoreMessageState, InFlightToolsState):
102
178
  overrides: OverridesState | None = None
103
179
 
104
180
 
105
- # class State(BaseModel):
106
- # """mutable data model to track state of an execution among one or more agents,
107
- # sharing correlation id"""
108
-
109
- # model_config = ConfigDict(extra="ignore")
110
- # run_state: AgentActivityState = Field(default_factory=AgentActivityState)
111
-
112
-
113
- # ---------------------------------------------------------------------------
114
- # Experimental PartialState implementation
115
- # ---------------------------------------------------------------------------
116
181
  AgentStateSubsetT = TypeVar("AgentStateSubsetT", CoreMessageState, InFlightToolsState)
117
182
 
118
183
  PartialState = Annotated[CoreMessageState | InFlightToolsState | Any, Discriminator("kind")]
@@ -135,7 +200,7 @@ class PendingToolBatch:
135
200
  base_state: State
136
201
 
137
202
  # map of tool call IDs to tool call results
138
- collected_results: dict[str, ToolCallResult | Any] = field(default_factory=dict)
203
+ collected_results: dict[str, CalfToolResult | Any] = field(default_factory=dict)
139
204
 
140
205
  @property
141
206
  def is_complete(self) -> bool:
@@ -2,6 +2,8 @@ import logging
2
2
  from collections.abc import Callable
3
3
  from typing import Any, ClassVar, Generic, cast
4
4
 
5
+ from pydantic import ValidationError
6
+
5
7
  from calfkit._protocol import NodeKind
6
8
  from calfkit._types import AgentOutputT
7
9
  from calfkit._vendor.pydantic_ai import Agent as InternalAgentLoop
@@ -11,19 +13,18 @@ from calfkit._vendor.pydantic_ai.output import OutputSpec
11
13
  from calfkit._vendor.pydantic_ai.settings import ModelSettings
12
14
  from calfkit._vendor.pydantic_ai.tools import DeferredToolResults
13
15
  from calfkit._vendor.pydantic_ai.toolsets.external import ExternalToolset
16
+ from calfkit.exceptions import ToolExecutionError
14
17
  from calfkit.models import Call, DataPart, NodeResult, ReturnCall, State, TailCall, TextPart
15
18
  from calfkit.models.actions import Silent
16
19
  from calfkit.models.node_schema import BaseToolNodeSchema
17
20
  from calfkit.models.session_context import SessionRunContext
18
- from calfkit.models.state import PendingToolBatch
21
+ from calfkit.models.state import FailedToolCall, PendingToolBatch
19
22
  from calfkit.nodes.base import BaseNodeDef, GateFunction
20
- from calfkit.nodes.tool import ToolNodeDef
23
+ from calfkit.nodes.tool import BaseToolNodeDef, ToolNodeDef, _safe_exc_message
21
24
  from calfkit.providers.pydantic_ai.model_client import PydanticModelClient
22
25
 
23
26
  logger = logging.getLogger(__name__)
24
27
 
25
- NoneType = type(None)
26
-
27
28
 
28
29
  class BaseAgentNodeDef(
29
30
  Generic[AgentOutputT],
@@ -85,11 +86,17 @@ class BaseAgentNodeDef(
85
86
  elif self.tools:
86
87
  tools_registry = {tool.tool_schema.name: tool for tool in self.tools}
87
88
 
89
+ # ``latest_tool_calls()`` walks ``message_history`` in reverse on each call;
90
+ # cache once for all pre-model uses. The post-model use after
91
+ # ``result.new_messages()`` is extended into history must re-call to see
92
+ # the model's new tool calls.
93
+ latest_tool_calls = ctx.state.latest_tool_calls()
94
+
88
95
  logger.debug(
89
96
  "[%s] agent run entered node=%s pending_tool_calls=%d history_len=%d",
90
97
  ctx.deps.correlation_id[:8],
91
98
  self.name,
92
- len(ctx.state.latest_tool_calls()),
99
+ len(latest_tool_calls),
93
100
  len(ctx.state.message_history),
94
101
  )
95
102
 
@@ -99,7 +106,61 @@ class BaseAgentNodeDef(
99
106
  if batch and not batch.is_complete:
100
107
  return Silent()
101
108
 
102
- latest_tool_calls = ctx.state.latest_tool_calls()
109
+ # Collect all FailedToolCall results in this turn so operators see every
110
+ # failure in a parallel batch; raise on the first after logging all.
111
+ # Also defend against corrupt marker dicts: a payload with the calfkit
112
+ # marker tag that fails ``FailedToolCall`` validation (e.g. schema drift
113
+ # during rolling deploy, a required field added without default, a
114
+ # tampered wire payload) round-trips through the union's ``| Any`` arm
115
+ # as a plain ``dict``. Synthesize a typed marker so the silent-failure
116
+ # path closes — operators still see the raise and the corrupt-keys
117
+ # context.
118
+ failed_tool_calls: list[FailedToolCall] = []
119
+ for tc in latest_tool_calls:
120
+ result = ctx.state.tool_results.get(tc.tool_call_id)
121
+ if isinstance(result, FailedToolCall):
122
+ failed_tool_calls.append(result)
123
+ elif isinstance(result, dict) and result.get("marker_kind") == "calfkit-tool-error":
124
+ logger.error(
125
+ "[%s] corrupt FailedToolCall marker detected node=%s tool_call_id=%s raw_keys=%s; "
126
+ "likely schema drift or version skew across the Kafka boundary",
127
+ ctx.deps.correlation_id[:8],
128
+ self.name,
129
+ tc.tool_call_id,
130
+ sorted(result.keys()),
131
+ )
132
+ # Synthesize a typed marker with sentinel fields known to pass
133
+ # FailedToolCall's validators. tc.tool_call_id is the LLM-emitted
134
+ # correlation key; fall back to "<missing>" if it's empty so
135
+ # ``min_length=1`` doesn't itself raise.
136
+ raw_tool_name = result.get("tool_name")
137
+ failed_tool_calls.append(
138
+ FailedToolCall(
139
+ tool_name=raw_tool_name if isinstance(raw_tool_name, str) and raw_tool_name else "<unknown>",
140
+ tool_call_id=tc.tool_call_id or "<missing>",
141
+ exc_type="CorruptFailedToolCallMarker",
142
+ exc_message=f"Marker dict failed validation as FailedToolCall (likely schema drift); raw_keys={sorted(result.keys())}",
143
+ )
144
+ )
145
+ if failed_tool_calls:
146
+ for failure in failed_tool_calls:
147
+ logger.error(
148
+ "[%s] tool execution error detected node=%s tool=%s tool_call_id=%s exc_type=%s exc_message=%s",
149
+ ctx.deps.correlation_id[:8],
150
+ self.name,
151
+ failure.tool_name,
152
+ failure.tool_call_id,
153
+ failure.exc_type,
154
+ failure.exc_message,
155
+ )
156
+ first = failed_tool_calls[0]
157
+ raise ToolExecutionError(
158
+ tool_name=first.tool_name,
159
+ tool_call_id=first.tool_call_id,
160
+ exc_type=first.exc_type,
161
+ exc_message=first.exc_message,
162
+ )
163
+
103
164
  tool_results = None
104
165
 
105
166
  if len(latest_tool_calls) > 0:
@@ -141,14 +202,13 @@ class BaseAgentNodeDef(
141
202
  model_settings=run_model_settings,
142
203
  )
143
204
  if isinstance(result.output, DeferredToolRequests):
144
- # The LLM called one or more tools
145
205
  logger.debug(
146
206
  "[%s] model returned DeferredToolRequests tool_count=%d node=%s",
147
207
  ctx.deps.correlation_id[:8],
148
208
  len(result.output.calls),
149
209
  self.name,
150
210
  )
151
- messages = result.new_messages() # preserve conversation history
211
+ messages = result.new_messages()
152
212
  ctx.state.message_history.extend(messages)
153
213
  latest_tool_calls = ctx.state.latest_tool_calls()
154
214
 
@@ -166,7 +226,9 @@ class BaseAgentNodeDef(
166
226
  tool_call_id=tool_call.tool_call_id,
167
227
  ),
168
228
  )
169
- elif tool_node.subscribe_topics is None:
229
+ continue
230
+
231
+ if tool_node.subscribe_topics is None:
170
232
  logger.error(
171
233
  "tool=%s is unreachable. No subscribe topics were provided for the tool node.",
172
234
  tool_call.tool_name,
@@ -179,8 +241,89 @@ class BaseAgentNodeDef(
179
241
  tool_call_id=tool_call.tool_call_id,
180
242
  ),
181
243
  )
244
+ continue
245
+
246
+ # Parse args from the LLM's emission. Applies to ALL dispatch
247
+ # paths so that malformed-JSON args from override (schema-only)
248
+ # tools are also surfaced as RetryPromptPart instead of escaping
249
+ # to the worker's hard FailedToolCall path.
250
+ #
251
+ # ``args_as_dict()`` can raise more than just ValueError /
252
+ # AssertionError: ``pydantic_core.from_json`` raises TypeError
253
+ # when ``args`` is a non-string/non-bytes value (e.g. an int or
254
+ # list emitted by an off-spec provider). Catch broadly so any
255
+ # parse failure surfaces as an LLM-retryable RetryPromptPart
256
+ # rather than escaping ``run()`` and hanging the caller.
257
+ try:
258
+ args = tool_call.args_as_dict()
259
+ except Exception as e:
260
+ content = f"Malformed tool arguments: {type(e).__name__}: {_safe_exc_message(e)}"
261
+ logger.warning(
262
+ "[%s] tool=%s args parse failed at dispatch: %s",
263
+ ctx.deps.correlation_id[:8],
264
+ tool_call.tool_name,
265
+ content,
266
+ exc_info=True,
267
+ )
268
+ ctx.state.add_tool_result(
269
+ tool_call.tool_call_id,
270
+ RetryPromptPart(
271
+ content=content,
272
+ tool_name=tool_call.tool_name,
273
+ tool_call_id=tool_call.tool_call_id,
274
+ ),
275
+ )
276
+ continue
277
+
278
+ # Validate against the schema if we have a runtime validator.
279
+ # Override-mode schemas (BaseToolNodeSchema-only) skip this step
280
+ # and dispatch unvalidated; this is the documented carve-out.
281
+ if isinstance(tool_node, BaseToolNodeDef):
282
+ try:
283
+ tool_node.validate_call_args(args)
284
+ except ValidationError as e:
285
+ validation_errors = e.errors(include_url=False, include_context=False)
286
+ logger.warning(
287
+ "[%s] tool=%s arg validation failed at dispatch: %s",
288
+ ctx.deps.correlation_id[:8],
289
+ tool_call.tool_name,
290
+ validation_errors,
291
+ )
292
+ ctx.state.add_tool_result(
293
+ tool_call.tool_call_id,
294
+ RetryPromptPart(
295
+ content=validation_errors,
296
+ tool_name=tool_call.tool_name,
297
+ tool_call_id=tool_call.tool_call_id,
298
+ ),
299
+ )
300
+ continue
301
+ except Exception as e:
302
+ # A user-authored Pydantic ``field_validator`` raised
303
+ # something other than ``ValidationError`` (e.g.
304
+ # ``RuntimeError``, ``TypeError``, a custom exception).
305
+ # Surface as ``RetryPromptPart`` so the LLM can retry
306
+ # rather than letting the exception escape ``run()`` and
307
+ # silently hang the caller.
308
+ validator_content = f"Tool argument validator raised {type(e).__name__}: {_safe_exc_message(e)}"
309
+ logger.warning(
310
+ "[%s] tool=%s arg validator raised %s; surfacing as RetryPromptPart",
311
+ ctx.deps.correlation_id[:8],
312
+ tool_call.tool_name,
313
+ type(e).__name__,
314
+ exc_info=True,
315
+ )
316
+ ctx.state.add_tool_result(
317
+ tool_call.tool_call_id,
318
+ RetryPromptPart(
319
+ content=validator_content,
320
+ tool_name=tool_call.tool_name,
321
+ tool_call_id=tool_call.tool_call_id,
322
+ ),
323
+ )
324
+ continue
182
325
 
183
- if ctx.state.all_call_ids_complete(*[tc.tool_call_id for tc in latest_tool_calls]): # All tool calls were invalid, we need to retry.
326
+ if ctx.state.all_call_ids_complete(*[tc.tool_call_id for tc in latest_tool_calls]):
184
327
  # TODO: maybe consider a node retry return type that doesn't require round trip to itself.
185
328
  # Tailcall to itself is a roundtrip.
186
329
  logger.debug("[%s] all tool calls invalid, TailCall retry node=%s", ctx.deps.correlation_id[:8], self.name)
@@ -0,0 +1,200 @@
1
+ import logging
2
+ from collections.abc import Awaitable, Callable
3
+ from dataclasses import dataclass, field
4
+ from typing import Any, ClassVar
5
+
6
+ import pydantic_core
7
+ from typing_extensions import Self
8
+
9
+ from calfkit._protocol import NodeKind
10
+ from calfkit._vendor.pydantic_ai import Tool
11
+ from calfkit._vendor.pydantic_ai.exceptions import ModelRetry
12
+ from calfkit._vendor.pydantic_ai.messages import RetryPromptPart, ToolReturn
13
+ from calfkit.models import SessionRunContext, Silent, State, ToolContext
14
+ from calfkit.models.actions import NodeResult, ReturnCall
15
+ from calfkit.models.node_schema import BaseToolNodeSchema
16
+ from calfkit.models.state import FailedToolCall
17
+ from calfkit.nodes.base import BaseNodeDef, GateFunction
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ def _safe_exc_message(e: BaseException) -> str:
23
+ """Best-effort string of an exception, robust against broken ``__str__``.
24
+
25
+ A bare ``str(e)`` can itself raise (if the exception's ``__str__`` is
26
+ broken or its args don't coerce). Inside the worker's ``except Exception``
27
+ block that would propagate out, prevent the FailedToolCall from being
28
+ constructed, and re-introduce the silent-hang failure mode this module
29
+ exists to prevent. Mirrors stdlib ``traceback._some_str`` with a ``repr``
30
+ fallback.
31
+ """
32
+ try:
33
+ return str(e)
34
+ except Exception:
35
+ try:
36
+ return repr(e)
37
+ except Exception:
38
+ return f"<unprintable {type(e).__name__}>"
39
+
40
+
41
+ @dataclass
42
+ class BaseToolNodeDef(BaseToolNodeSchema, BaseNodeDef):
43
+ _node_kind: ClassVar[NodeKind] = "tool"
44
+ _tool: Tool
45
+ gates: list[GateFunction] = field(default_factory=list)
46
+
47
+ def validate_call_args(self, args_dict: dict[str, Any]) -> Any:
48
+ """Validate ``args_dict`` against this tool's argument schema.
49
+
50
+ Raises ``pydantic.ValidationError`` on mismatch. Used by the agent to
51
+ fail-fast on LLM-produced malformed args before dispatching the call
52
+ across the Kafka boundary.
53
+
54
+ Note: tools with unannotated parameters (which default to ``Any`` in
55
+ pydantic-ai's function-schema) or with ``**kwargs`` (where extra fields
56
+ are allowed) bypass meaningful validation here — the worker-side
57
+ ``except Exception`` catch is the safety net for those.
58
+ """
59
+ return self._tool.function_schema.validator.validate_python(args_dict)
60
+
61
+
62
+ class ToolNodeDef(BaseToolNodeDef):
63
+ @classmethod
64
+ def create_tool_node(
65
+ cls,
66
+ func: Callable[..., Any],
67
+ subscribe_topics: str | list[str],
68
+ publish_topic: str,
69
+ gates: list[GateFunction] | None = None,
70
+ ) -> Self:
71
+ if not isinstance(subscribe_topics, (list, tuple)):
72
+ subscribe_topics = [subscribe_topics]
73
+ tool = Tool(func)
74
+ return cls(
75
+ node_id=f"tool_{func.__name__}",
76
+ tool_schema=tool.tool_def,
77
+ subscribe_topics=subscribe_topics,
78
+ publish_topic=publish_topic,
79
+ _tool=tool,
80
+ gates=list(gates) if gates else [],
81
+ )
82
+
83
+ async def run(self, ctx: SessionRunContext, tool_call_id: str) -> NodeResult[State]:
84
+ logger.debug(
85
+ "[%s] tool run entered tool=%s tool_call_id=%s emitter=%s",
86
+ ctx.deps.correlation_id[:8],
87
+ self.name,
88
+ tool_call_id,
89
+ ctx.emitter_node_id,
90
+ )
91
+ tool_call_part = ctx.state.get_tool_call(tool_call_id)
92
+ if tool_call_part is None:
93
+ logger.warning(
94
+ "tool node reached but no matching tool call found in run state for tool_call_id=%s",
95
+ tool_call_id,
96
+ )
97
+ return Silent()
98
+
99
+ tool_call_ctx = ToolContext(
100
+ deps=ctx.deps,
101
+ agent_name=ctx.emitter_node_id,
102
+ tool_call_id=tool_call_part.tool_call_id,
103
+ tool_name=tool_call_part.tool_name,
104
+ messages=ctx.state.message_history,
105
+ run_id=ctx.deps.correlation_id,
106
+ )
107
+
108
+ # TODO(#143): bounded retries / backoff for non-ModelRetry exceptions.
109
+ # ModelRetry below provides LLM-visible retry per pydantic-ai semantics
110
+ # but is not yet rate-limited on the deferred path.
111
+ try:
112
+ result = await self._tool.function_schema.call(tool_call_part.args_as_dict(), tool_call_ctx)
113
+ # Construct the ToolReturn and eagerly verify it is wire-safe BEFORE
114
+ # storing in state. FastStream's envelope serialization at publish
115
+ # time would raise PydanticSerializationError on a non-serializable
116
+ # return_value, killing the worker handler before the reply
117
+ # publishes — the silent-hang failure mode this module exists to
118
+ # prevent. By serializing inside the try block, any failure flows
119
+ # through ``except Exception`` below and surfaces as a FailedToolCall.
120
+ tool_return = ToolReturn(return_value=result, metadata={"tool_call_id": tool_call_part.tool_call_id})
121
+ pydantic_core.to_json(tool_return)
122
+ except ModelRetry as e:
123
+ logger.warning(
124
+ "[%s] tool=%s raised ModelRetry: %s",
125
+ ctx.deps.correlation_id[:8],
126
+ self.name,
127
+ e.message,
128
+ )
129
+ ctx.state.add_tool_result(
130
+ tool_call_part.tool_call_id,
131
+ RetryPromptPart(
132
+ content=e.message,
133
+ tool_name=tool_call_part.tool_name,
134
+ tool_call_id=tool_call_part.tool_call_id,
135
+ ),
136
+ )
137
+ return ReturnCall[State](state=ctx.state)
138
+ except Exception as e:
139
+ logger.exception(
140
+ "[%s] tool=%s tool_call_id=%s raised %s; surfacing FailedToolCall to agent",
141
+ ctx.deps.correlation_id[:8],
142
+ self.name,
143
+ tool_call_part.tool_call_id,
144
+ type(e).__name__,
145
+ )
146
+ # Construct the marker defensively: a validator on FailedToolCall (e.g.,
147
+ # min_length on tool_call_id) could itself raise inside this except block
148
+ # and re-introduce the silent-hang failure mode we exist to prevent.
149
+ try:
150
+ marker: FailedToolCall = FailedToolCall(
151
+ tool_name=tool_call_part.tool_name,
152
+ tool_call_id=tool_call_part.tool_call_id,
153
+ exc_type=type(e).__name__,
154
+ exc_message=_safe_exc_message(e),
155
+ )
156
+ except Exception as construct_err:
157
+ logger.exception(
158
+ "[%s] tool=%s tool_call_id=%s failed to construct FailedToolCall (%s); using fallback sentinel marker",
159
+ ctx.deps.correlation_id[:8],
160
+ self.name,
161
+ tool_call_part.tool_call_id,
162
+ type(construct_err).__name__,
163
+ )
164
+ # Fallback: preserve real ``tool_name`` / ``tool_call_id`` from
165
+ # ``tool_call_part`` when they are valid strings (so operators
166
+ # don't lose the correlation key in the raised ToolExecutionError);
167
+ # only substitute sentinels for fields that are themselves
168
+ # problematic. ``isinstance(..., str)`` and truthy guards keep
169
+ # construction total even if the originals are the cause of the
170
+ # primary failure (e.g. empty ``tool_call_id``).
171
+ fallback_tool_name = (
172
+ tool_call_part.tool_name if isinstance(tool_call_part.tool_name, str) and tool_call_part.tool_name else "<unknown>"
173
+ )
174
+ fallback_tool_call_id = (
175
+ tool_call_part.tool_call_id if isinstance(tool_call_part.tool_call_id, str) and tool_call_part.tool_call_id else "<missing>"
176
+ )
177
+ marker = FailedToolCall(
178
+ tool_name=fallback_tool_name,
179
+ tool_call_id=fallback_tool_call_id,
180
+ exc_type="FailedToolCallConstructionError",
181
+ exc_message=f"Failed to construct primary marker: {_safe_exc_message(construct_err)}",
182
+ )
183
+ ctx.state.add_tool_result(tool_call_part.tool_call_id, marker)
184
+ return ReturnCall[State](state=ctx.state)
185
+
186
+ # ``tool_return`` was constructed and serialization-verified inside the
187
+ # try block above; reuse it rather than constructing twice.
188
+ ctx.state.add_tool_result(tool_call_part.tool_call_id, tool_return)
189
+
190
+ logger.debug("[%s] tool completed tool=%s", ctx.deps.correlation_id[:8], self.name)
191
+ return ReturnCall[State](state=ctx.state)
192
+
193
+
194
+ def agent_tool(func: Callable[..., Any] | Callable[..., Awaitable[Any]]) -> ToolNodeDef:
195
+ """Decorator to turn a function into a deployable tool node that agents can call"""
196
+ subscribe_topic = f"tool.{func.__name__}.input"
197
+ publish_topic = f"tool.{func.__name__}.output"
198
+ tool_node = ToolNodeDef.create_tool_node(func=func, subscribe_topics=subscribe_topic, publish_topic=publish_topic)
199
+
200
+ return tool_node