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.
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/CHANGELOG.md +68 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/PKG-INFO +33 -3
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/README.md +32 -2
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/__init__.py +5 -1
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/backends/cli_exec.py +5 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/backends/codex.py +9 -1
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/backends/ollama.py +10 -1
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/events.py +24 -2
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/prebuilt/__init__.py +33 -0
- agent_workflow_sdk-0.6.0/agentflow/prebuilt/budget.py +174 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/prebuilt/gate.py +21 -4
- agent_workflow_sdk-0.6.0/agentflow/prebuilt/plan_executor.py +388 -0
- agent_workflow_sdk-0.6.0/agentflow/prebuilt/scoring.py +208 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/state.py +60 -0
- agent_workflow_sdk-0.6.0/design-notes/eval-harness.md +142 -0
- agent_workflow_sdk-0.6.0/design-notes/plan-executor.md +206 -0
- agent_workflow_sdk-0.6.0/design-notes/token-budget.md +149 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/concepts/state.md +29 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/durability/quality-gates.md +4 -4
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/patterns/long-running-loops.md +105 -6
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/patterns/prebuilt.md +1 -1
- agent_workflow_sdk-0.6.0/docs/reference/api/budget.md +13 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/reference/api/events.md +1 -0
- agent_workflow_sdk-0.6.0/docs/reference/api/plan-executor.md +14 -0
- agent_workflow_sdk-0.6.0/docs/reference/api/scoring.md +12 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/reference/api/state.md +1 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/reference/api.md +9 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/reference/glossary.md +1 -1
- agent_workflow_sdk-0.6.0/docs/tutorials/index.md +9 -0
- agent_workflow_sdk-0.6.0/docs/tutorials/optimizer/01-first-graph.md +83 -0
- agent_workflow_sdk-0.6.0/docs/tutorials/optimizer/02-branching.md +81 -0
- agent_workflow_sdk-0.6.0/docs/tutorials/optimizer/03-parallel.md +96 -0
- agent_workflow_sdk-0.6.0/docs/tutorials/optimizer/04-dual-state.md +82 -0
- agent_workflow_sdk-0.6.0/docs/tutorials/optimizer/05-backend.md +93 -0
- agent_workflow_sdk-0.6.0/docs/tutorials/optimizer/06-gate.md +87 -0
- agent_workflow_sdk-0.6.0/docs/tutorials/optimizer/07-human.md +70 -0
- agent_workflow_sdk-0.6.0/docs/tutorials/optimizer/08-durability.md +76 -0
- agent_workflow_sdk-0.6.0/docs/tutorials/optimizer/09-watch.md +70 -0
- agent_workflow_sdk-0.6.0/docs/tutorials/optimizer/10-plan-executor.md +91 -0
- agent_workflow_sdk-0.6.0/docs/tutorials/optimizer/index.md +28 -0
- 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
- agent_workflow_sdk-0.6.0/docs/tutorials/query-optimizer/01-first-graph.md +58 -0
- agent_workflow_sdk-0.6.0/docs/tutorials/query-optimizer/02-branching.md +51 -0
- agent_workflow_sdk-0.6.0/docs/tutorials/query-optimizer/03-parallel.md +35 -0
- agent_workflow_sdk-0.6.0/docs/tutorials/query-optimizer/04-dual-state.md +46 -0
- agent_workflow_sdk-0.6.0/docs/tutorials/query-optimizer/05-agent.md +69 -0
- agent_workflow_sdk-0.6.0/docs/tutorials/query-optimizer/06-gate.md +43 -0
- agent_workflow_sdk-0.6.0/docs/tutorials/query-optimizer/07-human.md +45 -0
- agent_workflow_sdk-0.6.0/docs/tutorials/query-optimizer/08-durability.md +55 -0
- agent_workflow_sdk-0.6.0/docs/tutorials/query-optimizer/09-watch.md +39 -0
- agent_workflow_sdk-0.6.0/docs/tutorials/query-optimizer/10-plan-executor.md +80 -0
- agent_workflow_sdk-0.6.0/docs/tutorials/query-optimizer/index.md +66 -0
- agent_workflow_sdk-0.6.0/examples/dual_state_search.py +188 -0
- agent_workflow_sdk-0.6.0/examples/plan_executor_demo.py +108 -0
- agent_workflow_sdk-0.6.0/examples/tutorial/01_first_graph.py +95 -0
- agent_workflow_sdk-0.6.0/examples/tutorial/02_branching.py +107 -0
- agent_workflow_sdk-0.6.0/examples/tutorial/03_parallel.py +143 -0
- agent_workflow_sdk-0.6.0/examples/tutorial/04_dual_state.py +156 -0
- agent_workflow_sdk-0.6.0/examples/tutorial/05_backend.py +176 -0
- agent_workflow_sdk-0.6.0/examples/tutorial/06_gate.py +154 -0
- agent_workflow_sdk-0.6.0/examples/tutorial/07_human.py +169 -0
- agent_workflow_sdk-0.6.0/examples/tutorial/08_durability.py +196 -0
- agent_workflow_sdk-0.6.0/examples/tutorial/09_watch.py +122 -0
- agent_workflow_sdk-0.6.0/examples/tutorial/10_plan_executor.py +164 -0
- agent_workflow_sdk-0.6.0/examples/tutorial/queryopt/__init__.py +20 -0
- agent_workflow_sdk-0.6.0/examples/tutorial/queryopt/ch01_first_graph.py +81 -0
- agent_workflow_sdk-0.6.0/examples/tutorial/queryopt/ch02_branching.py +90 -0
- agent_workflow_sdk-0.6.0/examples/tutorial/queryopt/ch03_parallel.py +93 -0
- agent_workflow_sdk-0.6.0/examples/tutorial/queryopt/ch04_dual_state.py +110 -0
- agent_workflow_sdk-0.6.0/examples/tutorial/queryopt/ch05_agent.py +100 -0
- agent_workflow_sdk-0.6.0/examples/tutorial/queryopt/ch06_gate.py +102 -0
- agent_workflow_sdk-0.6.0/examples/tutorial/queryopt/ch07_human.py +107 -0
- agent_workflow_sdk-0.6.0/examples/tutorial/queryopt/ch08_durability.py +120 -0
- agent_workflow_sdk-0.6.0/examples/tutorial/queryopt/ch09_watch.py +100 -0
- agent_workflow_sdk-0.6.0/examples/tutorial/queryopt/ch10_plan_executor.py +147 -0
- agent_workflow_sdk-0.6.0/examples/tutorial/queryopt/db.py +137 -0
- agent_workflow_sdk-0.6.0/examples/tutorial/queryopt/proposer.py +143 -0
- agent_workflow_sdk-0.6.0/examples/tutorial/queryopt/scorer.py +154 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/mkdocs.yml +30 -1
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/pyproject.toml +1 -1
- agent_workflow_sdk-0.6.0/tests/test_budget.py +175 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_gate.py +35 -5
- agent_workflow_sdk-0.6.0/tests/test_plan_executor.py +379 -0
- agent_workflow_sdk-0.6.0/tests/test_reducers.py +168 -0
- agent_workflow_sdk-0.6.0/tests/test_scoring.py +146 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/.env.example +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/.github/workflows/ci.yml +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/.github/workflows/docs.yml +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/.github/workflows/release.yml +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/.gitignore +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/CONTRIBUTING.md +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/DESIGN.md +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/LICENSE +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/NOTICE +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/SECURITY.md +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/_pg.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/backends/__init__.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/backends/_http.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/backends/anthropic.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/backends/base.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/backends/claude_code.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/backends/kiro.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/backends/openai.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/checkpoint/__init__.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/checkpoint/_serde.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/checkpoint/base.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/checkpoint/file.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/checkpoint/memory.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/checkpoint/postgres.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/checkpoint/redis.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/checkpoint/sqlite.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/compiled.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/controlplane/__init__.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/controlplane/memory.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/controlplane/postgres.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/controlplane/queue.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/controlplane/records.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/controlplane/registry.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/controlplane/worker.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/errors.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/graph.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/interrupts.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/observability.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/otel.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/prebuilt/effects.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/prebuilt/idempotent.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/prebuilt/loop.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/prebuilt/resilience.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/prebuilt/tool_loop.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/prebuilt/watch.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/prometheus.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/py.typed +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/redaction.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/runtime.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/server/__init__.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/server/app.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/store/__init__.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/store/_util.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/store/base.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/store/memory.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/store/postgres.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/agentflow/telemetry.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/backends/agent-backends.md +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/backends/llm-backends.md +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/backends/overview.md +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/backends/permissions.md +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/backends/retries.md +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/concepts/context.md +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/concepts/execution.md +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/concepts/nodes-and-edges.md +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/concepts/running.md +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/concepts/subgraphs.md +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/durability/checkpointing.md +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/durability/control-plane.md +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/durability/human-in-the-loop.md +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/durability/observability.md +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/durability/store.md +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/get-started/installation.md +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/get-started/mental-model.md +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/get-started/overview.md +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/get-started/quickstart.md +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/index.md +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/reference/api/backends.md +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/reference/api/checkpoint.md +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/reference/api/compiled.md +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/reference/api/controlplane.md +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/reference/api/errors.md +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/reference/api/gate.md +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/reference/api/graph.md +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/reference/api/interrupts.md +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/reference/api/observability.md +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/reference/api/permissions.md +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/reference/api/prebuilt.md +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/reference/api/runtime.md +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/reference/api/server.md +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/reference/api/store.md +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/docs/reference/api/watch.md +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/examples/agents_demo.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/examples/codebase_qa_ollama.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/examples/hello_graph.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/examples/hitl_permission.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/examples/idempotent_effect.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/examples/mixed_backends.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/examples/optimize_loop.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/examples/quality_gate.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/examples/research_plan_implement_review.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/examples/serve_control_plane.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/examples/telemetry_demo.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/examples/tool_loop_ollama.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/examples/watch_until_done.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/conftest.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_anthropic_backend.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_backend_lifecycle.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_checkpoint_contract.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_cli_backends.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_concurrency.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_controlplane.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_effects.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_graph_engine.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_hitl_permission.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_http_retry.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_idempotent.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_input_validation.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_kiro_backend.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_live_backends.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_observability.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_ollama_backend.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_openai_backend.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_otel.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_permission_policy.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_postgres_checkpointer.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_postgres_runqueue.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_postgres_store.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_prebuilt_loop.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_prebuilt_no_httpx.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_prometheus.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_provider_resilience.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_redaction.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_redis_checkpointer.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_resilience.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_run_timeout.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_server.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_sqlite_checkpointer.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_state_isolation.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_step_atomicity.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_store.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_subgraph.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_sync_facade.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_telemetry.py +0 -0
- {agent_workflow_sdk-0.4.0 → agent_workflow_sdk-0.6.0}/tests/test_tool_loop.py +0 -0
- {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.
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
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.
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
|
201
|
+
if result is not None and result["passed"]:
|
|
185
202
|
return "pass"
|
|
186
203
|
if attempt >= max_attempts:
|
|
187
204
|
return "escalate"
|