calfkit 0.3.0__tar.gz → 0.3.2__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 (272) hide show
  1. calfkit-0.3.2/.release-please-manifest.json +3 -0
  2. {calfkit-0.3.0 → calfkit-0.3.2}/CHANGELOG.md +25 -0
  3. {calfkit-0.3.0 → calfkit-0.3.2}/PKG-INFO +52 -1
  4. {calfkit-0.3.0 → calfkit-0.3.2}/README.md +51 -0
  5. calfkit-0.3.2/ROADMAP.md +9 -0
  6. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/__init__.py +4 -1
  7. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_protocol.py +1 -1
  8. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/client/client.py +32 -1
  9. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/client/deserialize.py +39 -15
  10. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/client/invocation_handle.py +7 -1
  11. calfkit-0.3.2/calfkit/client/node_result.py +103 -0
  12. calfkit-0.3.2/calfkit/models/node_schema.py +32 -0
  13. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/models/state.py +2 -1
  14. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/nodes/__init__.py +4 -0
  15. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/nodes/agent.py +12 -3
  16. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/nodes/base.py +34 -2
  17. calfkit-0.3.2/calfkit/nodes/consumer.py +331 -0
  18. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/worker/worker.py +12 -2
  19. calfkit-0.3.2/docs/calfkit-v1-design.md +3439 -0
  20. calfkit-0.3.2/docs/calfkit-v1-dx-review.md +1432 -0
  21. calfkit-0.3.2/docs/hooks-design.md +1082 -0
  22. {calfkit-0.3.0 → calfkit-0.3.2}/examples/quickstart/agent_service.py +1 -0
  23. calfkit-0.3.2/examples/quickstart/weather_sink.py +42 -0
  24. {calfkit-0.3.0 → calfkit-0.3.2}/pyproject.toml +1 -1
  25. {calfkit-0.3.0 → calfkit-0.3.2}/tests/conftest.py +38 -1
  26. calfkit-0.3.2/tests/test_co_tenant_tool_isolation.py +458 -0
  27. calfkit-0.3.2/tests/test_consumer.py +1049 -0
  28. calfkit-0.3.2/tests/test_model_settings.py +219 -0
  29. calfkit-0.3.0/.release-please-manifest.json +0 -3
  30. calfkit-0.3.0/calfkit/client/node_result.py +0 -40
  31. calfkit-0.3.0/calfkit/models/node_schema.py +0 -21
  32. {calfkit-0.3.0 → calfkit-0.3.2}/.github/CODEOWNERS +0 -0
  33. {calfkit-0.3.0 → calfkit-0.3.2}/.github/dependabot.yml +0 -0
  34. {calfkit-0.3.0 → calfkit-0.3.2}/.github/workflows/build.yml +0 -0
  35. {calfkit-0.3.0 → calfkit-0.3.2}/.github/workflows/code-checks.yml +0 -0
  36. {calfkit-0.3.0 → calfkit-0.3.2}/.github/workflows/release.yml +0 -0
  37. {calfkit-0.3.0 → calfkit-0.3.2}/.github/workflows/security.yml +0 -0
  38. {calfkit-0.3.0 → calfkit-0.3.2}/.github/workflows/test.yml +0 -0
  39. {calfkit-0.3.0 → calfkit-0.3.2}/.gitignore +0 -0
  40. {calfkit-0.3.0 → calfkit-0.3.2}/LICENSE +0 -0
  41. {calfkit-0.3.0 → calfkit-0.3.2}/Makefile +0 -0
  42. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_types.py +0 -0
  43. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/__init__.py +0 -0
  44. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/LICENSE +0 -0
  45. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/__init__.py +0 -0
  46. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/__main__.py +0 -0
  47. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/_a2a.py +0 -0
  48. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/_agent_graph.py +0 -0
  49. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/_cli/__init__.py +0 -0
  50. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/_cli/web.py +0 -0
  51. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/_function_schema.py +0 -0
  52. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/_griffe.py +0 -0
  53. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/_instrumentation.py +0 -0
  54. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/_json_schema.py +0 -0
  55. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/_mcp.py +0 -0
  56. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/_otel_messages.py +0 -0
  57. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/_output.py +0 -0
  58. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/_parts_manager.py +0 -0
  59. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/_run_context.py +0 -0
  60. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/_system_prompt.py +0 -0
  61. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/_thinking_part.py +0 -0
  62. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/_tool_manager.py +0 -0
  63. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/_utils.py +0 -0
  64. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/ag_ui.py +0 -0
  65. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/agent/__init__.py +0 -0
  66. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/agent/abstract.py +0 -0
  67. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/agent/wrapper.py +0 -0
  68. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/builtin_tools.py +0 -0
  69. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/common_tools/__init__.py +0 -0
  70. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/common_tools/duckduckgo.py +0 -0
  71. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/common_tools/exa.py +0 -0
  72. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/common_tools/tavily.py +0 -0
  73. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/direct.py +0 -0
  74. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/durable_exec/__init__.py +0 -0
  75. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/durable_exec/dbos/__init__.py +0 -0
  76. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/durable_exec/dbos/_agent.py +0 -0
  77. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/durable_exec/dbos/_fastmcp_toolset.py +0 -0
  78. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/durable_exec/dbos/_mcp.py +0 -0
  79. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/durable_exec/dbos/_mcp_server.py +0 -0
  80. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/durable_exec/dbos/_model.py +0 -0
  81. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/durable_exec/dbos/_utils.py +0 -0
  82. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/durable_exec/prefect/__init__.py +0 -0
  83. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/durable_exec/prefect/_agent.py +0 -0
  84. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/durable_exec/prefect/_cache_policies.py +0 -0
  85. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/durable_exec/prefect/_function_toolset.py +0 -0
  86. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/durable_exec/prefect/_mcp_server.py +0 -0
  87. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/durable_exec/prefect/_model.py +0 -0
  88. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/durable_exec/prefect/_toolset.py +0 -0
  89. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/durable_exec/prefect/_types.py +0 -0
  90. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/durable_exec/temporal/__init__.py +0 -0
  91. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/durable_exec/temporal/_agent.py +0 -0
  92. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/durable_exec/temporal/_dynamic_toolset.py +0 -0
  93. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/durable_exec/temporal/_fastmcp_toolset.py +0 -0
  94. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/durable_exec/temporal/_function_toolset.py +0 -0
  95. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/durable_exec/temporal/_logfire.py +0 -0
  96. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/durable_exec/temporal/_mcp.py +0 -0
  97. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/durable_exec/temporal/_mcp_server.py +0 -0
  98. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/durable_exec/temporal/_model.py +0 -0
  99. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/durable_exec/temporal/_run_context.py +0 -0
  100. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/durable_exec/temporal/_toolset.py +0 -0
  101. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/durable_exec/temporal/_workflow.py +0 -0
  102. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/embeddings/__init__.py +0 -0
  103. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/embeddings/base.py +0 -0
  104. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/embeddings/cohere.py +0 -0
  105. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/embeddings/google.py +0 -0
  106. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/embeddings/instrumented.py +0 -0
  107. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/embeddings/openai.py +0 -0
  108. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/embeddings/result.py +0 -0
  109. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/embeddings/sentence_transformers.py +0 -0
  110. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/embeddings/settings.py +0 -0
  111. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/embeddings/test.py +0 -0
  112. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/embeddings/voyageai.py +0 -0
  113. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/embeddings/wrapper.py +0 -0
  114. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/exceptions.py +0 -0
  115. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/ext/__init__.py +0 -0
  116. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/ext/aci.py +0 -0
  117. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/ext/langchain.py +0 -0
  118. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/format_prompt.py +0 -0
  119. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/mcp.py +0 -0
  120. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/messages.py +0 -0
  121. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/models/__init__.py +0 -0
  122. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/models/anthropic.py +0 -0
  123. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/models/bedrock.py +0 -0
  124. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/models/cerebras.py +0 -0
  125. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/models/cohere.py +0 -0
  126. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/models/fallback.py +0 -0
  127. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/models/function.py +0 -0
  128. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/models/gemini.py +0 -0
  129. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/models/google.py +0 -0
  130. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/models/groq.py +0 -0
  131. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/models/huggingface.py +0 -0
  132. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/models/instrumented.py +0 -0
  133. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/models/mcp_sampling.py +0 -0
  134. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/models/mistral.py +0 -0
  135. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/models/openai.py +0 -0
  136. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/models/openrouter.py +0 -0
  137. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/models/outlines.py +0 -0
  138. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/models/test.py +0 -0
  139. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/models/wrapper.py +0 -0
  140. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/models/xai.py +0 -0
  141. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/output.py +0 -0
  142. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/profiles/__init__.py +0 -0
  143. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/profiles/amazon.py +0 -0
  144. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/profiles/anthropic.py +0 -0
  145. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/profiles/cohere.py +0 -0
  146. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/profiles/deepseek.py +0 -0
  147. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/profiles/google.py +0 -0
  148. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/profiles/grok.py +0 -0
  149. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/profiles/groq.py +0 -0
  150. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/profiles/harmony.py +0 -0
  151. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/profiles/meta.py +0 -0
  152. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/profiles/mistral.py +0 -0
  153. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/profiles/moonshotai.py +0 -0
  154. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/profiles/openai.py +0 -0
  155. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/profiles/qwen.py +0 -0
  156. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/profiles/zai.py +0 -0
  157. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/providers/__init__.py +0 -0
  158. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/providers/alibaba.py +0 -0
  159. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/providers/anthropic.py +0 -0
  160. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/providers/azure.py +0 -0
  161. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/providers/bedrock.py +0 -0
  162. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/providers/cerebras.py +0 -0
  163. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/providers/cohere.py +0 -0
  164. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/providers/deepseek.py +0 -0
  165. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/providers/fireworks.py +0 -0
  166. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/providers/gateway.py +0 -0
  167. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/providers/github.py +0 -0
  168. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/providers/google.py +0 -0
  169. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/providers/google_gla.py +0 -0
  170. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/providers/google_vertex.py +0 -0
  171. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/providers/grok.py +0 -0
  172. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/providers/groq.py +0 -0
  173. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/providers/heroku.py +0 -0
  174. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/providers/huggingface.py +0 -0
  175. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/providers/litellm.py +0 -0
  176. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/providers/mistral.py +0 -0
  177. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/providers/moonshotai.py +0 -0
  178. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/providers/nebius.py +0 -0
  179. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/providers/ollama.py +0 -0
  180. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/providers/openai.py +0 -0
  181. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/providers/openrouter.py +0 -0
  182. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/providers/outlines.py +0 -0
  183. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/providers/ovhcloud.py +0 -0
  184. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/providers/sambanova.py +0 -0
  185. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/providers/sentence_transformers.py +0 -0
  186. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/providers/together.py +0 -0
  187. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/providers/vercel.py +0 -0
  188. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/providers/voyageai.py +0 -0
  189. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/providers/xai.py +0 -0
  190. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/py.typed +0 -0
  191. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/result.py +0 -0
  192. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/retries.py +0 -0
  193. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/run.py +0 -0
  194. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/settings.py +0 -0
  195. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/tools.py +0 -0
  196. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/toolsets/__init__.py +0 -0
  197. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/toolsets/_dynamic.py +0 -0
  198. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/toolsets/abstract.py +0 -0
  199. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/toolsets/approval_required.py +0 -0
  200. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/toolsets/combined.py +0 -0
  201. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/toolsets/external.py +0 -0
  202. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/toolsets/fastmcp.py +0 -0
  203. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/toolsets/filtered.py +0 -0
  204. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/toolsets/function.py +0 -0
  205. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/toolsets/prefixed.py +0 -0
  206. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/toolsets/prepared.py +0 -0
  207. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/toolsets/renamed.py +0 -0
  208. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/toolsets/wrapper.py +0 -0
  209. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/ui/__init__.py +0 -0
  210. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/ui/_adapter.py +0 -0
  211. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/ui/_event_stream.py +0 -0
  212. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/ui/_messages_builder.py +0 -0
  213. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/ui/_web/__init__.py +0 -0
  214. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/ui/_web/api.py +0 -0
  215. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/ui/_web/app.py +0 -0
  216. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/ui/ag_ui/__init__.py +0 -0
  217. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/ui/ag_ui/_adapter.py +0 -0
  218. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/ui/ag_ui/_event_stream.py +0 -0
  219. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/ui/ag_ui/app.py +0 -0
  220. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/ui/vercel_ai/__init__.py +0 -0
  221. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/ui/vercel_ai/_adapter.py +0 -0
  222. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/ui/vercel_ai/_event_stream.py +0 -0
  223. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/ui/vercel_ai/_models.py +0 -0
  224. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/ui/vercel_ai/_utils.py +0 -0
  225. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/ui/vercel_ai/request_types.py +0 -0
  226. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/ui/vercel_ai/response_types.py +0 -0
  227. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/pydantic_ai/usage.py +0 -0
  228. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/_vendor/vendor.txt +0 -0
  229. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/client/__init__.py +0 -0
  230. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/client/base.py +0 -0
  231. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/client/middleware.py +0 -0
  232. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/client/reply_dispatcher.py +0 -0
  233. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/exceptions.py +0 -0
  234. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/models/__init__.py +0 -0
  235. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/models/actions.py +0 -0
  236. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/models/envelope.py +0 -0
  237. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/models/payload.py +0 -0
  238. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/models/session_context.py +0 -0
  239. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/models/tool_context.py +0 -0
  240. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/nodes/node.py +0 -0
  241. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/nodes/tool.py +0 -0
  242. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/providers/__init__.py +0 -0
  243. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/providers/pydantic_ai/__init__.py +0 -0
  244. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/providers/pydantic_ai/anthropic.py +0 -0
  245. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/providers/pydantic_ai/model_client.py +0 -0
  246. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/providers/pydantic_ai/openai.py +0 -0
  247. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/worker/__init__.py +0 -0
  248. {calfkit-0.3.0 → calfkit-0.3.2}/calfkit/worker/worker_config.py +0 -0
  249. {calfkit-0.3.0 → calfkit-0.3.2}/codecov.yml +0 -0
  250. {calfkit-0.3.0 → calfkit-0.3.2}/examples/__init__.py +0 -0
  251. {calfkit-0.3.0 → calfkit-0.3.2}/examples/deprecated/agent_dispatcher.py +0 -0
  252. {calfkit-0.3.0 → calfkit-0.3.2}/examples/deprecated/chat_node.py +0 -0
  253. {calfkit-0.3.0 → calfkit-0.3.2}/examples/deprecated/chat_repl_cli.py +0 -0
  254. {calfkit-0.3.0 → calfkit-0.3.2}/examples/deprecated/invoke_agent.py +0 -0
  255. {calfkit-0.3.0 → calfkit-0.3.2}/examples/deprecated/router_node.py +0 -0
  256. {calfkit-0.3.0 → calfkit-0.3.2}/examples/deprecated/tool_nodes.py +0 -0
  257. {calfkit-0.3.0 → calfkit-0.3.2}/examples/quickstart/invoke.py +0 -0
  258. {calfkit-0.3.0 → calfkit-0.3.2}/examples/quickstart/weather_tool.py +0 -0
  259. {calfkit-0.3.0 → calfkit-0.3.2}/examples/rpc_worker.py +0 -0
  260. {calfkit-0.3.0 → calfkit-0.3.2}/release-please-config.json +0 -0
  261. {calfkit-0.3.0 → calfkit-0.3.2}/tests/__init__.py +0 -0
  262. {calfkit-0.3.0 → calfkit-0.3.2}/tests/integration/__init__.py +0 -0
  263. {calfkit-0.3.0 → calfkit-0.3.2}/tests/integration/test_agent_output_types.py +0 -0
  264. {calfkit-0.3.0 → calfkit-0.3.2}/tests/integration/test_agent_workers.py +0 -0
  265. {calfkit-0.3.0 → calfkit-0.3.2}/tests/providers.py +0 -0
  266. {calfkit-0.3.0 → calfkit-0.3.2}/tests/test_concurrent_tool_calls.py +0 -0
  267. {calfkit-0.3.0 → calfkit-0.3.2}/tests/test_gates.py +0 -0
  268. {calfkit-0.3.0 → calfkit-0.3.2}/tests/test_headers.py +0 -0
  269. {calfkit-0.3.0 → calfkit-0.3.2}/tests/test_instructions.py +0 -0
  270. {calfkit-0.3.0 → calfkit-0.3.2}/tests/test_overrides.py +0 -0
  271. {calfkit-0.3.0 → calfkit-0.3.2}/tests/test_serializable.py +0 -0
  272. {calfkit-0.3.0 → calfkit-0.3.2}/tests/utils.py +0 -0
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "0.3.2"
3
+ }
@@ -1,5 +1,30 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.2](https://github.com/calf-ai/calfkit-sdk/compare/v0.3.1...v0.3.2) (2026-05-21)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * route tool returns to private inbox to prevent co-tenant leak ([#142](https://github.com/calf-ai/calfkit-sdk/issues/142)) ([197463f](https://github.com/calf-ai/calfkit-sdk/commit/197463f2f7f338bae53bf144e3a86cf27373e350))
9
+
10
+
11
+ ### Documentation
12
+
13
+ * integrate DX review into v1 design and add reviewer artifact ([#138](https://github.com/calf-ai/calfkit-sdk/issues/138)) ([29a7961](https://github.com/calf-ai/calfkit-sdk/commit/29a7961d5c2d4e46fcb7e2e4a81f43116c34feb3))
14
+
15
+ ## [0.3.1](https://github.com/calf-ai/calfkit-sdk/compare/v0.3.0...v0.3.1) (2026-05-19)
16
+
17
+
18
+ ### Features
19
+
20
+ * add ConsumerNodeDef and expose State on NodeResult ([#137](https://github.com/calf-ai/calfkit-sdk/issues/137)) ([aa139c7](https://github.com/calf-ai/calfkit-sdk/commit/aa139c74fe7e0d1baa0a016b5db423ebdc6e65ad))
21
+ * add runtime model_settings to Agent and Client ([#134](https://github.com/calf-ai/calfkit-sdk/issues/134)) ([47d64b2](https://github.com/calf-ai/calfkit-sdk/commit/47d64b286b3ca151cc40022dfab22b4862d06b04))
22
+
23
+
24
+ ### Documentation
25
+
26
+ * add ROADMAP.md index pointing to design docs ([#135](https://github.com/calf-ai/calfkit-sdk/issues/135)) ([5fa2a1a](https://github.com/calf-ai/calfkit-sdk/commit/5fa2a1af731341e3d9bef71be42e30841451058a))
27
+
3
28
  ## [0.3.0](https://github.com/calf-ai/calfkit-sdk/compare/v0.2.6...v0.3.0) (2026-05-18)
4
29
 
5
30
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: calfkit
3
- Version: 0.3.0
3
+ Version: 0.3.2
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
@@ -356,6 +356,57 @@ For tool-node gating, pass `gates=[...]` to `ToolNodeDef.create_tool_node(...)`
356
356
 
357
357
  <br>
358
358
 
359
+ ### Consumer Nodes (Optional)
360
+
361
+ A **consumer node** is a terminal sink — it subscribes to one or more topics and runs arbitrary Python logic against every event flowing through. Consumers receive the same `NodeResult` that `Client.execute_node()` returns, including the full session state (`tool_calls`, `tool_results`, `message_history`, `metadata`).
362
+
363
+ Deploy a consumer as its own service. Wire it to an agent's `publish_topic` (or any topic carrying calfkit envelopes) to observe outputs from agents, tools, and intermediate hops:
364
+
365
+ ```python
366
+ # weather_sink.py
367
+ import asyncio
368
+ from calfkit.client import Client, NodeResult
369
+ from calfkit.nodes import consumer
370
+ from calfkit.worker import Worker
371
+
372
+ @consumer(subscribe_topics="weather_agent.output")
373
+ async def log_weather(result: NodeResult) -> None:
374
+ if result.output is None:
375
+ return # intermediate hop — no final output yet
376
+ print(f"[{result.correlation_id[:8]}] {result.output}")
377
+
378
+ async def main():
379
+ client = Client.connect("localhost:9092")
380
+ worker = Worker(client, nodes=[log_weather]) # Deploy the consumer node
381
+ await worker.run()
382
+
383
+ if __name__ == "__main__":
384
+ asyncio.run(main())
385
+ ```
386
+
387
+ Run alongside the agent service:
388
+
389
+ ```shell
390
+ python weather_sink.py
391
+ ```
392
+
393
+ An agent's `publish_topic` emits on **every** state transition — intermediate hops, tool completions, and terminals — so `result.output` is `None` on hops without final output parts. Filter via a gate if you only want agent terminals:
394
+
395
+ ```python
396
+ @consumer(
397
+ subscribe_topics="weather_agent.output",
398
+ gates=[lambda ctx: bool(ctx.state.final_output_parts)],
399
+ )
400
+ async def save_final(result: NodeResult) -> None:
401
+ await db.save(result.output) # always populated here
402
+ ```
403
+
404
+ **Upstream requirement**: the upstream agent or tool must have a `publish_topic` set for consumers to tap (e.g. add `publish_topic="weather_agent.output"` to the agent in step 4).
405
+
406
+ **Error policy**: exceptions from the consumer function are logged and swallowed by default so a single bad event can't poison-pill the Kafka offset. Pass `re_raise=True` to fail loud during development.
407
+
408
+ <br>
409
+
359
410
  ## Documentation
360
411
 
361
412
  Full documentation is coming soon. In the meantime, this README serves as the primary reference for getting started with Calfkit.
@@ -318,6 +318,57 @@ For tool-node gating, pass `gates=[...]` to `ToolNodeDef.create_tool_node(...)`
318
318
 
319
319
  <br>
320
320
 
321
+ ### Consumer Nodes (Optional)
322
+
323
+ A **consumer node** is a terminal sink — it subscribes to one or more topics and runs arbitrary Python logic against every event flowing through. Consumers receive the same `NodeResult` that `Client.execute_node()` returns, including the full session state (`tool_calls`, `tool_results`, `message_history`, `metadata`).
324
+
325
+ Deploy a consumer as its own service. Wire it to an agent's `publish_topic` (or any topic carrying calfkit envelopes) to observe outputs from agents, tools, and intermediate hops:
326
+
327
+ ```python
328
+ # weather_sink.py
329
+ import asyncio
330
+ from calfkit.client import Client, NodeResult
331
+ from calfkit.nodes import consumer
332
+ from calfkit.worker import Worker
333
+
334
+ @consumer(subscribe_topics="weather_agent.output")
335
+ async def log_weather(result: NodeResult) -> None:
336
+ if result.output is None:
337
+ return # intermediate hop — no final output yet
338
+ print(f"[{result.correlation_id[:8]}] {result.output}")
339
+
340
+ async def main():
341
+ client = Client.connect("localhost:9092")
342
+ worker = Worker(client, nodes=[log_weather]) # Deploy the consumer node
343
+ await worker.run()
344
+
345
+ if __name__ == "__main__":
346
+ asyncio.run(main())
347
+ ```
348
+
349
+ Run alongside the agent service:
350
+
351
+ ```shell
352
+ python weather_sink.py
353
+ ```
354
+
355
+ An agent's `publish_topic` emits on **every** state transition — intermediate hops, tool completions, and terminals — so `result.output` is `None` on hops without final output parts. Filter via a gate if you only want agent terminals:
356
+
357
+ ```python
358
+ @consumer(
359
+ subscribe_topics="weather_agent.output",
360
+ gates=[lambda ctx: bool(ctx.state.final_output_parts)],
361
+ )
362
+ async def save_final(result: NodeResult) -> None:
363
+ await db.save(result.output) # always populated here
364
+ ```
365
+
366
+ **Upstream requirement**: the upstream agent or tool must have a `publish_topic` set for consumers to tap (e.g. add `publish_topic="weather_agent.output"` to the agent in step 4).
367
+
368
+ **Error policy**: exceptions from the consumer function are logged and swallowed by default so a single bad event can't poison-pill the Kafka offset. Pass `re_raise=True` to fail loud during development.
369
+
370
+ <br>
371
+
321
372
  ## Documentation
322
373
 
323
374
  Full documentation is coming soon. In the meantime, this README serves as the primary reference for getting started with Calfkit.
@@ -0,0 +1,9 @@
1
+ # Roadmap
2
+
3
+ An index of potential features and changes under consideration for Calf SDK. Each entry links to a detailed design document in `docs/`. Inclusion here does not imply commitment — items may be reshaped, deferred, or dropped after review.
4
+
5
+ ## Proposed
6
+
7
+ - [Calfkit 1.0](docs/calfkit-v1-design.md) — 1.0 rewrite proposal covering node, agent, and result shape changes
8
+ - [Hook System](docs/hooks-design.md) — two-layer middleware proposal for pre/post-run extensibility on nodes
9
+ - [Durable Fan-Out Aggregator](docs/durable-fanout-aggregator.md) — replace in-process `_pending_batches` with a Kafka-backed compacted-state aggregator for parallel tool calls
@@ -2,7 +2,7 @@ from importlib.metadata import version
2
2
 
3
3
  from calfkit.client import Client, InvocationHandle, NodeResult
4
4
  from calfkit.models import ToolContext
5
- from calfkit.nodes import Agent, BaseNodeDef, GateFunction, NodeDef, ToolNodeDef, agent_tool
5
+ from calfkit.nodes import Agent, BaseNodeDef, ConsumerFn, ConsumerNodeDef, GateFunction, NodeDef, ToolNodeDef, agent_tool, consumer
6
6
  from calfkit.providers import AnthropicModelClient, OpenAIModelClient, OpenAIResponsesModelClient
7
7
  from calfkit.worker import Worker
8
8
 
@@ -18,10 +18,13 @@ __all__ = [
18
18
  # nodes
19
19
  "Agent",
20
20
  "BaseNodeDef",
21
+ "ConsumerFn",
22
+ "ConsumerNodeDef",
21
23
  "GateFunction",
22
24
  "NodeDef",
23
25
  "ToolNodeDef",
24
26
  "agent_tool",
27
+ "consumer",
25
28
  # providers
26
29
  "AnthropicModelClient",
27
30
  "OpenAIModelClient",
@@ -8,7 +8,7 @@ Do not add imports from ``calfkit.*`` to this module.
8
8
 
9
9
  from typing import Any, Literal
10
10
 
11
- NodeKind = Literal["node", "agent", "tool", "client"]
11
+ NodeKind = Literal["node", "agent", "tool", "client", "consumer"]
12
12
  """Closed value space for the ``x-calf-emitter-kind`` Kafka header. Subclasses of
13
13
  ``BaseNodeDef`` declare their kind via ``_node_kind: ClassVar[NodeKind]``; the
14
14
  client publishes with ``CLIENT_KIND`` directly.
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import json
3
4
  from collections.abc import Sequence
4
5
  from typing import Any, overload
5
6
 
@@ -7,6 +8,7 @@ import uuid_utils
7
8
 
8
9
  from calfkit._types import OutputT
9
10
  from calfkit._vendor.pydantic_ai.messages import ModelMessage, ModelRequest
11
+ from calfkit._vendor.pydantic_ai.settings import ModelSettings
10
12
  from calfkit.client.base import BaseClient
11
13
  from calfkit.client.deserialize import _UNSET
12
14
  from calfkit.client.invocation_handle import InvocationHandle
@@ -39,6 +41,7 @@ class Client(BaseClient):
39
41
  message_history: list[ModelMessage] | None = ...,
40
42
  run_args: Sequence[Any] | None = ...,
41
43
  deps: dict[str, Any] | None = ...,
44
+ model_settings: ModelSettings | dict[str, Any] | None = ...,
42
45
  ) -> InvocationHandle[OutputT]: ...
43
46
 
44
47
  @overload
@@ -54,6 +57,7 @@ class Client(BaseClient):
54
57
  message_history: list[ModelMessage] | None = ...,
55
58
  run_args: Sequence[Any] | None = ...,
56
59
  deps: dict[str, Any] | None = ...,
60
+ model_settings: ModelSettings | dict[str, Any] | None = ...,
57
61
  ) -> InvocationHandle[Any]: ...
58
62
 
59
63
  async def invoke_node(
@@ -69,6 +73,7 @@ class Client(BaseClient):
69
73
  message_history: list[ModelMessage] | None = None,
70
74
  run_args: Sequence[Any] | None = None,
71
75
  deps: dict[str, Any] | None = None,
76
+ model_settings: ModelSettings | dict[str, Any] | None = None,
72
77
  ) -> InvocationHandle[Any]:
73
78
  """Invoke an agent node asynchronously and return a handle for the reply.
74
79
 
@@ -95,11 +100,21 @@ class Client(BaseClient):
95
100
  method.
96
101
  deps: A mapping of dependency keys to values, made available to
97
102
  the node's tools at runtime via the session context.
103
+ model_settings: Per-call model settings (e.g. ``{"temperature": 0.0}``)
104
+ that merge over the agent's constructor defaults, which in turn
105
+ merge over the model client's defaults. Must be JSON-serializable
106
+ since it travels over Kafka.
98
107
 
99
108
  Returns:
100
109
  An :class:`InvocationHandle` whose ``result()`` resolves to a
101
110
  :class:`NodeResult`.
102
111
  """
112
+ if model_settings is not None:
113
+ try:
114
+ json.dumps(model_settings, allow_nan=False)
115
+ except (TypeError, ValueError) as exc:
116
+ raise ValueError(f"model_settings is not JSON-serializable: {exc}. Payload: {model_settings!r}") from exc
117
+
103
118
  if correlation_id is None:
104
119
  correlation_id = uuid_utils.uuid7().hex
105
120
  if reply_topic is None:
@@ -107,13 +122,21 @@ class Client(BaseClient):
107
122
 
108
123
  state = State(message_history=message_history or list(), temp_instructions=temp_instructions)
109
124
  state.stage_message(ModelRequest.user_text_prompt(user_prompt))
125
+ overrides = (
126
+ OverridesState(
127
+ override_agent_tools=tool_overrides,
128
+ model_settings=dict(model_settings) if model_settings is not None else None,
129
+ )
130
+ if tool_overrides is not None or model_settings is not None
131
+ else None
132
+ )
110
133
  return await self._invoke(
111
134
  topic=topic,
112
135
  reply_topic=reply_topic,
113
136
  correlation_id=correlation_id,
114
137
  run_args=run_args,
115
138
  state=state,
116
- overrides=OverridesState(override_agent_tools=tool_overrides) if tool_overrides is not None else None,
139
+ overrides=overrides,
117
140
  deps=deps,
118
141
  output_type=output_type,
119
142
  )
@@ -132,6 +155,7 @@ class Client(BaseClient):
132
155
  message_history: list[ModelMessage] | None = ...,
133
156
  run_args: Sequence[Any] | None = ...,
134
157
  deps: dict[str, Any] | None = ...,
158
+ model_settings: ModelSettings | dict[str, Any] | None = ...,
135
159
  timeout: float | None = ...,
136
160
  ) -> NodeResult[OutputT]: ...
137
161
 
@@ -148,6 +172,7 @@ class Client(BaseClient):
148
172
  message_history: list[ModelMessage] | None = ...,
149
173
  run_args: Sequence[Any] | None = ...,
150
174
  deps: dict[str, Any] | None = ...,
175
+ model_settings: ModelSettings | dict[str, Any] | None = ...,
151
176
  timeout: float | None = ...,
152
177
  ) -> NodeResult[Any]: ...
153
178
 
@@ -164,6 +189,7 @@ class Client(BaseClient):
164
189
  message_history: list[ModelMessage] | None = None,
165
190
  run_args: Sequence[Any] | None = None,
166
191
  deps: dict[str, Any] | None = None,
192
+ model_settings: ModelSettings | dict[str, Any] | None = None,
167
193
  timeout: float | None = None,
168
194
  ) -> NodeResult[Any]:
169
195
  """Invoke an agent node and await the reply in a single call.
@@ -193,6 +219,10 @@ class Client(BaseClient):
193
219
  method.
194
220
  deps: A mapping of dependency keys to values, made available to
195
221
  the node's tools at runtime via the session context.
222
+ model_settings: Per-call model settings (e.g. ``{"temperature": 0.0}``)
223
+ that merge over the agent's constructor defaults, which in turn
224
+ merge over the model client's defaults. Must be JSON-serializable
225
+ since it travels over Kafka.
196
226
  timeout: Maximum seconds to wait for the reply. ``None`` means
197
227
  wait indefinitely.
198
228
 
@@ -214,5 +244,6 @@ class Client(BaseClient):
214
244
  message_history=message_history,
215
245
  run_args=run_args,
216
246
  deps=deps,
247
+ model_settings=model_settings,
217
248
  )
218
249
  return await handle.result(timeout=timeout)
@@ -15,6 +15,9 @@ _UNSET: Any = object()
15
15
  def deserialize_to_node_result(
16
16
  envelope: Envelope,
17
17
  output_type: type[Any] = _UNSET,
18
+ *,
19
+ strict: bool = True,
20
+ type_adapter: TypeAdapter[Any] | None = None,
18
21
  ) -> NodeResult[Any]:
19
22
  """Project an ``Envelope`` into a client-facing ``NodeResult``.
20
23
 
@@ -26,41 +29,57 @@ def deserialize_to_node_result(
26
29
  (returned as a raw dict), fall back to ``TextPart.text`` (str).
27
30
  * ``str``: extract the first ``TextPart.text``.
28
31
  * **anything else**: extract the first ``DataPart.data`` and validate
29
- it through ``TypeAdapter(output_type)``.
32
+ it through ``TypeAdapter(output_type)`` (or *type_adapter* if
33
+ provided).
34
+ strict: When ``True`` (default — client semantics), raises
35
+ :class:`DeserializationError` if ``final_output_parts`` is empty or
36
+ doesn't contain the expected part type. When ``False`` (consumer
37
+ semantics), returns ``output=None`` for an empty
38
+ ``final_output_parts`` (intermediate hop / tool completion);
39
+ validation errors on *present* parts still propagate.
40
+ type_adapter: An optional pre-built :class:`pydantic.TypeAdapter` to
41
+ use for validating ``DataPart.data`` against *output_type*. When
42
+ ``None`` (default), a new adapter is constructed per call.
43
+ Consumers pre-build at wiring time so schema-generation errors
44
+ surface once at construction rather than per envelope.
30
45
 
31
46
  Returns:
32
- A ``NodeResult`` whose ``.output`` is typed according to *output_type*.
47
+ A ``NodeResult`` whose ``.output`` is typed according to *output_type*
48
+ (or ``None`` when ``strict=False`` and no parts are present).
33
49
 
34
50
  Raises:
35
51
  DeserializationError: If the expected content part is not found in
36
- ``final_output_parts``.
52
+ ``final_output_parts`` (and either ``strict=True`` or the parts
53
+ list is non-empty but lacks the expected shape).
54
+ pydantic.ValidationError: If ``output_type`` is provided and the
55
+ matching ``DataPart.data`` doesn't validate against it.
56
+ pydantic.PydanticSchemaGenerationError: If ``type_adapter`` is ``None``
57
+ and ``output_type`` cannot be schematized by :class:`TypeAdapter`.
37
58
  """
38
59
  state = envelope.context.state
39
- output_parts = state.final_output_parts
40
- message_history = state.message_history
41
- metadata = state.metadata
42
60
  correlation_id = envelope.context.deps.correlation_id
43
61
 
44
- output = _extract_output(output_parts, output_type)
62
+ if not state.final_output_parts and not strict:
63
+ output: Any = None
64
+ else:
65
+ output = _extract_output(state.final_output_parts, output_type, type_adapter=type_adapter)
45
66
 
46
67
  return NodeResult(
47
68
  output=output,
48
- output_parts=output_parts,
49
- message_history=message_history,
50
- metadata=metadata,
69
+ state=state,
51
70
  correlation_id=correlation_id,
52
71
  emitter_node_id=envelope.context.emitter_node_id,
53
72
  emitter_node_kind=envelope.context.emitter_node_kind,
54
73
  )
55
74
 
56
75
 
57
- def _extract_output(parts: list[Any], output_type: type[Any]) -> Any:
76
+ def _extract_output(parts: list[Any], output_type: type[Any], type_adapter: TypeAdapter[Any] | None = None) -> Any:
58
77
  """Extract and optionally deserialize the output from content parts."""
59
78
  if output_type is _UNSET:
60
79
  return _extract_auto(parts)
61
80
  if output_type is str:
62
81
  return _extract_text(parts)
63
- return _extract_data(parts, output_type)
82
+ return _extract_data(parts, output_type, type_adapter=type_adapter)
64
83
 
65
84
 
66
85
  def _extract_auto(parts: list[Any]) -> Any:
@@ -82,10 +101,15 @@ def _extract_text(parts: list[Any]) -> str:
82
101
  raise DeserializationError("No TextPart found in final_output_parts; expected output_type=str.")
83
102
 
84
103
 
85
- def _extract_data(parts: list[Any], output_type: type[Any]) -> Any:
86
- """Extract the first DataPart.data and validate via TypeAdapter."""
104
+ def _extract_data(parts: list[Any], output_type: type[Any], type_adapter: TypeAdapter[Any] | None = None) -> Any:
105
+ """Extract the first DataPart.data and validate via TypeAdapter.
106
+
107
+ Uses *type_adapter* if provided; otherwise constructs a new one (which may
108
+ raise :class:`pydantic.PydanticSchemaGenerationError` if *output_type* is
109
+ unschematizable).
110
+ """
87
111
  for part in parts:
88
112
  if isinstance(part, DataPart):
89
- adapter: TypeAdapter[Any] = TypeAdapter(output_type)
113
+ adapter = type_adapter if type_adapter is not None else TypeAdapter(output_type)
90
114
  return adapter.validate_python(part.data)
91
115
  raise DeserializationError(f"No DataPart found in final_output_parts; expected output_type={getattr(output_type, '__name__', str(output_type))}.")
@@ -30,7 +30,13 @@ class InvocationHandle(Generic[OutputT]):
30
30
  Raises:
31
31
  asyncio.TimeoutError: If *timeout* elapses before a reply arrives.
32
32
  RuntimeError: If this handle was created without a future (fire-and-forget).
33
- DeserializationError: If the expected output part type is missing.
33
+ DeserializationError: If the expected output part type is missing
34
+ from ``final_output_parts``.
35
+ pydantic.ValidationError: If ``output_type`` is provided and the
36
+ reply's ``DataPart.data`` doesn't validate against it.
37
+ pydantic.PydanticSchemaGenerationError: If ``output_type`` cannot
38
+ be schematized by :class:`pydantic.TypeAdapter` (e.g. an
39
+ unsupported generic or non-type value).
34
40
  """
35
41
  if self._future is None:
36
42
  raise RuntimeError("This handle has no associated future — was the client's reply dispatcher configured?")
@@ -0,0 +1,103 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any, Generic
5
+
6
+ from calfkit._types import OutputT
7
+ from calfkit._vendor.pydantic_ai.messages import ModelMessage
8
+ from calfkit.models import ContentPart, State
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class NodeResult(Generic[OutputT]):
13
+ """Client-facing projection of the session state after a node returns.
14
+
15
+ A ``NodeResult`` is what callers receive in two places:
16
+
17
+ * :meth:`Client.execute_node` / :meth:`InvocationHandle.result` — the
18
+ final reply from an agent invocation.
19
+ * The user function of a :class:`ConsumerNodeDef` — one per envelope on
20
+ every subscribed topic, including intermediate hops.
21
+
22
+ The ``state`` field is the full session :class:`~calfkit.models.State` at
23
+ the moment this envelope was published, exposing message history, in-flight
24
+ tool calls/results, application metadata, runtime overrides, and any other
25
+ state fields. ``message_history``, ``output_parts``, and ``metadata`` are
26
+ convenience properties that read through ``state``.
27
+
28
+ Treat ``NodeResult`` and its ``state`` as read-only. The dataclass is
29
+ frozen, but ``state`` is a mutable Pydantic model:
30
+
31
+ * **Consumer path**: the consumer never republishes (no ``publish_topic``),
32
+ so mutations have no observable downstream effect. They can still
33
+ surprise other code holding the same ``NodeResult`` instance.
34
+ * **Client path** (:meth:`Client.execute_node` / :meth:`InvocationHandle.result`):
35
+ the caller's lifetime owns the instance — mutations are caller-visible
36
+ and may corrupt any other code holding a parallel reference (caches,
37
+ retry layers, etc.).
38
+
39
+ ``NodeResult`` is intentionally unhashable (``__hash__ = None``): the
40
+ underlying ``state`` field is a mutable Pydantic model and cannot be
41
+ placed in a set or used as a dict key safely. Use ``correlation_id`` if
42
+ you need a hashable identifier.
43
+ """
44
+
45
+ output: OutputT | None
46
+ """Deserialized final output (typed via ``output_type``).
47
+
48
+ ``None`` on intermediate hops — envelopes whose ``state.final_output_parts``
49
+ is empty (e.g. agent hops mid-tool-call, tool completions). Populated when
50
+ the upstream node emitted a terminal envelope with final output parts.
51
+ Client-side strict-mode results (the default) always have ``output``
52
+ populated; consumer-side results may not.
53
+ """
54
+
55
+ state: State
56
+ """Full session state at this hop. Includes:
57
+
58
+ * ``message_history`` — cumulative conversation
59
+ * ``final_output_parts`` — agent's structured/text output (empty on
60
+ intermediate hops)
61
+ * ``tool_calls`` / ``tool_results`` — in-flight tool batch (keyed by
62
+ ``tool_call_id``)
63
+ * ``metadata`` — application-level metadata
64
+ * ``overrides`` — agent tool overrides applied to this invocation (or
65
+ ``None`` if unset)
66
+ * ``uncommitted_message`` / ``temp_instructions`` — agent-loop scratch;
67
+ usually ``None`` on terminal hops, may be populated mid-loop and is not
68
+ part of the public contract
69
+ """
70
+
71
+ correlation_id: str
72
+ """The correlation ID that ties this result to its invocation."""
73
+
74
+ emitter_node_id: str | None = None
75
+ """Node id of the node that emitted this reply (sourced from the
76
+ ``x-calf-emitter`` Kafka header). May be ``None`` if the upstream
77
+ producer didn't stamp the header (e.g. a non-calfkit publisher)."""
78
+
79
+ emitter_node_kind: str | None = None
80
+ """Coarse classification of the emitter (one of ``NodeKind``), sourced
81
+ from the ``x-calf-emitter-kind`` Kafka header. May be ``None`` if not
82
+ stamped."""
83
+
84
+ # NodeResult holds a mutable Pydantic model (state); the dataclass-
85
+ # synthesized __hash__ would recursively try to hash unhashable fields and
86
+ # raise at use-time. Declare unhashability explicitly so static type
87
+ # checkers and runtime introspection agree.
88
+ __hash__ = None # type: ignore[assignment]
89
+
90
+ @property
91
+ def output_parts(self) -> list[ContentPart]:
92
+ """Convenience: ``state.final_output_parts``."""
93
+ return self.state.final_output_parts
94
+
95
+ @property
96
+ def message_history(self) -> list[ModelMessage]:
97
+ """Convenience: ``state.message_history``."""
98
+ return self.state.message_history
99
+
100
+ @property
101
+ def metadata(self) -> Any:
102
+ """Convenience: ``state.metadata``."""
103
+ return self.state.metadata
@@ -0,0 +1,32 @@
1
+ from dataclasses import KW_ONLY, dataclass
2
+
3
+ from calfkit._vendor.pydantic_ai.tools import ToolDefinition
4
+
5
+
6
+ @dataclass
7
+ class BaseNodeSchema:
8
+ _: KW_ONLY
9
+ node_id: str
10
+ subscribe_topics: list[str]
11
+ publish_topic: str | None
12
+
13
+ def __post_init__(self) -> None:
14
+ if not isinstance(self.subscribe_topics, (list, tuple)):
15
+ self.subscribe_topics = [self.subscribe_topics]
16
+ # Reject empty subscribe_topics for every node kind (Agent, Consumer,
17
+ # Tool, …). Lives here rather than in ``BaseNodeDef.__init__`` because
18
+ # ``@dataclass`` subclasses like ``BaseToolNodeDef`` get an
19
+ # auto-generated ``__init__`` that bypasses ``BaseNodeDef.__init__``
20
+ # entirely; ``__post_init__`` is the one hook every subclass runs.
21
+ # Without this guard, ``Worker.register_handlers`` would still add
22
+ # ``_return_topic`` to the subscriber set (issue #141 fix), so the
23
+ # node would "register" successfully but have no public inbox — a
24
+ # silent zombie consumer.
25
+ if not self.subscribe_topics:
26
+ raise ValueError(f"node {self.node_id!r} requires at least one subscribe_topic; got empty list")
27
+
28
+
29
+ @dataclass
30
+ class BaseToolNodeSchema(BaseNodeSchema):
31
+ _: KW_ONLY
32
+ tool_schema: ToolDefinition
@@ -23,7 +23,8 @@ class OverridesState(BaseAgentActivityState):
23
23
  """State for storing any override objects"""
24
24
 
25
25
  model_config = ConfigDict(extra="ignore")
26
- override_agent_tools: list[BaseToolNodeSchema] | None
26
+ override_agent_tools: list[BaseToolNodeSchema] | None = None
27
+ model_settings: dict[str, Any] | None = None
27
28
 
28
29
 
29
30
  class CoreMessageState(BaseAgentActivityState):
@@ -1,5 +1,6 @@
1
1
  from calfkit.nodes.agent import Agent, BaseAgentNodeDef
2
2
  from calfkit.nodes.base import BaseNodeDef, GateFunction
3
+ from calfkit.nodes.consumer import ConsumerFn, ConsumerNodeDef, consumer
3
4
  from calfkit.nodes.node import NodeDef
4
5
  from calfkit.nodes.tool import BaseToolNodeDef, ToolNodeDef, agent_tool
5
6
 
@@ -8,8 +9,11 @@ __all__ = [
8
9
  "BaseAgentNodeDef",
9
10
  "BaseNodeDef",
10
11
  "BaseToolNodeDef",
12
+ "ConsumerFn",
13
+ "ConsumerNodeDef",
11
14
  "GateFunction",
12
15
  "NodeDef",
13
16
  "ToolNodeDef",
14
17
  "agent_tool",
18
+ "consumer",
15
19
  ]
@@ -1,6 +1,6 @@
1
1
  import logging
2
2
  from collections.abc import Callable
3
- from typing import Any, ClassVar, Generic
3
+ from typing import Any, ClassVar, Generic, cast
4
4
 
5
5
  from calfkit._protocol import NodeKind
6
6
  from calfkit._types import AgentOutputT
@@ -8,6 +8,7 @@ from calfkit._vendor.pydantic_ai import Agent as InternalAgentLoop
8
8
  from calfkit._vendor.pydantic_ai import DeferredToolRequests
9
9
  from calfkit._vendor.pydantic_ai.messages import RetryPromptPart
10
10
  from calfkit._vendor.pydantic_ai.output import OutputSpec
11
+ from calfkit._vendor.pydantic_ai.settings import ModelSettings
11
12
  from calfkit._vendor.pydantic_ai.tools import DeferredToolResults
12
13
  from calfkit._vendor.pydantic_ai.toolsets.external import ExternalToolset
13
14
  from calfkit.models import Call, DataPart, NodeResult, ReturnCall, State, TailCall, TextPart
@@ -42,6 +43,7 @@ class BaseAgentNodeDef(
42
43
  model_client: PydanticModelClient,
43
44
  final_output_type: OutputSpec[AgentOutputT] = str, # type: ignore[assignment]
44
45
  sequential_only_mode: bool = False,
46
+ model_settings: ModelSettings | dict[str, Any] | None = None,
45
47
  ):
46
48
  self.final_output_type = final_output_type
47
49
  self.system_prompt = system_prompt
@@ -55,7 +57,12 @@ class BaseAgentNodeDef(
55
57
  super().__init__(node_id=node_id, subscribe_topics=subscribe_topics, publish_topic=publish_topic, gates=gates)
56
58
 
57
59
  self._agent_loop: InternalAgentLoop[dict[str, Any], AgentOutputT | DeferredToolRequests] = InternalAgentLoop(
58
- model_client, name=self.name, output_type=[final_output_type, DeferredToolRequests], deps_type=dict, instructions=system_prompt
60
+ model_client,
61
+ name=self.name,
62
+ output_type=[final_output_type, DeferredToolRequests],
63
+ deps_type=dict,
64
+ instructions=system_prompt,
65
+ model_settings=cast(ModelSettings | None, model_settings),
59
66
  )
60
67
 
61
68
  def _parallel_state_aggregation(self, ctx: SessionRunContext) -> None:
@@ -124,12 +131,14 @@ class BaseAgentNodeDef(
124
131
  if ctx.state.uncommitted_message is not None:
125
132
  ctx.state.commit_message_to_history()
126
133
 
134
+ run_model_settings = cast(ModelSettings | None, ctx.state.overrides.model_settings) if ctx.state.overrides is not None else None
127
135
  result = await self._agent_loop.run(
128
136
  message_history=ctx.state.message_history,
129
137
  instructions=ctx.state.temp_instructions,
130
138
  toolsets=[ExternalToolset([tool.tool_schema for tool in tools_registry.values()])],
131
139
  deps=ctx.deps.provided_deps, # None valid when AgentDepsT=NoneType
132
140
  deferred_tool_results=tool_results,
141
+ model_settings=run_model_settings,
133
142
  )
134
143
  if isinstance(result.output, DeferredToolRequests):
135
144
  # The LLM called one or more tools
@@ -175,7 +184,7 @@ class BaseAgentNodeDef(
175
184
  # TODO: maybe consider a node retry return type that doesn't require round trip to itself.
176
185
  # Tailcall to itself is a roundtrip.
177
186
  logger.debug("[%s] all tool calls invalid, TailCall retry node=%s", ctx.deps.correlation_id[:8], self.name)
178
- return TailCall[State](target_topic=self.subscribe_topics[0], state=ctx.state)
187
+ return TailCall[State](target_topic=self._return_topic, state=ctx.state)
179
188
 
180
189
  pending_tool_calls = [tc for tc in latest_tool_calls if tc.tool_call_id not in ctx.state.tool_results]
181
190