nighthawk-python 0.10.0__tar.gz → 0.12.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 (182) hide show
  1. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/.claude/rules/docs.md +2 -2
  2. nighthawk_python-0.12.0/.devcontainer/devcontainer-lock.json +14 -0
  3. nighthawk_python-0.12.0/.devcontainer/devcontainer.json +16 -0
  4. nighthawk_python-0.12.0/.devcontainer/postCreateCommand.sh +16 -0
  5. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/.github/dependabot.yml +6 -0
  6. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/.github/workflows/docs.yml +1 -1
  7. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/CHANGELOG.md +28 -1
  8. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/CONTRIBUTING.md +12 -7
  9. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/PKG-INFO +13 -13
  10. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/README.md +3 -3
  11. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/docs/api.md +3 -0
  12. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/docs/coding-agent-backends.md +13 -0
  13. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/docs/for-coding-agents.md +1 -0
  14. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/docs/index.md +4 -4
  15. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/docs/natural-blocks.md +39 -0
  16. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/docs/patterns.md +21 -9
  17. nighthawk_python-0.12.0/docs/philosophy.md +196 -0
  18. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/docs/quickstart.md +1 -3
  19. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/docs/roadmap.md +8 -0
  20. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/docs/runtime-configuration.md +35 -8
  21. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/docs/specification.md +56 -14
  22. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/docs/verification.md +21 -12
  23. nighthawk_python-0.12.0/evals/promptfoo/evidence/2026-04-27-runtime-prompt-optimization.md +77 -0
  24. nighthawk_python-0.12.0/evals/promptfoo/promptfooconfig-nano-sanity.yaml +39 -0
  25. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/evals/promptfoo/promptfooconfig-prompt-ab.yaml +9 -9
  26. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/evals/promptfoo/promptfooconfig-suffix-ab.yaml +6 -6
  27. nighthawk_python-0.12.0/evals/promptfoo/prompts/eval_default.txt +25 -0
  28. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/evals/promptfoo/provider.py +5 -5
  29. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/evals/promptfoo/research-result.md +1 -0
  30. nighthawk_python-0.12.0/evals/promptfoo/test_cases/loop_outcomes_ja.yaml +18 -0
  31. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/pyproject.toml +19 -18
  32. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/src/nighthawk/__init__.py +15 -1
  33. nighthawk_python-0.12.0/src/nighthawk/backends/_text_fallback.py +141 -0
  34. nighthawk_python-0.12.0/src/nighthawk/backends/base.py +274 -0
  35. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/src/nighthawk/backends/claude_code_cli.py +17 -6
  36. nighthawk_python-0.12.0/src/nighthawk/backends/claude_code_sdk.py +306 -0
  37. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/src/nighthawk/backends/codex.py +22 -9
  38. nighthawk_python-0.12.0/src/nighthawk/backends/mcp_boundary.py +295 -0
  39. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/src/nighthawk/backends/mcp_server.py +44 -43
  40. nighthawk_python-0.12.0/src/nighthawk/backends/text_projection.py +352 -0
  41. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/src/nighthawk/backends/tool_bridge.py +100 -68
  42. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/src/nighthawk/configuration.py +9 -3
  43. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/src/nighthawk/json_renderer.py +17 -0
  44. nighthawk_python-0.12.0/src/nighthawk/runtime/_user_content.py +101 -0
  45. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/src/nighthawk/runtime/prompt.py +328 -116
  46. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/src/nighthawk/runtime/runner.py +2 -2
  47. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/src/nighthawk/runtime/scoping.py +47 -3
  48. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/src/nighthawk/runtime/step_context.py +7 -0
  49. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/src/nighthawk/runtime/step_contract.py +13 -4
  50. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/src/nighthawk/runtime/step_executor.py +19 -8
  51. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/src/nighthawk/runtime/tool_calls.py +9 -6
  52. nighthawk_python-0.12.0/src/nighthawk/tools/_tool_return_content.py +66 -0
  53. nighthawk_python-0.12.0/src/nighthawk/tools/contracts.py +195 -0
  54. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/src/nighthawk/tools/execution.py +93 -14
  55. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/tests/backends/test_codex.py +126 -3
  56. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/tests/docs/test_docs_architecture.py +28 -0
  57. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/tests/docs/test_prompt_examples.py +7 -4
  58. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/tests/execution/prompt_test_helpers.py +50 -1
  59. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/tests/execution/test_execution_outcome_prompt_fragment.py +16 -3
  60. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/tests/execution/test_globals_prompt.py +41 -7
  61. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/tests/execution/test_runtime.py +119 -3
  62. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/tests/execution/test_variables_prompt.py +520 -3
  63. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/tests/governance/test_governance.py +1 -1
  64. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/tests/integration/test_llm_integration.py +159 -0
  65. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/tests/public/test_public_api.py +67 -15
  66. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/tests/resilience/test_budget.py +1 -1
  67. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/tests/resilience/test_circuit_breaker.py +1 -1
  68. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/tests/resilience/test_retry.py +1 -1
  69. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/tests/resilience/test_timeout.py +1 -1
  70. nighthawk_python-0.12.0/tests/tools/test_contracts.py +202 -0
  71. nighthawk_python-0.12.0/tests/tools/test_multimodal_helpers.py +167 -0
  72. nighthawk_python-0.12.0/tests/tools/test_tool_boundary.py +1897 -0
  73. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/uv.lock +371 -377
  74. nighthawk_python-0.10.0/.devcontainer/Dockerfile.devcontainer +0 -3
  75. nighthawk_python-0.10.0/.devcontainer/Dockerfile.litellm +0 -11
  76. nighthawk_python-0.10.0/.devcontainer/devcontainer.json +0 -24
  77. nighthawk_python-0.10.0/.devcontainer/docker-compose.yaml +0 -19
  78. nighthawk_python-0.10.0/.devcontainer/litellm-config.yaml +0 -37
  79. nighthawk_python-0.10.0/docs/philosophy.md +0 -178
  80. nighthawk_python-0.10.0/src/nighthawk/backends/base.py +0 -129
  81. nighthawk_python-0.10.0/src/nighthawk/backends/claude_code_sdk.py +0 -289
  82. nighthawk_python-0.10.0/src/nighthawk/backends/mcp_boundary.py +0 -129
  83. nighthawk_python-0.10.0/src/nighthawk/tools/contracts.py +0 -71
  84. nighthawk_python-0.10.0/tests/tools/test_contracts.py +0 -106
  85. nighthawk_python-0.10.0/tests/tools/test_tool_boundary.py +0 -513
  86. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/.claude/rules/promptfoo.md +0 -0
  87. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/.claude/rules/src.md +0 -0
  88. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/.claude/rules/tests.md +0 -0
  89. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/.claude/settings.json +0 -0
  90. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/.claude/unset_envs.sh +0 -0
  91. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/.github/workflows/ci.yml +0 -0
  92. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/.github/workflows/publish.yml +0 -0
  93. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/.gitignore +0 -0
  94. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/.python-version +0 -0
  95. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/AGENTS.md +0 -0
  96. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/CLAUDE.md +0 -0
  97. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/LICENSE +0 -0
  98. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/docs/AGENTS.md +0 -0
  99. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/docs/assets/nighthawk_logo-128x128.png +0 -0
  100. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/docs/executors.md +0 -0
  101. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/docs/pydantic-ai-providers.md +0 -0
  102. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/evals/promptfoo/assertions/__init__.py +0 -0
  103. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/evals/promptfoo/assertions/binding_value.py +0 -0
  104. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/evals/promptfoo/assertions/outcome_kind.py +0 -0
  105. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/evals/promptfoo/assertions/raise_message.py +0 -0
  106. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/evals/promptfoo/evidence/2026-03-26-baseline-prompt-ab.md +0 -0
  107. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/evals/promptfoo/evidence/2026-03-26-baseline-regression.md +0 -0
  108. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/evals/promptfoo/evidence/2026-03-26-baseline-suffix-ab.md +0 -0
  109. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/evals/promptfoo/promptfooconfig-agents.yaml +0 -0
  110. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/evals/promptfoo/promptfooconfig.yaml +0 -0
  111. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/evals/promptfoo/prompts/eval_coding_agent.txt +0 -0
  112. /nighthawk_python-0.10.0/evals/promptfoo/prompts/eval_default.txt → /nighthawk_python-0.12.0/evals/promptfoo/prompts/eval_legacy_default.txt +0 -0
  113. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/evals/promptfoo/prompts/eval_mutation_aware.txt +0 -0
  114. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/evals/promptfoo/prompts/eval_sequenced.txt +0 -0
  115. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/evals/promptfoo/test_cases/binding_operations.yaml +0 -0
  116. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/evals/promptfoo/test_cases/edge_cases.yaml +0 -0
  117. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/evals/promptfoo/test_cases/loop_outcomes.yaml +0 -0
  118. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/evals/promptfoo/test_cases/multi_step.yaml +0 -0
  119. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/evals/promptfoo/test_cases/null_handling.yaml +0 -0
  120. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/evals/promptfoo/test_cases/outcome_kinds.yaml +0 -0
  121. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/evals/promptfoo/test_cases/tool_selection.yaml +0 -0
  122. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/mkdocs.yml +0 -0
  123. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/pyrightconfig.json +0 -0
  124. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/src/AGENTS.md +0 -0
  125. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/src/nighthawk/backends/__init__.py +0 -0
  126. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/src/nighthawk/backends/claude_code_settings.py +0 -0
  127. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/src/nighthawk/errors.py +0 -0
  128. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/src/nighthawk/identifier_path.py +0 -0
  129. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/src/nighthawk/natural/__init__.py +0 -0
  130. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/src/nighthawk/natural/blocks.py +0 -0
  131. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/src/nighthawk/natural/decorator.py +0 -0
  132. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/src/nighthawk/natural/transform.py +0 -0
  133. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/src/nighthawk/oversight.py +0 -0
  134. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/src/nighthawk/resilience/__init__.py +0 -0
  135. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/src/nighthawk/resilience/_budget.py +0 -0
  136. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/src/nighthawk/resilience/_circuit_breaker.py +0 -0
  137. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/src/nighthawk/resilience/_fallback.py +0 -0
  138. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/src/nighthawk/resilience/_retry.py +0 -0
  139. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/src/nighthawk/resilience/_timeout.py +0 -0
  140. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/src/nighthawk/resilience/_vote.py +0 -0
  141. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/src/nighthawk/runtime/__init__.py +0 -0
  142. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/src/nighthawk/runtime/async_bridge.py +0 -0
  143. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/src/nighthawk/testing.py +0 -0
  144. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/src/nighthawk/tools/__init__.py +0 -0
  145. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/src/nighthawk/tools/assignment.py +0 -0
  146. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/src/nighthawk/tools/provided.py +0 -0
  147. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/src/nighthawk/tools/registry.py +0 -0
  148. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/src/nighthawk/ulid.py +0 -0
  149. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/tests/AGENTS.md +0 -0
  150. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/tests/__init__.py +0 -0
  151. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/tests/backends/__init__.py +0 -0
  152. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/tests/backends/test_claude_code_cli.py +0 -0
  153. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/tests/backends/test_claude_code_sdk.py +0 -0
  154. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/tests/conftest.py +0 -0
  155. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/tests/docs/__init__.py +0 -0
  156. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/tests/docs/test_coding_agent_examples.py +0 -0
  157. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/tests/execution/__init__.py +0 -0
  158. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/tests/execution/stub_executor.py +0 -0
  159. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/tests/execution/test_infer_binding_types.py +0 -0
  160. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/tests/execution/test_natural_block_ordering.py +0 -0
  161. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/tests/execution/test_natural_traceback.py +0 -0
  162. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/tests/integration/__init__.py +0 -0
  163. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/tests/integration/skip_helpers.py +0 -0
  164. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/tests/integration/test_carry_pattern.py +0 -0
  165. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/tests/integration/test_claude_code_cli_integration.py +0 -0
  166. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/tests/integration/test_claude_code_sdk_integration.py +0 -0
  167. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/tests/integration/test_codex_integration.py +0 -0
  168. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/tests/natural/__init__.py +0 -0
  169. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/tests/natural/test_blocks.py +0 -0
  170. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/tests/public/__init__.py +0 -0
  171. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/tests/public/test_readme_example.py +0 -0
  172. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/tests/public/test_usage_meter.py +0 -0
  173. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/tests/resilience/__init__.py +0 -0
  174. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/tests/resilience/test_composition.py +0 -0
  175. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/tests/resilience/test_fallback.py +0 -0
  176. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/tests/resilience/test_vote.py +0 -0
  177. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/tests/test_renderer.py +0 -0
  178. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/tests/test_testing.py +0 -0
  179. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/tests/tools/__init__.py +0 -0
  180. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/tests/tools/test_assignment_async.py +0 -0
  181. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/tests/tools/test_provided_async.py +0 -0
  182. {nighthawk_python-0.10.0 → nighthawk_python-0.12.0}/tests/tools/test_registry.py +0 -0
@@ -39,7 +39,7 @@ Public API documentation layering:
39
39
  | `coding-agent-backends.md` | Backend users | Backend reference | Backend-specific settings, shared capabilities, skills, MCP tool exposure, working directory, troubleshooting. |
40
40
  | `for-coding-agents.md` | Coding agents (LLMs) | Operational guide | Condensed, decision-oriented rules derived from human-oriented docs. Self-contained with absolute URLs. |
41
41
  | `specification.md` | Implementors | Canonical spec | Syntax, state layers, tools, outcomes, frontmatter, runtime semantics, observability contract, and custom backend capability/protocol semantics. Numbered section headings. |
42
- | `philosophy.md` | Evaluators | Design rationale | Execution model, harness evidence, design consequences (resilience, scoped execution contexts, tool exposure, multi-agent coordination, tradeoffs), runtime evaluation rationale, design landscape. |
42
+ | `philosophy.md` | Evaluators | Design rationale | Execution model, design landscape, boundary-first consequences, trust and safety boundaries, tradeoffs, and runtime evaluation rationale. |
43
43
  | `api.md` | Developers | API reference | Auto-generated from docstrings, including protocol and extension-hook symbols. |
44
44
  | `roadmap.md` | Contributors | Future directions | Ideas only. Remove when implemented. |
45
45
  | `docs/AGENTS.md` | Coding agents editing docs | Documentation governance | Canonical ownership, page roles, routing rules, and docs test invariants. Symlink to `.claude/rules/docs.md`. |
@@ -171,7 +171,7 @@ Pages in the Getting started, Patterns & verification, and Configuration nav gro
171
171
 
172
172
  **`philosophy.md`**
173
173
 
174
- - Owns the cumulative argument: execution model, harness evidence, design consequences (resilience, scoped execution contexts, tool exposure, multi-agent coordination, tradeoffs), runtime evaluation rationale, and design landscape.
174
+ - Owns the cumulative argument: execution model, design landscape, boundary-first consequences (resilience, scoped execution contexts, tool exposure, multi-agent coordination), trust and safety boundaries, tradeoffs, and runtime evaluation rationale.
175
175
  - External references acceptable. Prefer stable URLs with enough inline context to survive link rot.
176
176
  - No how-to code examples for patterns in `natural-blocks.md` or `patterns.md`. Exception: positioning examples may reuse function names from those pages.
177
177
 
@@ -0,0 +1,14 @@
1
+ {
2
+ "features": {
3
+ "ghcr.io/devcontainers/features/docker-in-docker:4.1.0": {
4
+ "version": "4.1.0",
5
+ "resolved": "ghcr.io/devcontainers/features/docker-in-docker@sha256:7d979c4a36d595eb4ab79909caa3f396daf3b6152dcf38e8328ea4eff42206c7",
6
+ "integrity": "sha256:7d979c4a36d595eb4ab79909caa3f396daf3b6152dcf38e8328ea4eff42206c7"
7
+ },
8
+ "ghcr.io/devcontainers/features/node:2.1.0": {
9
+ "version": "2.1.0",
10
+ "resolved": "ghcr.io/devcontainers/features/node@sha256:586c9a6f7dd40bd3ba2cd41e7f2f88dcc31fbe5d1442afcbf07ffbc66b686857",
11
+ "integrity": "sha256:586c9a6f7dd40bd3ba2cd41e7f2f88dcc31fbe5d1442afcbf07ffbc66b686857"
12
+ }
13
+ }
14
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "Python 3.13",
3
+ "image": "mcr.microsoft.com/devcontainers/python:3-3.13-bookworm",
4
+ "features": {
5
+ "ghcr.io/devcontainers/features/docker-in-docker:4.1.0": {},
6
+ "ghcr.io/devcontainers/features/node:2.1.0": {}
7
+ },
8
+ "postCreateCommand": "/bin/sh .devcontainer/postCreateCommand.sh",
9
+ "customizations": {
10
+ "vscode": {
11
+ "extensions": [
12
+ "charliermarsh.ruff"
13
+ ]
14
+ }
15
+ }
16
+ }
@@ -0,0 +1,16 @@
1
+ #!/bin/sh
2
+ set -eu
3
+
4
+ sudo apt-get update && export DEBIAN_FRONTEND=noninteractive \
5
+ && sudo apt-get -y install --no-install-recommends ripgrep fzf bubblewrap \
6
+ && sudo apt-get clean \
7
+ && sudo rm -rf /var/lib/apt/lists/*
8
+
9
+ curl -LsSf https://astral.sh/uv/install.sh | sh
10
+
11
+ uv sync
12
+
13
+ npm install -g pyright
14
+
15
+ curl -LsSf https://chatgpt.com/codex/install.sh | CODEX_NON_INTERACTIVE=1 sh
16
+ curl -LsSf https://claude.ai/install.sh | bash
@@ -6,6 +6,12 @@
6
6
 
7
7
  version: 2
8
8
  updates:
9
+ - package-ecosystem: "uv"
10
+ directory: "/"
11
+ schedule:
12
+ interval: weekly
13
+ cooldown:
14
+ default-days: 7
9
15
  - package-ecosystem: "devcontainers"
10
16
  directory: "/"
11
17
  schedule:
@@ -34,7 +34,7 @@ jobs:
34
34
  - name: Build docs
35
35
  run: uv run mkdocs build
36
36
 
37
- - uses: actions/upload-pages-artifact@v4
37
+ - uses: actions/upload-pages-artifact@v5
38
38
  with:
39
39
  path: site
40
40
 
@@ -7,6 +7,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.12.0]
11
+
12
+ ### Changed
13
+ - Clarified the boundary-first philosophy page.
14
+ - Clarified multimodal documentation and specification.
15
+ - Raised the minimum Pydantic AI version to 2.37.0 and migrated coding-agent backends to its native-tool and structured-instruction APIs.
16
+ - Raised the MCP minimum version to 2.1.1 and migrated the embedded tool server and content types to the MCP 2 API.
17
+ - Updated the Claude Agent SDK integration to 0.2.151.
18
+
19
+ ## [0.11.0]
20
+
21
+ ### Added
22
+ - Native Pydantic AI multimodal binding support for Natural block prompts, including `BinaryContent`, URL-based media, `UploadedFile`, and explicit dotted multimodal references.
23
+ - Multimodal tool-result transport for coding-agent backends, preserving mixed text/media order and carrying MCP image/audio content natively where supported.
24
+ - Public scope snapshot getters: `get_implicit_references`, `get_system_prompt_suffix_fragments`, and `get_user_prompt_suffix_fragments`.
25
+
26
+ ### Changed
27
+ - Coding-agent backends now text-project multimodal user prompts into local file paths or URL placeholders while provider-backed executors pass `UserContent` natively.
28
+ - Tool results now retain `ToolOutcome` payloads through backend boundaries so multimodal-capable transports can render native content before falling back to JSON previews.
29
+ - Optimize Natural block runtime prompt
30
+ - Update dependencies
31
+
32
+ ### Fixed
33
+ - Cleaned up staged multimodal prompt files when text projection fails before a projected request is returned.
34
+
10
35
  ## [0.10.0]
11
36
 
12
37
  ### Added
@@ -163,7 +188,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
163
188
  - Step executor abstraction and provider integration foundation.
164
189
  - Core documentation and project scaffolding.
165
190
 
166
- [Unreleased]: https://github.com/kurusugawa-computer/nighthawk-python/compare/v0.10.0...HEAD
191
+ [Unreleased]: https://github.com/kurusugawa-computer/nighthawk-python/compare/v0.12.0...HEAD
192
+ [0.12.0]: https://github.com/kurusugawa-computer/nighthawk-python/compare/v0.11.0...v0.12.0
193
+ [0.11.0]: https://github.com/kurusugawa-computer/nighthawk-python/compare/v0.10.0...v0.11.0
167
194
  [0.10.0]: https://github.com/kurusugawa-computer/nighthawk-python/compare/v0.9.0...v0.10.0
168
195
  [0.9.0]: https://github.com/kurusugawa-computer/nighthawk-python/compare/v0.8.0...v0.9.0
169
196
  [0.8.0]: https://github.com/kurusugawa-computer/nighthawk-python/compare/v0.7.0...v0.8.0
@@ -12,6 +12,14 @@
12
12
  uv sync --all-extras --all-groups
13
13
  ```
14
14
 
15
+ `uv` manages the project virtual environment in `.venv`. Do not create or
16
+ activate a separate virtual environment; run project commands through `uv run`.
17
+ The `.venv` directory is generated and ignored by Git. It can be deleted to
18
+ rebuild the local environment, and the next `uv sync` or `uv run` recreates it.
19
+
20
+ Declare project dependencies in `pyproject.toml` through `uv add`, rather than
21
+ installing them directly into `.venv` with `uv pip install`.
22
+
15
23
  If you see a `uv` warning about hardlinking (common in containers or cross-filesystem workspaces), suppress it with:
16
24
 
17
25
  ```bash
@@ -39,13 +47,8 @@ uv run pytest -q # quiet output
39
47
 
40
48
  ### Integration tests
41
49
 
42
- Integration tests require provider packages and API keys. Install the provider you need:
43
-
44
- ```bash
45
- uv pip install pydantic-ai-slim[openai]
46
- ```
47
-
48
- Then run with your `.env` loaded:
50
+ The full setup command installs the OpenAI provider development dependency.
51
+ Run integration tests with your `.env` loaded:
49
52
 
50
53
  ```bash
51
54
  set -a; source .env; set +a; uv run pytest -q
@@ -81,6 +84,7 @@ PFOO="cd evals/promptfoo && PROMPTFOO_PYTHON=\"$(uv python find)\" npx promptfoo
81
84
  |---|---|
82
85
  | `eval $PFOO eval` | Full regression (all backends, all tests) |
83
86
  | `eval $PFOO eval -c promptfooconfig-prompt-ab.yaml` | Prompt A/B test (gpt-5.4-mini only) |
87
+ | `eval $PFOO eval -c promptfooconfig-nano-sanity.yaml --no-cache` | Default-model sanity check (gpt-5.4-nano only) |
84
88
  | `eval $PFOO eval -c promptfooconfig-agents.yaml` | Coding agent backends (reduced test set) |
85
89
  | `eval $PFOO eval -c promptfooconfig.yaml --filter-pattern "P-BIND-001"` | Single test |
86
90
  | `eval $PFOO eval -c promptfooconfig.yaml --filter-providers "claude-code-cli"` | Single backend |
@@ -90,6 +94,7 @@ PFOO="cd evals/promptfoo && PROMPTFOO_PYTHON=\"$(uv python find)\" npx promptfoo
90
94
 
91
95
  - `promptfooconfig.yaml` — Regression: winner prompt combo across openai-responses, claude-code-cli, and codex backends. All test cases.
92
96
  - `promptfooconfig-prompt-ab.yaml` — A/B testing: 4 prompt/tool variants on gpt-5.4-mini. All test cases.
97
+ - `promptfooconfig-nano-sanity.yaml` — Default-model sanity check on openai-responses:gpt-5.4-nano. All test cases.
93
98
  - `promptfooconfig-agents.yaml` — Coding agent only: claude-code-cli and codex with reduced test set.
94
99
 
95
100
  #### Directory layout
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: nighthawk-python
3
- Version: 0.10.0
3
+ Version: 0.12.0
4
4
  Summary: A Python library where Python controls flow and LLMs or coding agents reason within constrained Natural blocks.
5
5
  Project-URL: Repository, https://github.com/kurusugawa-computer/nighthawk-python
6
6
  Project-URL: Documentation, https://kurusugawa-computer.github.io/nighthawk-python/
@@ -20,17 +20,17 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
20
  Classifier: Typing :: Typed
21
21
  Requires-Python: >=3.13
22
22
  Requires-Dist: headson>=0.16.1
23
- Requires-Dist: pydantic-ai-slim>=1.68
24
- Requires-Dist: pydantic>=2
25
- Requires-Dist: pyyaml>=6
26
- Requires-Dist: tenacity>=9
27
- Requires-Dist: tiktoken>=0.12
23
+ Requires-Dist: pydantic-ai-slim>=2.37.0
24
+ Requires-Dist: pydantic>=2.13.3
25
+ Requires-Dist: pyyaml>=6.0.3
26
+ Requires-Dist: tenacity>=9.1.4
27
+ Requires-Dist: tiktoken>=0.12.0
28
28
  Provides-Extra: claude-code-cli
29
- Requires-Dist: mcp>=1.26; extra == 'claude-code-cli'
29
+ Requires-Dist: mcp>=2.1.1; extra == 'claude-code-cli'
30
30
  Provides-Extra: claude-code-sdk
31
- Requires-Dist: claude-agent-sdk==0.1.48; extra == 'claude-code-sdk'
31
+ Requires-Dist: claude-agent-sdk==0.2.151; extra == 'claude-code-sdk'
32
32
  Provides-Extra: codex
33
- Requires-Dist: mcp>=1.26; extra == 'codex'
33
+ Requires-Dist: mcp>=2.1.1; extra == 'codex'
34
34
  Description-Content-Type: text/markdown
35
35
 
36
36
  [![PyPI](https://img.shields.io/pypi/v/nighthawk-python)](https://pypi.org/project/nighthawk-python)
@@ -68,12 +68,12 @@ For other providers, see [Pydantic AI providers](https://kurusugawa-computer.git
68
68
  ```py
69
69
  import nighthawk as nh
70
70
 
71
+
71
72
  def python_average(numbers):
72
73
  return sum(numbers) / len(numbers)
73
74
 
74
- step_executor = nh.AgentStepExecutor.from_configuration(
75
- configuration=nh.StepExecutorConfiguration(model="openai-responses:gpt-5.4-nano")
76
- )
75
+
76
+ step_executor = nh.AgentStepExecutor.from_configuration(configuration=nh.StepExecutorConfiguration(model="openai-responses:gpt-5.4-nano"))
77
77
 
78
78
  with nh.run(step_executor):
79
79
 
@@ -33,12 +33,12 @@ For other providers, see [Pydantic AI providers](https://kurusugawa-computer.git
33
33
  ```py
34
34
  import nighthawk as nh
35
35
 
36
+
36
37
  def python_average(numbers):
37
38
  return sum(numbers) / len(numbers)
38
39
 
39
- step_executor = nh.AgentStepExecutor.from_configuration(
40
- configuration=nh.StepExecutorConfiguration(model="openai-responses:gpt-5.4-nano")
41
- )
40
+
41
+ step_executor = nh.AgentStepExecutor.from_configuration(configuration=nh.StepExecutorConfiguration(model="openai-responses:gpt-5.4-nano"))
42
42
 
43
43
  with nh.run(step_executor):
44
44
 
@@ -22,7 +22,10 @@
22
22
  - get_current_step_context
23
23
  - get_current_usage_meter
24
24
  - get_execution_ref
25
+ - get_implicit_references
25
26
  - get_step_executor
27
+ - get_system_prompt_suffix_fragments
28
+ - get_user_prompt_suffix_fragments
26
29
  - resilience
27
30
  - UsageMeter
28
31
  - oversight
@@ -38,6 +38,18 @@ All three backends provide capabilities not available with Pydantic AI providers
38
38
  - **Callable discoverability:** Same rules as [Natural blocks](natural-blocks.md#functions-and-discoverability).
39
39
  - **Project-scoped files:** The CLI loads its own project files (CLAUDE.md, AGENTS.md) from `working_directory`.
40
40
 
41
+ ### Multimodal inputs
42
+
43
+ Coding agent backends do not receive native VLM-style user-prompt multimodal input. Nighthawk projects multimodal prompt content into the CLI text channel instead:
44
+
45
+ - `BinaryContent` is staged to a local file and referenced in the prompt with `<image>` / `<file>` plus a local file path.
46
+ - URL-based content (`ImageUrl`, `AudioUrl`, `DocumentUrl`, `VideoUrl`) is referenced by URL in the projected prompt text.
47
+ - `UploadedFile` user-prompt content is rejected because these backends cannot resolve provider-owned file identifiers. Tool results still preserve the rest of the payload and replace only the `UploadedFile` item with explanatory fallback text.
48
+ - Tool results cross the backend boundary as MCP content. Image (`BinaryContent.is_image`) and audio (`BinaryContent.is_audio`) items travel natively as `ImageContent` / `AudioContent`; all other `BinaryContent` and `FileUrl` items project to `TextContent` so the MCP path stays symmetric with the text-projected prompt path. Native MCP video carriage is intentionally unsupported for now because video-modal requirements are unstable; document carriage also remains text-projected unless a future backend advertises an explicit capability. When mixed top-level text/multimodal tool-result sequences preserve their original order they satisfy the same `UserContent` rule used for prompt hoisting. The user prompt itself remains text-projected for these backends.
49
+ - When a successful tool result splits to no text and no user content, the projected prompt stays header-only rather than inserting the preview JSON envelope.
50
+
51
+ This differs from provider-backed executors that can pass a `tuple[UserContent, ...]` to a multimodal-capable provider API directly.
52
+
41
53
  ### Working directory
42
54
 
43
55
  All three backends accept `working_directory` in their model settings. This absolute path determines:
@@ -269,6 +281,7 @@ Example Natural function that invokes the skill:
269
281
  ```py
270
282
  import nighthawk as nh
271
283
 
284
+
272
285
  @nh.natural_function
273
286
  def summarize_feedback(feedback_items: list[str]) -> str:
274
287
  def group_feedback_by_topic(feedback_items: list[str]) -> dict[str, list[str]]:
@@ -14,6 +14,7 @@ You are expected to write, review, and debug Python code that uses Nighthawk as
14
14
  - Prefer explicit, typed write bindings. Runtime inference exists for unannotated write bindings, but new code should not rely on it.
15
15
  - Keep outputs narrow and typed, especially when using coding agent backends. A block may do broad internal work, but the Python boundary should stay small.
16
16
  - When prompt context is truncated and you see `<snipped>`, first reduce context surface or split the block. Increase `StepContextLimits` only after simplification fails.
17
+ - Explicit dotted multimodal references such as `<holder.photo>` can still be omitted under tight `locals_max_items` / `locals_max_tokens`; resize budgets rather than assuming hoisting is unconditional.
17
18
 
18
19
  ## 2. First decision: should this be Natural at all?
19
20
 
@@ -14,12 +14,12 @@ The same mechanism handles lightweight LLM judgments ("classify this sentiment")
14
14
  ```py
15
15
  import nighthawk as nh
16
16
 
17
+
17
18
  def python_average(numbers):
18
19
  return sum(numbers) / len(numbers)
19
20
 
20
- step_executor = nh.AgentStepExecutor.from_configuration(
21
- configuration=nh.StepExecutorConfiguration(model="openai-responses:gpt-5.4-nano")
22
- )
21
+
22
+ step_executor = nh.AgentStepExecutor.from_configuration(configuration=nh.StepExecutorConfiguration(model="openai-responses:gpt-5.4-nano"))
23
23
 
24
24
  with nh.run(step_executor):
25
25
 
@@ -61,7 +61,7 @@ The LLM interprets the mixed-format input list via `<numbers>`, calls the Python
61
61
 
62
62
  ## Background and project
63
63
 
64
- - **[Philosophy](philosophy.md)** -- Design rationale: execution model, harness evidence, design consequences (resilience, scoped execution contexts, tool exposure, multi-agent coordination, tradeoffs), runtime evaluation rationale, and design landscape.
64
+ - **[Philosophy](philosophy.md)** -- Design rationale: execution model, design landscape, boundary-first consequences, trust and safety boundaries, tradeoffs, and runtime evaluation rationale.
65
65
  - **[Roadmap](roadmap.md)** -- Future directions and open questions.
66
66
 
67
67
  ## Origins
@@ -63,6 +63,7 @@ Three sections:
63
63
 
64
64
  - **`<<<NH:PROGRAM>>>`** -- your Natural block text (after sentinel removal and `textwrap.dedent`).
65
65
  - **`<<<NH:LOCALS>>>`** -- step locals, rendered alphabetically as `name: type = value`.
66
+ Nighthawk renders the full current locals snapshot by design; control context size by deciding which locals exist before the block.
66
67
  - **`<<<NH:GLOBALS>>>`** -- module-level names referenced via `<name>` that are not in step locals.
67
68
 
68
69
  ### One block, one task
@@ -138,11 +139,13 @@ Write bindings can use Pydantic models for structured output with automatic vali
138
139
  ```py
139
140
  from pydantic import BaseModel
140
141
 
142
+
141
143
  class ReviewVerdict(BaseModel):
142
144
  approved: bool
143
145
  reason: str
144
146
  risk_level: str
145
147
 
148
+
146
149
  @nh.natural_function
147
150
  def judge_review(review_data: str) -> ReviewVerdict:
148
151
  verdict: ReviewVerdict
@@ -161,6 +164,31 @@ When the LLM assigns a value to `verdict`, Nighthawk validates and coerces it to
161
164
 
162
165
  Read and write bindings are rendered identically in the LOCALS section (e.g., `name: type = value`). The `<name>` vs `<:name>` distinction in the Natural program text is the signal that tells the LLM which names it may update. At runtime, Nighthawk enforces the distinction: read bindings block rebinding, while write bindings allow rebinding and commit values back to Python locals.
163
166
 
167
+ ### Multimodal bindings
168
+
169
+ A binding may hold a Pydantic AI multimodal value -- `BinaryContent`, `ImageUrl`, `AudioUrl`, `DocumentUrl`, `VideoUrl`, or `UploadedFile`. These are first-class binding values: reference them with `<name>` like any other binding, and Nighthawk renders the inline placeholder (`<image>` for image media, `<file>` otherwise) in the LOCALS or GLOBALS line. Explicit dotted leaf references use the same mechanism, and dotted `list` / `tuple` leaves are hoisted when they satisfy the same `UserContent` rule as top-level bindings. `UploadedFile` is only usable as native user-prompt content on provider-backed executors that can resolve the provider-owned file reference; coding-agent backends reject it at the user-prompt boundary.
170
+
171
+ Transport depends on the executor:
172
+
173
+ - Provider-backed executors that accept Pydantic AI `UserContent` send the value natively to the VLM API.
174
+ - Coding-agent backends text-project the value (staged local files or URL references). See [Coding agent backends: multimodal inputs](coding-agent-backends.md#multimodal-inputs).
175
+
176
+ Nested multimodal values inside dicts, models, and objects are not recursively hoisted. Keep the prompt boundary explicit by lifting the media-bearing leaf into a helper variable before the Natural block:
177
+
178
+ ```py
179
+ def summarize_report(report: Report) -> str:
180
+ report_scan = report.attachments.scan_image
181
+ summary = ""
182
+ """natural
183
+ Read <report_scan> and summarize the visual evidence in <:summary>.
184
+ """
185
+ return summary
186
+ ```
187
+
188
+ This keeps the media selection visible in ordinary Python, avoids accidental traversal of large object graphs, and lets normal LOCALS budgeting decide whether the helper variable is rendered.
189
+
190
+ For the normative rendering and transport rules, including dotted-reference hoisting and nested multimodal behavior, see [Specification Section 8.2](specification.md#82-locals-summary).
191
+
164
192
  ### f-string injection
165
193
 
166
194
  Inline f-string blocks embed Python expressions directly into the Natural program text. The expression is evaluated when Python evaluates the f-string -- before the LLM sees the prompt.
@@ -168,6 +196,7 @@ Inline f-string blocks embed Python expressions directly into the Natural progra
168
196
  ```py
169
197
  PROJECT_POLICY = ["safety-first", "concise-output", "cite-assumptions"]
170
198
 
199
+
171
200
  @nh.natural_function
172
201
  def choose_policy(post: str) -> str:
173
202
  selected_policy = ""
@@ -207,10 +236,12 @@ Member accesses and function results work too:
207
236
  ```py
208
237
  from pydantic import BaseModel
209
238
 
239
+
210
240
  class Config(BaseModel):
211
241
  max_length: int = 100
212
242
  style: str = "formal"
213
243
 
244
+
214
245
  @nh.natural_function
215
246
  def generate(config: Config, topic: str) -> str:
216
247
  output = ""
@@ -284,6 +315,7 @@ When a Natural block references a module-level name via `<name>`, it appears in
284
315
  def python_average(numbers):
285
316
  return sum(numbers) / len(numbers)
286
317
 
318
+
287
319
  @nh.natural_function
288
320
  def calculate_average(numbers):
289
321
  """natural
@@ -323,6 +355,8 @@ python_average: (numbers)
323
355
 
324
356
  Function parameters and local variables appear in LOCALS. Module-level names referenced via `<name>` that are _not_ in locals appear in GLOBALS. Nighthawk renders callable entries with their full signature and docstring intent -- but only when type information is available.
325
357
 
358
+ This is a deliberate tradeoff: LOCALS stays a flat, predictable snapshot of what Python has already bound. If you want less prompt context, keep fewer locals alive at the block boundary. Section truncation still follows the normal lexicographic rendering order, so under tight budgets an explicitly referenced dotted multimodal leaf can still be omitted along with any other later entry.
359
+
326
360
  When you pass a module-level callable as a function parameter with a generic type (`object`, `Any`, or no annotation), the name moves from GLOBALS to LOCALS and **its signature is erased**. The LLM cannot discover the correct arguments or return type.
327
361
 
328
362
  The principle extends beyond callables. Any module-level name that is stable across invocations -- constants, classes, utility functions -- should stay in GLOBALS via `<name>` read bindings rather than being pulled into LOCALS via parameters or local assignments. Reserve function parameters for data that genuinely varies per call.
@@ -361,11 +395,13 @@ For deterministic functions (no Natural blocks), the boundary is at the function
361
395
  ```py
362
396
  from pydantic import BaseModel
363
397
 
398
+
364
399
  class ScoreInput(BaseModel):
365
400
  base: int
366
401
  bonus: int
367
402
  multiplier: float = 1.0
368
403
 
404
+
369
405
  def compute_score(score_input: ScoreInput) -> int:
370
406
  return int((score_input.base + score_input.bonus) * score_input.multiplier)
371
407
  ```
@@ -378,11 +414,13 @@ For judgment-heavy functions (containing Natural blocks), the boundary moves *in
378
414
  from pydantic import BaseModel
379
415
  from nighthawk import JsonableValue
380
416
 
417
+
381
418
  class ReviewVerdict(BaseModel):
382
419
  approved: bool
383
420
  reason: str
384
421
  risk_level: str
385
422
 
423
+
386
424
  @nh.natural_function
387
425
  def judge_review(review_data: str | JsonableValue) -> ReviewVerdict:
388
426
  verdict: ReviewVerdict
@@ -456,6 +494,7 @@ def get_feedback_summary(topic: str, max_items: int = 10) -> str:
456
494
  items = fetch_feedback(topic=topic, limit=max_items)
457
495
  return format_summary(items)
458
496
 
497
+
459
498
  # Natural block sees only what it needs
460
499
  @nh.natural_function
461
500
  def analyze_feedback(topic: str) -> str:
@@ -117,6 +117,7 @@ def validate(data: str) -> str:
117
117
  """
118
118
  return result
119
119
 
120
+
120
121
  try:
121
122
  validate("corrupted-input")
122
123
  except nh.ExecutionError as e:
@@ -131,6 +132,7 @@ When a Natural block references exception types visible in step locals or global
131
132
  class InputError(Exception):
132
133
  pass
133
134
 
135
+
134
136
  @nh.natural_function
135
137
  def strict_validate(data: str) -> str:
136
138
  result = ""
@@ -140,6 +142,7 @@ def strict_validate(data: str) -> str:
140
142
  """
141
143
  return result
142
144
 
145
+
143
146
  try:
144
147
  strict_validate("bad")
145
148
  except InputError as e:
@@ -165,6 +168,7 @@ async def summarize_async(text: str) -> str:
165
168
  """
166
169
  return result
167
170
 
171
+
168
172
  summary = await summarize_async("A long document about climate change...")
169
173
  ```
170
174
 
@@ -181,6 +185,7 @@ async def fetch_data(query: str) -> list[str]:
181
185
  """Fetch data matching the query from an external API."""
182
186
  ...
183
187
 
188
+
184
189
  @nh.natural_function
185
190
  async def analyze(query: str) -> str:
186
191
  result = ""
@@ -199,6 +204,7 @@ Async natural functions are ordinary coroutines, so you can run multiple Natural
199
204
  ```py
200
205
  import asyncio
201
206
 
207
+
202
208
  @nh.natural_function
203
209
  async def classify(text: str) -> str:
204
210
  label: str = ""
@@ -207,6 +213,7 @@ async def classify(text: str) -> str:
207
213
  """
208
214
  return label
209
215
 
216
+
210
217
  async def classify_batch(texts: list[str]) -> list[str]:
211
218
  return list(await asyncio.gather(*(classify(t) for t in texts)))
212
219
  ```
@@ -242,6 +249,7 @@ def step_1(carry: list[str]) -> int:
242
249
  """
243
250
  return result
244
251
 
252
+
245
253
  @nh.natural_function
246
254
  def step_2(carry: list[str]) -> int:
247
255
  result = 0
@@ -253,9 +261,10 @@ def step_2(carry: list[str]) -> int:
253
261
  """
254
262
  return result
255
263
 
264
+
256
265
  carry: list[str] = []
257
- r1 = step_1(carry) # carry now has 1 entry
258
- r2 = step_2(carry) # carry now has 2 entries
266
+ r1 = step_1(carry) # carry now has 1 entry
267
+ r2 = step_2(carry) # carry now has 2 entries
259
268
  ```
260
269
 
261
270
  Any mutable object works -- `list`, `dict`, Pydantic models, custom classes.
@@ -293,7 +302,7 @@ seed_step(carry)
293
302
  carry_a = carry.copy()
294
303
  carry_b = carry.copy()
295
304
 
296
- result_a = branch_add(carry_a) # diverges from here
305
+ result_a = branch_add(carry_a) # diverges from here
297
306
  result_b = branch_multiply(carry_b) # independent path
298
307
  ```
299
308
 
@@ -443,9 +452,11 @@ For monetary budgets, supply a `cost_function` that converts `RunUsage` to a flo
443
452
  ```py
444
453
  from pydantic_ai.usage import RunUsage
445
454
 
455
+
446
456
  def dollar_cost(usage: RunUsage) -> float:
447
457
  return usage.input_tokens * 3e-6 + usage.output_tokens * 15e-6
448
458
 
459
+
449
460
  budgeted = budget(cost=1.00, cost_function=dollar_cost)(classify)
450
461
  ```
451
462
 
@@ -459,7 +470,7 @@ Prevent repeated calls to a failing service. After `fail_threshold` consecutive
459
470
  from nighthawk.resilience import circuit_breaker, CircuitState
460
471
 
461
472
  protected_api = circuit_breaker(fail_threshold=5, reset_timeout=60)(call_api)
462
- protected_api.state # CircuitState.CLOSED
473
+ protected_api.state # CircuitState.CLOSED
463
474
  protected_api.reset() # manual reset
464
475
  ```
465
476
 
@@ -471,11 +482,11 @@ All transformers produce callables with the original signature, so they compose
471
482
 
472
483
  ```py
473
484
  robust_classify = fallback(
474
- retrying(attempts=2)( # 3. Retry the voted call
475
- vote(count=3)(classify_gpt4) # 2. Vote 3x with GPT-4
476
- ), # 1. Try GPT-4 first
477
- retrying(attempts=2)(classify_mini), # 4. Fall back to mini
478
- default="unknown", # 5. Last resort
485
+ retrying(attempts=2)( # 3. Retry the voted call
486
+ vote(count=3)(classify_gpt4) # 2. Vote 3x with GPT-4
487
+ ), # 1. Try GPT-4 first
488
+ retrying(attempts=2)(classify_mini), # 4. Fall back to mini
489
+ default="unknown", # 5. Last resort
479
490
  )
480
491
 
481
492
  result = robust_classify(text)
@@ -499,6 +510,7 @@ Recommended composition order (innermost to outermost):
499
510
  ```py
500
511
  from functools import lru_cache
501
512
 
513
+
502
514
  @lru_cache(maxsize=256)
503
515
  @nh.natural_function
504
516
  def classify(text: str) -> str: