agent-workflow-sdk 0.4.0__tar.gz → 0.6.0__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 (234) hide show
  1. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/CHANGELOG.md +68 -0
  2. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/PKG-INFO +33 -3
  3. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/README.md +32 -2
  4. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/__init__.py +5 -1
  5. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/backends/cli_exec.py +5 -0
  6. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/backends/codex.py +9 -1
  7. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/backends/ollama.py +10 -1
  8. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/events.py +24 -2
  9. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/prebuilt/__init__.py +33 -0
  10. agent_workflow_sdk-0.6.0/agentflow/prebuilt/budget.py +174 -0
  11. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/prebuilt/gate.py +21 -4
  12. agent_workflow_sdk-0.6.0/agentflow/prebuilt/plan_executor.py +388 -0
  13. agent_workflow_sdk-0.6.0/agentflow/prebuilt/scoring.py +208 -0
  14. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/state.py +60 -0
  15. agent_workflow_sdk-0.6.0/design-notes/eval-harness.md +142 -0
  16. agent_workflow_sdk-0.6.0/design-notes/plan-executor.md +206 -0
  17. agent_workflow_sdk-0.6.0/design-notes/token-budget.md +149 -0
  18. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/concepts/state.md +29 -0
  19. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/durability/quality-gates.md +4 -4
  20. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/patterns/long-running-loops.md +105 -6
  21. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/patterns/prebuilt.md +1 -1
  22. agent_workflow_sdk-0.6.0/docs/reference/api/budget.md +13 -0
  23. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/reference/api/events.md +1 -0
  24. agent_workflow_sdk-0.6.0/docs/reference/api/plan-executor.md +14 -0
  25. agent_workflow_sdk-0.6.0/docs/reference/api/scoring.md +12 -0
  26. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/reference/api/state.md +1 -0
  27. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/reference/api.md +9 -0
  28. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/reference/glossary.md +1 -1
  29. agent_workflow_sdk-0.6.0/docs/tutorials/index.md +9 -0
  30. agent_workflow_sdk-0.6.0/docs/tutorials/optimizer/01-first-graph.md +83 -0
  31. agent_workflow_sdk-0.6.0/docs/tutorials/optimizer/02-branching.md +81 -0
  32. agent_workflow_sdk-0.6.0/docs/tutorials/optimizer/03-parallel.md +96 -0
  33. agent_workflow_sdk-0.6.0/docs/tutorials/optimizer/04-dual-state.md +82 -0
  34. agent_workflow_sdk-0.6.0/docs/tutorials/optimizer/05-backend.md +93 -0
  35. agent_workflow_sdk-0.6.0/docs/tutorials/optimizer/06-gate.md +87 -0
  36. agent_workflow_sdk-0.6.0/docs/tutorials/optimizer/07-human.md +70 -0
  37. agent_workflow_sdk-0.6.0/docs/tutorials/optimizer/08-durability.md +76 -0
  38. agent_workflow_sdk-0.6.0/docs/tutorials/optimizer/09-watch.md +70 -0
  39. agent_workflow_sdk-0.6.0/docs/tutorials/optimizer/10-plan-executor.md +91 -0
  40. agent_workflow_sdk-0.6.0/docs/tutorials/optimizer/index.md +28 -0
  41. agent_workflow_sdk-0.4.0/docs/patterns/tutorial-qa-agent.md → agent_workflow_sdk-0.6.0/docs/tutorials/qa-agent.md +1 -1
  42. agent_workflow_sdk-0.6.0/docs/tutorials/query-optimizer/01-first-graph.md +58 -0
  43. agent_workflow_sdk-0.6.0/docs/tutorials/query-optimizer/02-branching.md +51 -0
  44. agent_workflow_sdk-0.6.0/docs/tutorials/query-optimizer/03-parallel.md +35 -0
  45. agent_workflow_sdk-0.6.0/docs/tutorials/query-optimizer/04-dual-state.md +46 -0
  46. agent_workflow_sdk-0.6.0/docs/tutorials/query-optimizer/05-agent.md +69 -0
  47. agent_workflow_sdk-0.6.0/docs/tutorials/query-optimizer/06-gate.md +43 -0
  48. agent_workflow_sdk-0.6.0/docs/tutorials/query-optimizer/07-human.md +45 -0
  49. agent_workflow_sdk-0.6.0/docs/tutorials/query-optimizer/08-durability.md +55 -0
  50. agent_workflow_sdk-0.6.0/docs/tutorials/query-optimizer/09-watch.md +39 -0
  51. agent_workflow_sdk-0.6.0/docs/tutorials/query-optimizer/10-plan-executor.md +80 -0
  52. agent_workflow_sdk-0.6.0/docs/tutorials/query-optimizer/index.md +66 -0
  53. agent_workflow_sdk-0.6.0/examples/dual_state_search.py +188 -0
  54. agent_workflow_sdk-0.6.0/examples/plan_executor_demo.py +108 -0
  55. agent_workflow_sdk-0.6.0/examples/tutorial/01_first_graph.py +95 -0
  56. agent_workflow_sdk-0.6.0/examples/tutorial/02_branching.py +107 -0
  57. agent_workflow_sdk-0.6.0/examples/tutorial/03_parallel.py +143 -0
  58. agent_workflow_sdk-0.6.0/examples/tutorial/04_dual_state.py +156 -0
  59. agent_workflow_sdk-0.6.0/examples/tutorial/05_backend.py +176 -0
  60. agent_workflow_sdk-0.6.0/examples/tutorial/06_gate.py +154 -0
  61. agent_workflow_sdk-0.6.0/examples/tutorial/07_human.py +169 -0
  62. agent_workflow_sdk-0.6.0/examples/tutorial/08_durability.py +196 -0
  63. agent_workflow_sdk-0.6.0/examples/tutorial/09_watch.py +122 -0
  64. agent_workflow_sdk-0.6.0/examples/tutorial/10_plan_executor.py +164 -0
  65. agent_workflow_sdk-0.6.0/examples/tutorial/queryopt/__init__.py +20 -0
  66. agent_workflow_sdk-0.6.0/examples/tutorial/queryopt/ch01_first_graph.py +81 -0
  67. agent_workflow_sdk-0.6.0/examples/tutorial/queryopt/ch02_branching.py +90 -0
  68. agent_workflow_sdk-0.6.0/examples/tutorial/queryopt/ch03_parallel.py +93 -0
  69. agent_workflow_sdk-0.6.0/examples/tutorial/queryopt/ch04_dual_state.py +110 -0
  70. agent_workflow_sdk-0.6.0/examples/tutorial/queryopt/ch05_agent.py +100 -0
  71. agent_workflow_sdk-0.6.0/examples/tutorial/queryopt/ch06_gate.py +102 -0
  72. agent_workflow_sdk-0.6.0/examples/tutorial/queryopt/ch07_human.py +107 -0
  73. agent_workflow_sdk-0.6.0/examples/tutorial/queryopt/ch08_durability.py +120 -0
  74. agent_workflow_sdk-0.6.0/examples/tutorial/queryopt/ch09_watch.py +100 -0
  75. agent_workflow_sdk-0.6.0/examples/tutorial/queryopt/ch10_plan_executor.py +147 -0
  76. agent_workflow_sdk-0.6.0/examples/tutorial/queryopt/db.py +137 -0
  77. agent_workflow_sdk-0.6.0/examples/tutorial/queryopt/proposer.py +143 -0
  78. agent_workflow_sdk-0.6.0/examples/tutorial/queryopt/scorer.py +154 -0
  79. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/mkdocs.yml +30 -1
  80. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/pyproject.toml +1 -1
  81. agent_workflow_sdk-0.6.0/tests/test_budget.py +175 -0
  82. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_gate.py +35 -5
  83. agent_workflow_sdk-0.6.0/tests/test_plan_executor.py +379 -0
  84. agent_workflow_sdk-0.6.0/tests/test_reducers.py +168 -0
  85. agent_workflow_sdk-0.6.0/tests/test_scoring.py +146 -0
  86. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/.env.example +0 -0
  87. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  88. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  89. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  90. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/.github/workflows/ci.yml +0 -0
  91. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/.github/workflows/docs.yml +0 -0
  92. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/.github/workflows/release.yml +0 -0
  93. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/.gitignore +0 -0
  94. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/CONTRIBUTING.md +0 -0
  95. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/DESIGN.md +0 -0
  96. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/LICENSE +0 -0
  97. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/NOTICE +0 -0
  98. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/SECURITY.md +0 -0
  99. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/_pg.py +0 -0
  100. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/backends/__init__.py +0 -0
  101. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/backends/_http.py +0 -0
  102. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/backends/anthropic.py +0 -0
  103. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/backends/base.py +0 -0
  104. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/backends/claude_code.py +0 -0
  105. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/backends/kiro.py +0 -0
  106. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/backends/openai.py +0 -0
  107. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/checkpoint/__init__.py +0 -0
  108. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/checkpoint/_serde.py +0 -0
  109. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/checkpoint/base.py +0 -0
  110. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/checkpoint/file.py +0 -0
  111. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/checkpoint/memory.py +0 -0
  112. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/checkpoint/postgres.py +0 -0
  113. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/checkpoint/redis.py +0 -0
  114. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/checkpoint/sqlite.py +0 -0
  115. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/compiled.py +0 -0
  116. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/controlplane/__init__.py +0 -0
  117. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/controlplane/memory.py +0 -0
  118. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/controlplane/postgres.py +0 -0
  119. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/controlplane/queue.py +0 -0
  120. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/controlplane/records.py +0 -0
  121. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/controlplane/registry.py +0 -0
  122. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/controlplane/worker.py +0 -0
  123. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/errors.py +0 -0
  124. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/graph.py +0 -0
  125. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/interrupts.py +0 -0
  126. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/observability.py +0 -0
  127. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/otel.py +0 -0
  128. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/prebuilt/effects.py +0 -0
  129. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/prebuilt/idempotent.py +0 -0
  130. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/prebuilt/loop.py +0 -0
  131. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/prebuilt/resilience.py +0 -0
  132. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/prebuilt/tool_loop.py +0 -0
  133. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/prebuilt/watch.py +0 -0
  134. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/prometheus.py +0 -0
  135. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/py.typed +0 -0
  136. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/redaction.py +0 -0
  137. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/runtime.py +0 -0
  138. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/server/__init__.py +0 -0
  139. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/server/app.py +0 -0
  140. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/store/__init__.py +0 -0
  141. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/store/_util.py +0 -0
  142. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/store/base.py +0 -0
  143. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/store/memory.py +0 -0
  144. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/store/postgres.py +0 -0
  145. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/telemetry.py +0 -0
  146. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/backends/agent-backends.md +0 -0
  147. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/backends/llm-backends.md +0 -0
  148. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/backends/overview.md +0 -0
  149. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/backends/permissions.md +0 -0
  150. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/backends/retries.md +0 -0
  151. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/concepts/context.md +0 -0
  152. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/concepts/execution.md +0 -0
  153. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/concepts/nodes-and-edges.md +0 -0
  154. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/concepts/running.md +0 -0
  155. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/concepts/subgraphs.md +0 -0
  156. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/durability/checkpointing.md +0 -0
  157. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/durability/control-plane.md +0 -0
  158. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/durability/human-in-the-loop.md +0 -0
  159. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/durability/observability.md +0 -0
  160. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/durability/store.md +0 -0
  161. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/get-started/installation.md +0 -0
  162. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/get-started/mental-model.md +0 -0
  163. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/get-started/overview.md +0 -0
  164. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/get-started/quickstart.md +0 -0
  165. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/index.md +0 -0
  166. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/reference/api/backends.md +0 -0
  167. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/reference/api/checkpoint.md +0 -0
  168. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/reference/api/compiled.md +0 -0
  169. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/reference/api/controlplane.md +0 -0
  170. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/reference/api/errors.md +0 -0
  171. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/reference/api/gate.md +0 -0
  172. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/reference/api/graph.md +0 -0
  173. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/reference/api/interrupts.md +0 -0
  174. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/reference/api/observability.md +0 -0
  175. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/reference/api/permissions.md +0 -0
  176. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/reference/api/prebuilt.md +0 -0
  177. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/reference/api/runtime.md +0 -0
  178. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/reference/api/server.md +0 -0
  179. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/reference/api/store.md +0 -0
  180. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/reference/api/watch.md +0 -0
  181. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/examples/agents_demo.py +0 -0
  182. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/examples/codebase_qa_ollama.py +0 -0
  183. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/examples/hello_graph.py +0 -0
  184. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/examples/hitl_permission.py +0 -0
  185. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/examples/idempotent_effect.py +0 -0
  186. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/examples/mixed_backends.py +0 -0
  187. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/examples/optimize_loop.py +0 -0
  188. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/examples/quality_gate.py +0 -0
  189. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/examples/research_plan_implement_review.py +0 -0
  190. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/examples/serve_control_plane.py +0 -0
  191. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/examples/telemetry_demo.py +0 -0
  192. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/examples/tool_loop_ollama.py +0 -0
  193. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/examples/watch_until_done.py +0 -0
  194. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/conftest.py +0 -0
  195. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_anthropic_backend.py +0 -0
  196. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_backend_lifecycle.py +0 -0
  197. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_checkpoint_contract.py +0 -0
  198. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_cli_backends.py +0 -0
  199. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_concurrency.py +0 -0
  200. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_controlplane.py +0 -0
  201. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_effects.py +0 -0
  202. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_graph_engine.py +0 -0
  203. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_hitl_permission.py +0 -0
  204. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_http_retry.py +0 -0
  205. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_idempotent.py +0 -0
  206. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_input_validation.py +0 -0
  207. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_kiro_backend.py +0 -0
  208. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_live_backends.py +0 -0
  209. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_observability.py +0 -0
  210. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_ollama_backend.py +0 -0
  211. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_openai_backend.py +0 -0
  212. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_otel.py +0 -0
  213. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_permission_policy.py +0 -0
  214. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_postgres_checkpointer.py +0 -0
  215. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_postgres_runqueue.py +0 -0
  216. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_postgres_store.py +0 -0
  217. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_prebuilt_loop.py +0 -0
  218. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_prebuilt_no_httpx.py +0 -0
  219. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_prometheus.py +0 -0
  220. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_provider_resilience.py +0 -0
  221. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_redaction.py +0 -0
  222. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_redis_checkpointer.py +0 -0
  223. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_resilience.py +0 -0
  224. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_run_timeout.py +0 -0
  225. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_server.py +0 -0
  226. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_sqlite_checkpointer.py +0 -0
  227. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_state_isolation.py +0 -0
  228. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_step_atomicity.py +0 -0
  229. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_store.py +0 -0
  230. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_subgraph.py +0 -0
  231. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_sync_facade.py +0 -0
  232. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_telemetry.py +0 -0
  233. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_tool_loop.py +0 -0
  234. {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_watch.py +0 -0
@@ -4,6 +4,74 @@ All notable changes to `agent-workflow-sdk` are documented here. The format
4
4
  follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the
5
5
  project aims to follow [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## [0.6.0] - 2026-09-29
8
+
9
+ ### Changed (breaking)
10
+
11
+ - Quality-gate results and plan-executor tasks are now stored in state as plain
12
+ dicts, not dataclasses, so they are JSON-serializable and survive a durable
13
+ checkpointer. Read `state["gate_results"][name]` as a mapping
14
+ (`result["passed"]`, not `result.passed`), and read `plan_executor` results
15
+ as dicts (`r["output"]`, not `r.output`). The evaluator you pass to
16
+ `add_quality_gate` still returns a `GateResult`, and a handler still returns a
17
+ `TaskResult` — only the *stored* form changed. This fixes a real bug: either
18
+ prebuilt combined with a file/sqlite/postgres/redis checkpointer previously
19
+ raised "not JSON serializable" on the first checkpoint.
20
+
21
+ ### Added
22
+
23
+ - `plan_executor` (`agentflow.prebuilt`): `add_plan_executor(g, handlers=...,
24
+ on_idle=...)` runs a dependency graph of tasks a planner emits as data — a
25
+ task runs once its prerequisites are done, independent tasks fan out
26
+ concurrently into a fixed slot pool, and the plan is checkpointed so a resume
27
+ recomputes the runnable frontier. Ships `Task`, `TaskResult`, the `PlanState`
28
+ mixin, and `plan_complete` / `plan_stalled`.
29
+ - `scoring` (`agentflow.prebuilt`): a deterministic, backend-independent measure
30
+ for a candidate — `Score`, the `Scorer` contract, `score_candidates` (batch,
31
+ bounded concurrency), `best_of` (promote the best feasible, keep the incumbent
32
+ unless strictly beaten), and `ScoreCache`. The verifier half of an optimizer
33
+ loop, so swapping the proposer (a local agent, a hosted LLM) never changes the
34
+ score.
35
+ - Token / cost budget: `Usage` (input/output/total tokens, optional `cost_usd`)
36
+ is now an optional field on `TurnEnd`, populated by the Ollama and Codex
37
+ backends. New `agentflow.prebuilt` helpers: `TokenMeter` (a `Hooks` listener
38
+ that sums usage for observation), the `BudgetState` mixin, `record_usage`, and
39
+ `over_budget` — budget lives in state, survives a resume, and a router stops
40
+ the run at a token or cost cap.
41
+
42
+ ### Documentation
43
+
44
+ - A unified Tutorials section: the Q&A agent quick start, a ten-chapter
45
+ "Mechanics: building an optimizer" tutorial on a toy cost model, and a
46
+ ten-chapter "Applied: a query optimizer with agents" tutorial that measures
47
+ real index/rewrite candidates against a seeded SQLite database, with a local
48
+ Codex agent as the opt-in proposer.
49
+
50
+ ## [0.5.0] - 2026-10-04
51
+
52
+ ### Added
53
+
54
+ - `by_key(policy)` reducer (`agentflow.state`, re-exported from `agentflow`):
55
+ a reducer for a dict channel whose fields each fold with their own reducer.
56
+ Where `merge` applies one last-wins policy to a whole dict, `by_key` lets a
57
+ single structured channel hold fields that accumulate differently — e.g. one
58
+ `search` channel where `intents` / `candidates` / `verified` grow via
59
+ `append` while `events` / `incumbent` are replaced via `last`. This is the
60
+ core primitive a dual-state search/optimization layer (single planner,
61
+ concurrent workers, a deterministic promotion boundary) builds on; the
62
+ fan-out, state isolation, checkpoints, Store, and quality gates it also needs
63
+ already exist. See the dual-state search recipe and
64
+ `examples/dual_state_search.py`.
65
+
66
+ ### Notes
67
+
68
+ - `by_key` folding is one level of structure, not a recursive deep-merge: each
69
+ field's value is combined whole by its reducer (so `last` on a field replaces
70
+ the whole object, it does not patch one of its keys). Writing a field absent
71
+ from the policy raises `KeyError`, mirroring how the engine rejects writes to
72
+ undeclared top-level channels. Use it for distinct fields that accumulate
73
+ differently, not for concurrent partial patches of the same nested object.
74
+
7
75
  ## [0.4.0] - 2026-10-03
8
76
 
9
77
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: agent-workflow-sdk
3
- Version: 0.4.0
3
+ Version: 0.6.0
4
4
  Summary: Async-first SDK for orchestrating local coding-agent CLIs (Claude Code, Codex, Kiro) and LLMs as durable, graph-based workflows.
5
5
  Project-URL: Homepage, https://github.com/dankosorkin/agent-workflow-sdk
6
6
  Project-URL: Repository, https://github.com/dankosorkin/agent-workflow-sdk
@@ -188,7 +188,8 @@ shows the iterate-until-converged loop.
188
188
  State is a `TypedDict` subclass of `State`. Each field is a channel; annotate
189
189
  it with a reducer that folds each node's update into the current value. A
190
190
  field without a reducer uses `last` (last-value-wins). Built-in reducers:
191
- `last`, `append`, `add`, `merge`, `union`. A node returns a partial update; it
191
+ `last`, `append`, `add`, `merge`, `union`, and `by_key` (a dict channel whose
192
+ fields each fold with their own reducer). A node returns a partial update; it
192
193
  never mutates the state it was given.
193
194
 
194
195
  ### Nodes and edges
@@ -540,6 +541,35 @@ app = iterate_until_converged(work, perfect_score=100.0, patience=4)
540
541
  result = await app.invoke({})
541
542
  ```
542
543
 
544
+ `agentflow.prebuilt.add_plan_executor(g, handlers=..., on_idle=...)` runs a
545
+ dependency graph of tasks that a planner emits as data. The planner writes
546
+ `Task`s (each with an `id`, a `kind`, and `prerequisites`) into a `by_key`
547
+ `plan` channel; the executor runs each task once its prerequisites are done,
548
+ fans independent ones out concurrently up to `max_fanout`, and loops until
549
+ nothing is runnable. The graph stays static — a new task is new data, not a new
550
+ node — so the plan is checkpointed and a resume recomputes what is runnable.
551
+ This is how a workflow builds and re-optimizes its own execution plan as it
552
+ goes. See `examples/plan_executor_demo.py`.
553
+
554
+ ```python
555
+ from agentflow.prebuilt import PlanState, Task, TaskResult, add_plan_executor
556
+
557
+ async def run_step(task, state, ctx) -> TaskResult:
558
+ return TaskResult(task.id, output=await do_work(task))
559
+
560
+ async def planner(state, ctx):
561
+ return {"plan": {"tasks": [
562
+ Task("fetch", "step"),
563
+ Task("build", "step", prerequisites=("fetch",)),
564
+ ]}}
565
+
566
+ g = Graph(PlanState)
567
+ g.add_node("planner", planner)
568
+ add_plan_executor(g, handlers={"step": run_step}, on_idle="planner")
569
+ g.add_edge(START, "planner")
570
+ g.add_edge("planner", "dispatch")
571
+ ```
572
+
543
573
  `agentflow.prebuilt.tool_loop(llm, tools)` compiles the function-calling agent
544
574
  loop as a two-node graph: an `agent` node calls the `LLMBackend`, a `tools`
545
575
  node runs any tool the model requested and appends the result to the
@@ -594,7 +624,7 @@ agentflow/
594
624
  checkpoint/ Checkpointer protocol + memory/file/sqlite/redis/postgres
595
625
  store/ Store protocol (cross-thread memory) + memory/postgres
596
626
  controlplane/ RunQueue + GraphRegistry + Worker (memory/postgres)
597
- prebuilt/ iterate_until_converged, tool_loop, with_retry/with_timeout
627
+ prebuilt/ iterate_until_converged, tool_loop, plan_executor, gates
598
628
  examples/ runnable examples
599
629
  tests/ pytest suite (async; offline by default, `-m live` for real backends)
600
630
  DESIGN.md architecture and contracts
@@ -139,7 +139,8 @@ shows the iterate-until-converged loop.
139
139
  State is a `TypedDict` subclass of `State`. Each field is a channel; annotate
140
140
  it with a reducer that folds each node's update into the current value. A
141
141
  field without a reducer uses `last` (last-value-wins). Built-in reducers:
142
- `last`, `append`, `add`, `merge`, `union`. A node returns a partial update; it
142
+ `last`, `append`, `add`, `merge`, `union`, and `by_key` (a dict channel whose
143
+ fields each fold with their own reducer). A node returns a partial update; it
143
144
  never mutates the state it was given.
144
145
 
145
146
  ### Nodes and edges
@@ -491,6 +492,35 @@ app = iterate_until_converged(work, perfect_score=100.0, patience=4)
491
492
  result = await app.invoke({})
492
493
  ```
493
494
 
495
+ `agentflow.prebuilt.add_plan_executor(g, handlers=..., on_idle=...)` runs a
496
+ dependency graph of tasks that a planner emits as data. The planner writes
497
+ `Task`s (each with an `id`, a `kind`, and `prerequisites`) into a `by_key`
498
+ `plan` channel; the executor runs each task once its prerequisites are done,
499
+ fans independent ones out concurrently up to `max_fanout`, and loops until
500
+ nothing is runnable. The graph stays static — a new task is new data, not a new
501
+ node — so the plan is checkpointed and a resume recomputes what is runnable.
502
+ This is how a workflow builds and re-optimizes its own execution plan as it
503
+ goes. See `examples/plan_executor_demo.py`.
504
+
505
+ ```python
506
+ from agentflow.prebuilt import PlanState, Task, TaskResult, add_plan_executor
507
+
508
+ async def run_step(task, state, ctx) -> TaskResult:
509
+ return TaskResult(task.id, output=await do_work(task))
510
+
511
+ async def planner(state, ctx):
512
+ return {"plan": {"tasks": [
513
+ Task("fetch", "step"),
514
+ Task("build", "step", prerequisites=("fetch",)),
515
+ ]}}
516
+
517
+ g = Graph(PlanState)
518
+ g.add_node("planner", planner)
519
+ add_plan_executor(g, handlers={"step": run_step}, on_idle="planner")
520
+ g.add_edge(START, "planner")
521
+ g.add_edge("planner", "dispatch")
522
+ ```
523
+
494
524
  `agentflow.prebuilt.tool_loop(llm, tools)` compiles the function-calling agent
495
525
  loop as a two-node graph: an `agent` node calls the `LLMBackend`, a `tools`
496
526
  node runs any tool the model requested and appends the result to the
@@ -545,7 +575,7 @@ agentflow/
545
575
  checkpoint/ Checkpointer protocol + memory/file/sqlite/redis/postgres
546
576
  store/ Store protocol (cross-thread memory) + memory/postgres
547
577
  controlplane/ RunQueue + GraphRegistry + Worker (memory/postgres)
548
- prebuilt/ iterate_until_converged, tool_loop, with_retry/with_timeout
578
+ prebuilt/ iterate_until_converged, tool_loop, plan_executor, gates
549
579
  examples/ runnable examples
550
580
  tests/ pytest suite (async; offline by default, `-m live` for real backends)
551
581
  DESIGN.md architecture and contracts
@@ -74,6 +74,7 @@ from agentflow.events import (
74
74
  ToolResult,
75
75
  ToolSpec,
76
76
  TurnEnd,
77
+ Usage,
77
78
  )
78
79
  from agentflow.graph import END, START, Graph, Node, Router
79
80
  from agentflow.interrupts import (
@@ -90,6 +91,7 @@ from agentflow.state import (
90
91
  State,
91
92
  add,
92
93
  append,
94
+ by_key,
93
95
  last,
94
96
  merge,
95
97
  union,
@@ -97,7 +99,7 @@ from agentflow.state import (
97
99
  from agentflow.store import Item, MemoryStore, Store
98
100
  from agentflow.telemetry import JsonlTelemetry, MultiHooks
99
101
 
100
- __version__ = "0.4.0"
102
+ __version__ = "0.6.0"
101
103
 
102
104
  __all__ = [
103
105
  "__version__",
@@ -109,6 +111,7 @@ __all__ = [
109
111
  "append",
110
112
  "add",
111
113
  "merge",
114
+ "by_key",
112
115
  "union",
113
116
  # graph
114
117
  "Graph",
@@ -139,6 +142,7 @@ __all__ = [
139
142
  "ToolResult",
140
143
  "PermissionRequest",
141
144
  "PermissionOption",
145
+ "Usage",
142
146
  "TurnEnd",
143
147
  "ErrorEvent",
144
148
  "GateEvent",
@@ -48,6 +48,7 @@ from agentflow.events import (
48
48
  ErrorEvent,
49
49
  TextRequest,
50
50
  TurnEnd,
51
+ Usage,
51
52
  )
52
53
 
53
54
  __all__ = ["CLIExecBackend", "TurnAccumulator"]
@@ -61,6 +62,9 @@ class TurnAccumulator:
61
62
  stop_reason: str | None = None
62
63
  session_id: str | None = None
63
64
  is_error: bool = False
65
+ #: Token/cost usage, if the CLI reports it (set by the subclass on the
66
+ #: terminal event). Left None when the CLI surfaces no counts.
67
+ usage: Usage | None = None
64
68
 
65
69
  def add_text(self, text: str) -> None:
66
70
  if text:
@@ -178,6 +182,7 @@ class CLIExecBackend(BaseAgentBackend):
178
182
  yield TurnEnd(
179
183
  text=acc.text,
180
184
  stop_reason=acc.stop_reason or ("error" if acc.is_error else "end_turn"),
185
+ usage=acc.usage,
181
186
  )
182
187
 
183
188
  # ------------------------------------------------------------------
@@ -34,7 +34,7 @@ from typing import Any, Literal
34
34
 
35
35
  from agentflow.backends.base import PermissionPolicy, policy_intent
36
36
  from agentflow.backends.cli_exec import CLIExecBackend, TurnAccumulator
37
- from agentflow.events import BackendEvent, TextChunk, ToolCall, ToolResult
37
+ from agentflow.events import BackendEvent, TextChunk, ToolCall, ToolResult, Usage
38
38
 
39
39
  __all__ = ["CodexBackend"]
40
40
 
@@ -109,6 +109,14 @@ class CodexBackend(CLIExecBackend):
109
109
 
110
110
  if kind == "turn.completed":
111
111
  acc.stop_reason = "end_turn"
112
+ usage = obj.get("usage")
113
+ if isinstance(usage, dict):
114
+ pin = usage.get("input_tokens")
115
+ pout = usage.get("output_tokens")
116
+ total = usage.get("total_tokens")
117
+ if total is None and (pin is not None or pout is not None):
118
+ total = (pin or 0) + (pout or 0)
119
+ acc.usage = Usage(input_tokens=pin, output_tokens=pout, total_tokens=total)
112
120
  return []
113
121
 
114
122
  if kind == "turn.failed" or kind == "error":
@@ -45,6 +45,7 @@ from agentflow.events import (
45
45
  ToolCall,
46
46
  ToolCallSpec,
47
47
  TurnEnd,
48
+ Usage,
48
49
  )
49
50
 
50
51
  try:
@@ -99,6 +100,7 @@ class OllamaBackend(BaseLLMBackend):
99
100
  text_parts: list[str] = []
100
101
  tool_calls: list[ToolCallSpec] = []
101
102
  stop_reason: str | None = None
103
+ usage: Usage | None = None
102
104
 
103
105
  async with (
104
106
  self.concurrency_guard(),
@@ -127,6 +129,13 @@ class OllamaBackend(BaseLLMBackend):
127
129
 
128
130
  if chunk.get("done"):
129
131
  stop_reason = chunk.get("done_reason") or "stop"
132
+ # Ollama reports token counts on the final chunk:
133
+ # prompt_eval_count = input, eval_count = output.
134
+ pin = chunk.get("prompt_eval_count")
135
+ pout = chunk.get("eval_count")
136
+ if pin is not None or pout is not None:
137
+ total = (pin or 0) + (pout or 0) if (pin or pout) else None
138
+ usage = Usage(input_tokens=pin, output_tokens=pout, total_tokens=total)
130
139
  except httpx.HTTPError as exc:
131
140
  raise BackendTransportError(f"ollama stream failed: {exc}") from exc
132
141
 
@@ -136,7 +145,7 @@ class OllamaBackend(BaseLLMBackend):
136
145
  content=final_text,
137
146
  tool_calls=tuple(tool_calls),
138
147
  )
139
- yield TurnEnd(text=final_text, stop_reason=stop_reason, message=message)
148
+ yield TurnEnd(text=final_text, stop_reason=stop_reason, message=message, usage=usage)
140
149
 
141
150
  # ------------------------------------------------------------------
142
151
 
@@ -44,6 +44,7 @@ __all__ = [
44
44
  "ToolResult",
45
45
  "PermissionOption",
46
46
  "PermissionRequest",
47
+ "Usage",
47
48
  "TurnEnd",
48
49
  "ErrorEvent",
49
50
  "BackendEvent",
@@ -178,14 +179,35 @@ class PermissionRequest:
178
179
  detail: dict[str, Any] = field(default_factory=dict)
179
180
 
180
181
 
182
+ @dataclass(frozen=True, slots=True)
183
+ class Usage:
184
+ """Token (and optional cost) usage reported by a backend for one turn.
185
+
186
+ All fields are optional because backends differ in what they report: some
187
+ give input/output splits, some only a total, some none at all. A field left
188
+ ``None`` means "not reported" — a token budget simply cannot count what a
189
+ backend does not surface, and the budget helpers treat ``None`` as zero.
190
+ ``cost_usd`` is only present when a backend or the caller supplies pricing;
191
+ the SDK ships no price table (it would go stale), so budget on tokens by
192
+ default and set ``cost_usd`` yourself if you want a dollar cap.
193
+ """
194
+
195
+ input_tokens: int | None = None
196
+ output_tokens: int | None = None
197
+ total_tokens: int | None = None
198
+ cost_usd: float | None = None
199
+
200
+
181
201
  @dataclass(frozen=True, slots=True)
182
202
  class TurnEnd:
183
- """Terminal event of a turn. Carries the fully assembled assistant text
184
- and the backend's stop reason (e.g. ``end_turn``, ``max_tokens``)."""
203
+ """Terminal event of a turn. Carries the fully assembled assistant text,
204
+ the backend's stop reason (e.g. ``end_turn``, ``max_tokens``), and optional
205
+ token/cost :class:`Usage` when the backend reports it."""
185
206
 
186
207
  text: str = ""
187
208
  stop_reason: str | None = None
188
209
  message: Message | None = None
210
+ usage: Usage | None = None
189
211
 
190
212
 
191
213
  @dataclass(frozen=True, slots=True)
@@ -14,11 +14,28 @@
14
14
 
15
15
  """Prebuilt graph patterns built entirely from core primitives."""
16
16
 
17
+ from agentflow.prebuilt.budget import BudgetState, TokenMeter, over_budget, record_usage
17
18
  from agentflow.prebuilt.effects import IdempotentOp, IncompleteEffectError, effect_key
18
19
  from agentflow.prebuilt.gate import Evaluator, GateResult, GateState, add_quality_gate
19
20
  from agentflow.prebuilt.idempotent import artifact_key, skip_if_done
20
21
  from agentflow.prebuilt.loop import Candidate, iterate_until_converged
22
+ from agentflow.prebuilt.plan_executor import (
23
+ Handler,
24
+ PlanState,
25
+ Task,
26
+ TaskResult,
27
+ add_plan_executor,
28
+ plan_complete,
29
+ plan_stalled,
30
+ )
21
31
  from agentflow.prebuilt.resilience import with_retry, with_timeout
32
+ from agentflow.prebuilt.scoring import (
33
+ Score,
34
+ ScoreCache,
35
+ Scorer,
36
+ best_of,
37
+ score_candidates,
38
+ )
22
39
  from agentflow.prebuilt.tool_loop import Tool, ToolLoopState, tool_loop
23
40
  from agentflow.prebuilt.watch import (
24
41
  CommandWatcher,
@@ -31,6 +48,13 @@ from agentflow.prebuilt.watch import (
31
48
  __all__ = [
32
49
  "Candidate",
33
50
  "iterate_until_converged",
51
+ "Task",
52
+ "TaskResult",
53
+ "PlanState",
54
+ "Handler",
55
+ "add_plan_executor",
56
+ "plan_complete",
57
+ "plan_stalled",
34
58
  "GateResult",
35
59
  "GateState",
36
60
  "Evaluator",
@@ -45,6 +69,15 @@ __all__ = [
45
69
  "tool_loop",
46
70
  "with_retry",
47
71
  "with_timeout",
72
+ "Score",
73
+ "Scorer",
74
+ "ScoreCache",
75
+ "score_candidates",
76
+ "best_of",
77
+ "TokenMeter",
78
+ "BudgetState",
79
+ "record_usage",
80
+ "over_budget",
48
81
  "WatchResult",
49
82
  "Watcher",
50
83
  "CommandWatcher",
@@ -0,0 +1,174 @@
1
+ # Copyright 2026 Daniel Sorkin
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Token / cost budget for an autonomous run.
16
+
17
+ An agentic loop that proposes with an LLM runs many rounds, each costing
18
+ tokens. Time has a guard (``with_timeout``), load has one (``max_concurrency``),
19
+ loop length has one (``step_limit``) — this is the guard for *spend*.
20
+
21
+ Two pieces, matching "how much did I spend?" and "stop at a cap":
22
+
23
+ - :class:`TokenMeter` is a :class:`~agentflow.observability.Hooks` listener that
24
+ sums usage across a run from the backend events a node emits. Pure
25
+ observation — it never affects the run (a hook cannot, by contract), so it
26
+ answers "how much?" but cannot stop anything.
27
+
28
+ - To *stop*, budget is state. Mix :class:`BudgetState` into your schema, have
29
+ the node that drives the LLM add its turn's usage with :func:`record_usage`,
30
+ and have your router call :func:`over_budget` and route to a stop/escalate
31
+ node when the cap is exceeded. Because the budget rides the checkpoint, it
32
+ survives a resume (a resumed run does not forget what it already spent), and
33
+ stopping is a routing decision — the same discipline as every other stop in
34
+ the SDK: the work writes an artifact, a router decides what happens next.
35
+
36
+ class SearchState(BudgetState):
37
+ ...
38
+
39
+ async def propose(state, ctx):
40
+ turn = await call_llm(...) # yields a TurnEnd with .usage
41
+ return {**result, **record_usage(turn.usage)}
42
+
43
+ def route(state):
44
+ if over_budget(state, max_tokens=100_000):
45
+ return "stop"
46
+ return "continue"
47
+
48
+ The stop takes effect at the next super-step boundary, not mid-turn, so a run
49
+ may overshoot the cap by at most the turn in flight. That is the deliberate
50
+ trade of the state-based design: durable and engine-free, at the cost of
51
+ turn-granular (not token-exact) enforcement.
52
+ """
53
+
54
+ from __future__ import annotations
55
+
56
+ from typing import Annotated, Any
57
+
58
+ from agentflow.events import Usage
59
+ from agentflow.observability import Hooks
60
+ from agentflow.state import State, add, by_key
61
+
62
+ __all__ = ["TokenMeter", "BudgetState", "record_usage", "over_budget"]
63
+
64
+
65
+ class BudgetState(State):
66
+ """Mix into a schema to add the ``budget`` channel the helpers accumulate.
67
+
68
+ The channel is a dict whose numeric fields each sum via ``add``, so many
69
+ nodes (or many rounds) fold their spend together. Fields mirror
70
+ :class:`~agentflow.events.Usage`: ``input_tokens``, ``output_tokens``,
71
+ ``total_tokens``, ``cost_usd``.
72
+ """
73
+
74
+ budget: Annotated[
75
+ dict,
76
+ by_key(
77
+ {
78
+ "input_tokens": add,
79
+ "output_tokens": add,
80
+ "total_tokens": add,
81
+ "cost_usd": add,
82
+ }
83
+ ),
84
+ ]
85
+
86
+
87
+ def record_usage(usage: Usage | None) -> dict[str, Any]:
88
+ """Return a state update that adds ``usage`` into the ``budget`` channel.
89
+
90
+ A ``None`` usage (a backend that reports no counts) contributes nothing —
91
+ the returned update is empty, so a node can unconditionally
92
+ ``return {**result, **record_usage(turn.usage)}``. Only the fields the
93
+ backend actually reported are added; a missing field is treated as zero and
94
+ omitted, so it does not fabricate a total.
95
+ """
96
+ if usage is None:
97
+ return {}
98
+ fields: dict[str, Any] = {}
99
+ if usage.input_tokens is not None:
100
+ fields["input_tokens"] = usage.input_tokens
101
+ if usage.output_tokens is not None:
102
+ fields["output_tokens"] = usage.output_tokens
103
+ # total: prefer the reported total, else derive from input+output if present
104
+ if usage.total_tokens is not None:
105
+ fields["total_tokens"] = usage.total_tokens
106
+ elif usage.input_tokens is not None or usage.output_tokens is not None:
107
+ fields["total_tokens"] = (usage.input_tokens or 0) + (usage.output_tokens or 0)
108
+ if usage.cost_usd is not None:
109
+ fields["cost_usd"] = usage.cost_usd
110
+ if not fields:
111
+ return {}
112
+ return {"budget": fields}
113
+
114
+
115
+ def over_budget(
116
+ state: Any,
117
+ *,
118
+ max_tokens: int | None = None,
119
+ max_cost_usd: float | None = None,
120
+ ) -> bool:
121
+ """True when accumulated spend has reached or passed a given cap.
122
+
123
+ Checks whichever caps are provided: ``max_tokens`` against the summed
124
+ ``total_tokens``, ``max_cost_usd`` against the summed ``cost_usd``. With no
125
+ caps given it is always ``False`` (nothing to enforce). Spend not yet
126
+ recorded (a backend that reports nothing) reads as zero, so a token cap can
127
+ only fire when some backend actually surfaced counts.
128
+ """
129
+ budget = state.get("budget") or {}
130
+ tokens_over = max_tokens is not None and budget.get("total_tokens", 0) >= max_tokens
131
+ cost_over = max_cost_usd is not None and budget.get("cost_usd", 0.0) >= max_cost_usd
132
+ return bool(tokens_over or cost_over)
133
+
134
+
135
+ class TokenMeter(Hooks):
136
+ """A Hooks listener that accumulates token/cost usage across a run.
137
+
138
+ Attach at ``compile(hooks=...)``. It sums the usage on every ``TurnEnd`` a
139
+ node surfaces via ``ctx.emit``. Pure observation for logging or a metrics
140
+ readout — it does not and cannot stop a run (hooks must never change
141
+ behavior). To stop on a cap, use :func:`over_budget` in a router.
142
+ """
143
+
144
+ def __init__(self) -> None:
145
+ self.input_tokens = 0
146
+ self.output_tokens = 0
147
+ self.total_tokens = 0
148
+ self.cost_usd = 0.0
149
+ self.turns = 0
150
+
151
+ async def on_event(self, thread: str, step: int, node: str, event: Any) -> None:
152
+ usage = getattr(event, "usage", None)
153
+ if not isinstance(usage, Usage):
154
+ return
155
+ self.turns += 1
156
+ if usage.input_tokens:
157
+ self.input_tokens += usage.input_tokens
158
+ if usage.output_tokens:
159
+ self.output_tokens += usage.output_tokens
160
+ if usage.total_tokens is not None:
161
+ self.total_tokens += usage.total_tokens
162
+ elif usage.input_tokens or usage.output_tokens:
163
+ self.total_tokens += (usage.input_tokens or 0) + (usage.output_tokens or 0)
164
+ if usage.cost_usd:
165
+ self.cost_usd += usage.cost_usd
166
+
167
+ def summary(self) -> dict[str, Any]:
168
+ return {
169
+ "turns": self.turns,
170
+ "input_tokens": self.input_tokens,
171
+ "output_tokens": self.output_tokens,
172
+ "total_tokens": self.total_tokens,
173
+ "cost_usd": round(self.cost_usd, 6),
174
+ }
@@ -81,6 +81,11 @@ class GateResult:
81
81
  ``passed`` is the only field the router requires. ``score`` and ``reasons``
82
82
  are for observability and human review; ``feedback`` is the actionable text
83
83
  a revise node can consume to improve the artifact on the next attempt.
84
+
85
+ The gate stores this verdict in state as a plain dict (via :meth:`to_dict`),
86
+ not as this object, so it is JSON-serializable and survives a durable
87
+ checkpointer. Read it from ``state["gate_results"][name]`` as a mapping:
88
+ ``result["passed"]``, ``result["feedback"]``, and so on.
84
89
  """
85
90
 
86
91
  passed: bool
@@ -88,6 +93,15 @@ class GateResult:
88
93
  reasons: list[str] = field(default_factory=list)
89
94
  feedback: str | None = None
90
95
 
96
+ def to_dict(self) -> dict[str, Any]:
97
+ """A JSON-serializable view, as stored in the ``gate_results`` channel."""
98
+ return {
99
+ "passed": self.passed,
100
+ "score": self.score,
101
+ "reasons": list(self.reasons),
102
+ "feedback": self.feedback,
103
+ }
104
+
91
105
 
92
106
  class GateState(State):
93
107
  """Mix into a state schema to add the channels quality gates need.
@@ -99,7 +113,10 @@ class GateState(State):
99
113
 
100
114
  #: Attempt count per gate name.
101
115
  gate_attempts: Annotated[dict, merge]
102
- #: The most recent :class:`GateResult` per gate name.
116
+ #: The most recent verdict per gate name, stored as a plain dict
117
+ #: (``{"passed", "score", "reasons", "feedback"}``) so it is
118
+ #: JSON-serializable and survives a durable checkpointer. Read it as a
119
+ #: mapping — ``state["gate_results"]["name"]["passed"]``.
103
120
  gate_results: Annotated[dict, last]
104
121
 
105
122
 
@@ -173,15 +190,15 @@ def add_quality_gate(
173
190
  )
174
191
 
175
192
  results = dict(state.get("gate_results") or {})
176
- results[name] = result
193
+ results[name] = result.to_dict() # store a plain dict, not the dataclass
177
194
  return {"gate_attempts": {name: attempt}, "gate_results": results}
178
195
 
179
196
  def route(state: Mapping[str, Any]) -> str:
180
197
  results = state.get("gate_results") or {}
181
- result: GateResult | None = results.get(name)
198
+ result: dict[str, Any] | None = results.get(name)
182
199
  attempts = state.get("gate_attempts") or {}
183
200
  attempt = int(attempts.get(name, 0))
184
- if result is not None and result.passed:
201
+ if result is not None and result["passed"]:
185
202
  return "pass"
186
203
  if attempt >= max_attempts:
187
204
  return "escalate"