nighthawk-python 0.9.0__tar.gz → 0.11.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 (179) hide show
  1. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/CHANGELOG.md +45 -1
  2. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/CONTRIBUTING.md +2 -0
  3. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/PKG-INFO +8 -8
  4. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/docs/api.md +7 -3
  5. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/docs/coding-agent-backends.md +12 -0
  6. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/docs/for-coding-agents.md +2 -1
  7. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/docs/natural-blocks.md +15 -1
  8. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/docs/patterns.md +2 -1
  9. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/docs/philosophy.md +1 -1
  10. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/docs/runtime-configuration.md +74 -4
  11. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/docs/specification.md +74 -21
  12. nighthawk_python-0.11.0/evals/promptfoo/evidence/2026-04-27-runtime-prompt-optimization.md +77 -0
  13. nighthawk_python-0.11.0/evals/promptfoo/promptfooconfig-nano-sanity.yaml +39 -0
  14. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/evals/promptfoo/promptfooconfig-prompt-ab.yaml +9 -9
  15. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/evals/promptfoo/promptfooconfig-suffix-ab.yaml +6 -6
  16. nighthawk_python-0.11.0/evals/promptfoo/prompts/eval_default.txt +25 -0
  17. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/evals/promptfoo/provider.py +5 -5
  18. nighthawk_python-0.11.0/evals/promptfoo/test_cases/loop_outcomes_ja.yaml +18 -0
  19. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/pyproject.toml +16 -16
  20. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/src/nighthawk/__init__.py +20 -3
  21. nighthawk_python-0.11.0/src/nighthawk/backends/_text_fallback.py +141 -0
  22. nighthawk_python-0.11.0/src/nighthawk/backends/base.py +248 -0
  23. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/src/nighthawk/backends/claude_code_cli.py +16 -4
  24. nighthawk_python-0.11.0/src/nighthawk/backends/claude_code_sdk.py +307 -0
  25. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/src/nighthawk/backends/codex.py +21 -7
  26. nighthawk_python-0.11.0/src/nighthawk/backends/mcp_boundary.py +295 -0
  27. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/src/nighthawk/backends/mcp_server.py +19 -29
  28. nighthawk_python-0.11.0/src/nighthawk/backends/text_projection.py +352 -0
  29. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/src/nighthawk/backends/tool_bridge.py +102 -68
  30. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/src/nighthawk/configuration.py +9 -3
  31. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/src/nighthawk/json_renderer.py +17 -0
  32. nighthawk_python-0.11.0/src/nighthawk/oversight.py +180 -0
  33. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/src/nighthawk/resilience/__init__.py +2 -3
  34. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/src/nighthawk/resilience/_vote.py +6 -5
  35. nighthawk_python-0.11.0/src/nighthawk/runtime/_user_content.py +101 -0
  36. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/src/nighthawk/runtime/prompt.py +332 -120
  37. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/src/nighthawk/runtime/runner.py +210 -79
  38. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/src/nighthawk/runtime/scoping.py +112 -34
  39. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/src/nighthawk/runtime/step_context.py +15 -1
  40. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/src/nighthawk/runtime/step_contract.py +13 -4
  41. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/src/nighthawk/runtime/step_executor.py +24 -13
  42. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/src/nighthawk/runtime/tool_calls.py +9 -6
  43. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/src/nighthawk/testing.py +11 -16
  44. nighthawk_python-0.11.0/src/nighthawk/tools/_tool_return_content.py +66 -0
  45. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/src/nighthawk/tools/assignment.py +7 -4
  46. nighthawk_python-0.11.0/src/nighthawk/tools/contracts.py +195 -0
  47. nighthawk_python-0.11.0/src/nighthawk/tools/execution.py +239 -0
  48. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/tests/backends/test_codex.py +125 -2
  49. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/tests/docs/test_docs_architecture.py +19 -0
  50. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/tests/docs/test_prompt_examples.py +7 -4
  51. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/tests/execution/prompt_test_helpers.py +50 -1
  52. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/tests/execution/stub_executor.py +2 -2
  53. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/tests/execution/test_execution_outcome_prompt_fragment.py +16 -3
  54. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/tests/execution/test_globals_prompt.py +41 -7
  55. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/tests/execution/test_natural_block_ordering.py +3 -3
  56. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/tests/execution/test_natural_traceback.py +2 -1
  57. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/tests/execution/test_runtime.py +290 -9
  58. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/tests/execution/test_variables_prompt.py +520 -3
  59. nighthawk_python-0.11.0/tests/governance/test_governance.py +198 -0
  60. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/tests/integration/test_llm_integration.py +159 -0
  61. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/tests/public/test_public_api.py +74 -21
  62. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/tests/resilience/test_vote.py +8 -0
  63. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/tests/tools/test_assignment_async.py +37 -0
  64. nighthawk_python-0.11.0/tests/tools/test_contracts.py +202 -0
  65. nighthawk_python-0.11.0/tests/tools/test_multimodal_helpers.py +167 -0
  66. nighthawk_python-0.11.0/tests/tools/test_tool_boundary.py +1897 -0
  67. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/uv.lock +264 -258
  68. nighthawk_python-0.9.0/src/nighthawk/backends/base.py +0 -129
  69. nighthawk_python-0.9.0/src/nighthawk/backends/claude_code_sdk.py +0 -289
  70. nighthawk_python-0.9.0/src/nighthawk/backends/mcp_boundary.py +0 -129
  71. nighthawk_python-0.9.0/src/nighthawk/tools/contracts.py +0 -72
  72. nighthawk_python-0.9.0/src/nighthawk/tools/execution.py +0 -83
  73. nighthawk_python-0.9.0/tests/tools/test_contracts.py +0 -106
  74. nighthawk_python-0.9.0/tests/tools/test_tool_boundary.py +0 -218
  75. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/.claude/rules/docs.md +0 -0
  76. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/.claude/rules/promptfoo.md +0 -0
  77. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/.claude/rules/src.md +0 -0
  78. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/.claude/rules/tests.md +0 -0
  79. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/.claude/settings.json +0 -0
  80. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/.claude/unset_envs.sh +0 -0
  81. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/.devcontainer/Dockerfile.devcontainer +0 -0
  82. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/.devcontainer/Dockerfile.litellm +0 -0
  83. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/.devcontainer/devcontainer.json +0 -0
  84. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/.devcontainer/docker-compose.yaml +0 -0
  85. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/.devcontainer/litellm-config.yaml +0 -0
  86. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/.github/dependabot.yml +0 -0
  87. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/.github/workflows/ci.yml +0 -0
  88. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/.github/workflows/docs.yml +0 -0
  89. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/.github/workflows/publish.yml +0 -0
  90. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/.gitignore +0 -0
  91. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/.python-version +0 -0
  92. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/AGENTS.md +0 -0
  93. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/CLAUDE.md +0 -0
  94. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/LICENSE +0 -0
  95. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/README.md +0 -0
  96. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/docs/AGENTS.md +0 -0
  97. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/docs/assets/nighthawk_logo-128x128.png +0 -0
  98. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/docs/executors.md +0 -0
  99. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/docs/index.md +0 -0
  100. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/docs/pydantic-ai-providers.md +0 -0
  101. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/docs/quickstart.md +0 -0
  102. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/docs/roadmap.md +0 -0
  103. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/docs/verification.md +0 -0
  104. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/evals/promptfoo/assertions/__init__.py +0 -0
  105. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/evals/promptfoo/assertions/binding_value.py +0 -0
  106. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/evals/promptfoo/assertions/outcome_kind.py +0 -0
  107. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/evals/promptfoo/assertions/raise_message.py +0 -0
  108. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/evals/promptfoo/evidence/2026-03-26-baseline-prompt-ab.md +0 -0
  109. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/evals/promptfoo/evidence/2026-03-26-baseline-regression.md +0 -0
  110. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/evals/promptfoo/evidence/2026-03-26-baseline-suffix-ab.md +0 -0
  111. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/evals/promptfoo/promptfooconfig-agents.yaml +0 -0
  112. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/evals/promptfoo/promptfooconfig.yaml +0 -0
  113. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/evals/promptfoo/prompts/eval_coding_agent.txt +0 -0
  114. /nighthawk_python-0.9.0/evals/promptfoo/prompts/eval_default.txt → /nighthawk_python-0.11.0/evals/promptfoo/prompts/eval_legacy_default.txt +0 -0
  115. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/evals/promptfoo/prompts/eval_mutation_aware.txt +0 -0
  116. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/evals/promptfoo/prompts/eval_sequenced.txt +0 -0
  117. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/evals/promptfoo/research-result.md +0 -0
  118. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/evals/promptfoo/test_cases/binding_operations.yaml +0 -0
  119. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/evals/promptfoo/test_cases/edge_cases.yaml +0 -0
  120. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/evals/promptfoo/test_cases/loop_outcomes.yaml +0 -0
  121. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/evals/promptfoo/test_cases/multi_step.yaml +0 -0
  122. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/evals/promptfoo/test_cases/null_handling.yaml +0 -0
  123. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/evals/promptfoo/test_cases/outcome_kinds.yaml +0 -0
  124. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/evals/promptfoo/test_cases/tool_selection.yaml +0 -0
  125. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/mkdocs.yml +0 -0
  126. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/pyrightconfig.json +0 -0
  127. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/src/AGENTS.md +0 -0
  128. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/src/nighthawk/backends/__init__.py +0 -0
  129. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/src/nighthawk/backends/claude_code_settings.py +0 -0
  130. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/src/nighthawk/errors.py +0 -0
  131. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/src/nighthawk/identifier_path.py +0 -0
  132. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/src/nighthawk/natural/__init__.py +0 -0
  133. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/src/nighthawk/natural/blocks.py +0 -0
  134. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/src/nighthawk/natural/decorator.py +0 -0
  135. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/src/nighthawk/natural/transform.py +0 -0
  136. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/src/nighthawk/resilience/_budget.py +0 -0
  137. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/src/nighthawk/resilience/_circuit_breaker.py +0 -0
  138. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/src/nighthawk/resilience/_fallback.py +0 -0
  139. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/src/nighthawk/resilience/_retry.py +0 -0
  140. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/src/nighthawk/resilience/_timeout.py +0 -0
  141. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/src/nighthawk/runtime/__init__.py +0 -0
  142. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/src/nighthawk/runtime/async_bridge.py +0 -0
  143. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/src/nighthawk/tools/__init__.py +0 -0
  144. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/src/nighthawk/tools/provided.py +0 -0
  145. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/src/nighthawk/tools/registry.py +0 -0
  146. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/src/nighthawk/ulid.py +0 -0
  147. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/tests/AGENTS.md +0 -0
  148. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/tests/__init__.py +0 -0
  149. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/tests/backends/__init__.py +0 -0
  150. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/tests/backends/test_claude_code_cli.py +0 -0
  151. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/tests/backends/test_claude_code_sdk.py +0 -0
  152. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/tests/conftest.py +0 -0
  153. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/tests/docs/__init__.py +0 -0
  154. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/tests/docs/test_coding_agent_examples.py +0 -0
  155. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/tests/execution/__init__.py +0 -0
  156. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/tests/execution/test_infer_binding_types.py +0 -0
  157. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/tests/integration/__init__.py +0 -0
  158. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/tests/integration/skip_helpers.py +0 -0
  159. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/tests/integration/test_carry_pattern.py +0 -0
  160. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/tests/integration/test_claude_code_cli_integration.py +0 -0
  161. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/tests/integration/test_claude_code_sdk_integration.py +0 -0
  162. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/tests/integration/test_codex_integration.py +0 -0
  163. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/tests/natural/__init__.py +0 -0
  164. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/tests/natural/test_blocks.py +0 -0
  165. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/tests/public/__init__.py +0 -0
  166. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/tests/public/test_readme_example.py +0 -0
  167. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/tests/public/test_usage_meter.py +0 -0
  168. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/tests/resilience/__init__.py +0 -0
  169. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/tests/resilience/test_budget.py +0 -0
  170. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/tests/resilience/test_circuit_breaker.py +0 -0
  171. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/tests/resilience/test_composition.py +0 -0
  172. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/tests/resilience/test_fallback.py +0 -0
  173. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/tests/resilience/test_retry.py +0 -0
  174. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/tests/resilience/test_timeout.py +0 -0
  175. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/tests/test_renderer.py +0 -0
  176. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/tests/test_testing.py +0 -0
  177. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/tests/tools/__init__.py +0 -0
  178. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/tests/tools/test_provided_async.py +0 -0
  179. {nighthawk_python-0.9.0 → nighthawk_python-0.11.0}/tests/tools/test_registry.py +0 -0
@@ -7,7 +7,48 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.11.0]
11
+
12
+ ### Added
13
+ - Native Pydantic AI multimodal binding support for Natural block prompts, including `BinaryContent`, URL-based media, `UploadedFile`, and explicit dotted multimodal references.
14
+ - Multimodal tool-result transport for coding-agent backends, preserving mixed text/media order and carrying MCP image/audio content natively where supported.
15
+ - Public scope snapshot getters: `get_implicit_references`, `get_system_prompt_suffix_fragments`, and `get_user_prompt_suffix_fragments`.
16
+
17
+ ### Changed
18
+ - Coding-agent backends now text-project multimodal user prompts into local file paths or URL placeholders while provider-backed executors pass `UserContent` natively.
19
+ - Tool results now retain `ToolOutcome` payloads through backend boundaries so multimodal-capable transports can render native content before falling back to JSON previews.
20
+ - Optimize Natural block runtime prompt
21
+ - Update dependencies
22
+
23
+ ### Fixed
24
+ - Cleaned up staged multimodal prompt files when text projection fails before a projected request is returned.
25
+
26
+ ## [0.10.0]
27
+
28
+ ### Added
29
+ - Scope-level oversight hooks in `nh.scope(oversight=...)` for synchronous tool-call inspection and step-commit inspection.
30
+ - Oversight trace events and backend/wrapper coverage for accept/reject decision paths.
31
+ - Validation for `vote(min_success=...)` bounds (`>= 1` and `<= count`) with matching tests.
32
+ - Tool-boundary regression test ensuring recoverable `ToolBoundaryError` is wrapped into JSON payload.
33
+
34
+ ### Changed
35
+ - Finalized committed binding validation at step boundaries, added write-root dirty tracking for dotted `nh_assign`, and aligned prompt/docs with the new validation contract.
36
+ - Introduced `ExecutionRef` (`run_id`, `scope_id`, optional `step_id`) and renamed runtime accessor to `get_execution_ref`.
37
+ - Added top-level module access for `oversight` and `resilience`, with docs and public API tests aligned.
38
+ - Consolidated tool-call oversight inspection to a single wrapper-side entry point and removed duplicate inspection guard state.
39
+ - Unified tool result handling to TypedDict-based envelope types (`ToolResult`, `ToolError`) instead of Pydantic models.
40
+ - Clarified tool-boundary policy: recoverable tool-call failures are JSON-envelope responses, host invariant violations propagate as Python exceptions.
41
+ - Updated specification and runtime configuration docs to codify envelope-vs-raise boundary semantics for tool-call failures.
42
+ - Updated API and patterns docs for `ExecutionRef`, `get_execution_ref`, and oversight/resilience namespace exposure.
43
+
44
+ ### Fixed
45
+ - Backend handler now preserves wrapped tool error payloads (`{"value": ..., "error": ...}`) without dropping error details.
46
+ - Tool-call context propagation now preserves step identity in execution ref during step execution.
47
+
48
+ ## [0.9.0]
49
+
10
50
  ### Changed
51
+
11
52
  - Redesigned `nh.scope()` around `mode` semantics (`"inherit"` default, `"replace"` for explicit replacement).
12
53
  - Scope prompt suffix arguments now use list-based forms: `system_prompt_suffix_fragments` and `user_prompt_suffix_fragments`.
13
54
 
@@ -138,7 +179,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
138
179
  - Step executor abstraction and provider integration foundation.
139
180
  - Core documentation and project scaffolding.
140
181
 
141
- [Unreleased]: https://github.com/kurusugawa-computer/nighthawk-python/compare/v0.8.0...HEAD
182
+ [Unreleased]: https://github.com/kurusugawa-computer/nighthawk-python/compare/v0.11.0...HEAD
183
+ [0.11.0]: https://github.com/kurusugawa-computer/nighthawk-python/compare/v0.10.0...v0.11.0
184
+ [0.10.0]: https://github.com/kurusugawa-computer/nighthawk-python/compare/v0.9.0...v0.10.0
185
+ [0.9.0]: https://github.com/kurusugawa-computer/nighthawk-python/compare/v0.8.0...v0.9.0
142
186
  [0.8.0]: https://github.com/kurusugawa-computer/nighthawk-python/compare/v0.7.0...v0.8.0
143
187
  [0.7.0]: https://github.com/kurusugawa-computer/nighthawk-python/compare/v0.6.1...v0.7.0
144
188
  [0.6.1]: https://github.com/kurusugawa-computer/nighthawk-python/compare/v0.6.0...v0.6.1
@@ -81,6 +81,7 @@ PFOO="cd evals/promptfoo && PROMPTFOO_PYTHON=\"$(uv python find)\" npx promptfoo
81
81
  |---|---|
82
82
  | `eval $PFOO eval` | Full regression (all backends, all tests) |
83
83
  | `eval $PFOO eval -c promptfooconfig-prompt-ab.yaml` | Prompt A/B test (gpt-5.4-mini only) |
84
+ | `eval $PFOO eval -c promptfooconfig-nano-sanity.yaml --no-cache` | Default-model sanity check (gpt-5.4-nano only) |
84
85
  | `eval $PFOO eval -c promptfooconfig-agents.yaml` | Coding agent backends (reduced test set) |
85
86
  | `eval $PFOO eval -c promptfooconfig.yaml --filter-pattern "P-BIND-001"` | Single test |
86
87
  | `eval $PFOO eval -c promptfooconfig.yaml --filter-providers "claude-code-cli"` | Single backend |
@@ -90,6 +91,7 @@ PFOO="cd evals/promptfoo && PROMPTFOO_PYTHON=\"$(uv python find)\" npx promptfoo
90
91
 
91
92
  - `promptfooconfig.yaml` — Regression: winner prompt combo across openai-responses, claude-code-cli, and codex backends. All test cases.
92
93
  - `promptfooconfig-prompt-ab.yaml` — A/B testing: 4 prompt/tool variants on gpt-5.4-mini. All test cases.
94
+ - `promptfooconfig-nano-sanity.yaml` — Default-model sanity check on openai-responses:gpt-5.4-nano. All test cases.
93
95
  - `promptfooconfig-agents.yaml` — Coding agent only: claude-code-cli and codex with reduced test set.
94
96
 
95
97
  #### Directory layout
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nighthawk-python
3
- Version: 0.9.0
3
+ Version: 0.11.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>=1.87.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>=1.27.0; extra == 'claude-code-cli'
30
30
  Provides-Extra: claude-code-sdk
31
31
  Requires-Dist: claude-agent-sdk==0.1.48; extra == 'claude-code-sdk'
32
32
  Provides-Extra: codex
33
- Requires-Dist: mcp>=1.26; extra == 'codex'
33
+ Requires-Dist: mcp>=1.27.0; 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)
@@ -18,12 +18,17 @@
18
18
  - StepContextLimits
19
19
  - JsonableValue
20
20
  - to_jsonable_value
21
- - ExecutionContext
21
+ - ExecutionRef
22
22
  - get_current_step_context
23
23
  - get_current_usage_meter
24
- - get_execution_context
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
29
+ - resilience
26
30
  - UsageMeter
31
+ - oversight
27
32
 
28
33
  ## Errors
29
34
 
@@ -123,4 +128,3 @@
123
128
  ## Testing
124
129
 
125
130
  ::: nighthawk.testing
126
-
@@ -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. 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:
@@ -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
 
@@ -327,7 +328,7 @@ Async rule:
327
328
  Resilience rule:
328
329
 
329
330
  - Keep retry, fallback, timeout, budget, and circuit-breaker policy in Python, not inside Natural text.
330
- - Import from `nighthawk.resilience` (not re-exported from `nighthawk`):
331
+ - Import primitives from `nighthawk.resilience`. After `import nighthawk as nh`, the module namespace is also available as `nh.resilience`:
331
332
 
332
333
  ```py
333
334
  from nighthawk.resilience import retrying
@@ -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
@@ -129,7 +130,7 @@ def detect_language(text: str):
129
130
  return language
130
131
  ```
131
132
 
132
- Type annotations on write bindings enable validation and coercion at commit time.
133
+ Type annotations on write bindings enable validation and coercion at step finalization for values that are actually committed.
133
134
 
134
135
  ### Pydantic model write bindings
135
136
 
@@ -161,6 +162,17 @@ When the LLM assigns a value to `verdict`, Nighthawk validates and coerces it to
161
162
 
162
163
  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
164
 
165
+ ### Multimodal bindings
166
+
167
+ 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.
168
+
169
+ Transport depends on the executor:
170
+
171
+ - Provider-backed executors that accept Pydantic AI `UserContent` send the value natively to the VLM API.
172
+ - 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).
173
+
174
+ For the normative rendering and transport rules, including dotted-reference hoisting and the remaining v0.11.0 nesting restrictions, see [Specification Section 8.2](specification.md#82-locals-summary).
175
+
164
176
  ### f-string injection
165
177
 
166
178
  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.
@@ -323,6 +335,8 @@ python_average: (numbers)
323
335
 
324
336
  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
337
 
338
+ 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.
339
+
326
340
  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
341
 
328
342
  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.
@@ -326,7 +326,8 @@ Natural blocks are non-deterministic by nature. Production deployments need stra
326
326
  from nighthawk.resilience import retrying, fallback, vote, timeout, budget, circuit_breaker
327
327
  ```
328
328
 
329
- Import directly from `nighthawk.resilience`. Resilience primitives are not re-exported from the top-level `nighthawk` namespace.
329
+ Import directly from `nighthawk.resilience`.
330
+ After `import nighthawk as nh`, the module namespace is also available as `nh.resilience`.
330
331
 
331
332
  ### Retrying
332
333
 
@@ -23,7 +23,7 @@ calculate_average([1, "2", "three", "cuatro", "五"]) # 3.0
23
23
 
24
24
  Binding functions like `<python_average>` appear in the prompt as a compact signature line. The LLM's pre-trained Python knowledge lets it reason about types, return values, and composition from the signature alone, without JSON Schema or protocol overhead. See [Tool exposure efficiency](#tool-exposure-efficiency) for the quantitative comparison with MCP and CLI tool exposure.
25
25
 
26
- With provider-backed executors, each Natural block is a single LLM call. A sentiment classifier whose write binding is typed as `Literal["positive", "negative", "neutral"]` rejects any output outside the declared set -- Pydantic validates the type annotation at runtime, not as a hint. The same mechanism applies to numeric extraction (`int`, `float`), structured parsing (Pydantic models), and any task where the judgment space is bounded. Because the host program owns the loop, a misclassified result can be retried, logged, or routed to a fallback -- all in ordinary Python.
26
+ With provider-backed executors, each Natural block is a single LLM call. A sentiment classifier whose write binding is typed as `Literal["positive", "negative", "neutral"]` rejects any output outside the declared set -- Pydantic validates committed write bindings at step finalization, not as a hint. The same mechanism applies to numeric extraction (`int`, `float`), structured parsing (Pydantic models), and any task where the judgment space is bounded. Because the host program owns the loop, a misclassified result can be retried, logged, or routed to a fallback -- all in ordinary Python.
27
27
 
28
28
  With [coding agent backends](coding-agent-backends.md), the same boundary contract applies, but each Natural block becomes an autonomous agent execution. The agent can read files, run commands, and invoke skills -- while typed bindings enforce what crosses the boundary back to Python. The same `scope()` and `run()` context managers that structure human-written workflows are equally legible to a coding agent constructing workflows programmatically. When a coding agent operates inside a Natural block, binding functions appear as Python signatures in the prompt:
29
29
 
@@ -35,6 +35,15 @@ with nh.run(step_executor):
35
35
  # Add implicit global references for this scope
36
36
  with nh.scope(implicit_references={"search_repository": search_repository}):
37
37
  typed_labeling_step(ticket_text)
38
+
39
+ # Add synchronous oversight for this scope
40
+ with nh.scope(
41
+ oversight=nh.oversight.Oversight(
42
+ inspect_tool_call=inspect_tool_call,
43
+ inspect_step_commit=inspect_step_commit,
44
+ )
45
+ ):
46
+ inspected_step(ticket_text)
38
47
  ```
39
48
 
40
49
  Parameters:
@@ -42,6 +51,7 @@ Parameters:
42
51
  - `mode`: scope composition mode. Default: `"inherit"`.
43
52
  - `step_executor_configuration`: replace the entire configuration.
44
53
  - `step_executor`: replace the step executor entirely.
54
+ - `oversight`: scope-level synchronous tool-call inspection and step-commit inspection hooks.
45
55
  - `system_prompt_suffix_fragments`: scope-level system suffix fragments.
46
56
  - `user_prompt_suffix_fragments`: scope-level user suffix fragments.
47
57
  - `implicit_references`: scope-level implicit global references.
@@ -56,10 +66,16 @@ Mode semantics:
56
66
  - Explicit `[]` or `{}` clears inherited list/dict values.
57
67
  - Explicit list/dict values (for example `[e1, e2]` or `{k1: v1, k2: v2}`) fully replace inherited values.
58
68
 
69
+ For `oversight`, omitted means inherit the current hooks, while explicit `None` clears them for the nested scope.
70
+
59
71
  The context manager yields the resolved `StepExecutor` for the scope.
60
72
 
61
73
  `StepExecutorConfiguration` also accepts `system_prompt_suffix_fragments` and `user_prompt_suffix_fragments` (tuples of strings) as baseline suffix fragments for the whole run. In `mode="inherit"`, scope-level fragments are appended after configuration-level fragments.
62
74
 
75
+ System prompt suffix fragments are rendered with the same `$tool_result_max_tokens` placeholder used by built-in prompt templates. Use `$$tool_result_max_tokens` if the literal text must appear in the final prompt.
76
+
77
+ Text-projected backends that expose Nighthawk tools automatically add a short tool-result preview warning to the system prompt. You do not need to add a separate preview warning fragment for coding-agent or other text-projected tool transports.
78
+
63
79
  ## Scoped implicit references
64
80
 
65
81
  `implicit_references` can inject global helper functions as step capabilities:
@@ -138,6 +154,59 @@ with nh.scope(mode="replace", system_prompt_suffix_fragments=None):
138
154
  ```
139
155
 
140
156
  The same `replace` semantics apply to `user_prompt_suffix_fragments`.
157
+
158
+ ## Reading the active scope
159
+
160
+ Within an active `nh.run()` context, three snapshot getters expose the current scope's accumulated state:
161
+
162
+ - `nh.get_implicit_references()` returns a `Mapping[str, object]` snapshot of implicit references.
163
+ - `nh.get_system_prompt_suffix_fragments()` returns the system prompt suffix fragments as a `tuple[str, ...]`.
164
+ - `nh.get_user_prompt_suffix_fragments()` returns the user prompt suffix fragments as a `tuple[str, ...]`.
165
+
166
+ The prompt suffix getters return only fragments accumulated via `nh.scope(...)`; configuration-level baseline fragments from `StepExecutorConfiguration` are not included.
167
+
168
+ Use these when composing nested scopes from a host helper: read the parent state, derive a subset, then re-enter via `nh.scope(mode="replace", ...)`. Both narrowing (keeping a subset) and masking (removing specific keys) are expressed by deriving a new mapping and passing it under `mode="replace"`.
169
+
170
+ ```py
171
+ with nh.run(step_executor):
172
+ with nh.scope(implicit_references={"a": helper_a, "b": helper_b}):
173
+ current = nh.get_implicit_references()
174
+ narrowed = {name: value for name, value in current.items() if name != "b"}
175
+ with nh.scope(mode="replace", implicit_references=narrowed):
176
+ triage_issue(ticket_text)
177
+ ```
178
+
179
+ All three getters require an active run context. Outside `nh.run()` they raise `NighthawkError`, matching `nh.get_step_executor()` semantics. Catch `NighthawkError` if a helper needs to detect the absence of a run.
180
+
181
+ ## Synchronous oversight in scopes
182
+
183
+ Use `nh.scope(oversight=...)` when the host needs synchronous inspection around tool calls or a final rewrite/reject checkpoint before Nighthawk commits a step result.
184
+
185
+ ```py
186
+ def inspect_tool_call(tool_call: nh.oversight.ToolCall) -> nh.oversight.ToolCallDecision:
187
+ if tool_call.tool_name == "delete_file":
188
+ return nh.oversight.Reject("Deletion must be approved by a human.")
189
+ return nh.oversight.Accept()
190
+
191
+
192
+ def inspect_step_commit(proposal: nh.oversight.StepCommitProposal) -> nh.oversight.StepCommitDecision:
193
+ if "result" in proposal.proposed_binding_name_to_value:
194
+ return nh.oversight.Rewrite(rewritten_binding_name_to_value={"result": "reviewed"})
195
+ return nh.oversight.Accept()
196
+
197
+
198
+ with nh.run(step_executor):
199
+ with nh.scope(
200
+ oversight=nh.oversight.Oversight(
201
+ inspect_tool_call=inspect_tool_call,
202
+ inspect_step_commit=inspect_step_commit,
203
+ )
204
+ ):
205
+ inspected_step(ticket_text)
206
+ ```
207
+
208
+ Tool rejections are returned to the model as a recoverable observation. On preview-based paths this appears with `error.kind == "oversight"` in the projected preview; provider-backed paths that use Pydantic AI's standard retry loop may instead surface the same structured details as a retry prompt whose final line is compact JSON. Step rejections raise `nh.oversight.OversightRejectedError` to the host. Rewrite values still flow through the normal step finalization path. For the normative boundary rule on which tool-call failures are projected back to the model versus propagated as host exceptions, see [Specification Section 8.3](specification.md#83-tool-boundary-contract-built-in-tooling).
209
+
141
210
  ## Mixing executors
142
211
 
143
212
  Use `nh.scope(step_executor=...)` to switch executors within a single run. This is the standard pattern for mixing a cheap classifier with a deep autonomous step:
@@ -181,12 +250,13 @@ See [Specification Section 8.2](specification.md#82-prompt-context) for the full
181
250
 
182
251
  ## Runtime execution identity
183
252
 
184
- Each `nh.run()` generates an `ExecutionContext` with a unique `run_id` (trace root) and `scope_id`. Nested `nh.scope()` calls generate new `scope_id` values while keeping the same `run_id`.
253
+ Each `nh.run()` generates an `ExecutionRef` with a unique `run_id` (trace root) and `scope_id`. Nested `nh.scope()` calls generate new `scope_id` values while keeping the same `run_id`.
185
254
 
186
255
  ```py
187
- context = nh.get_execution_context()
188
- context.run_id # trace root -- stable across nested scopes
189
- context.scope_id # current scope -- changes with each nh.scope()
256
+ execution_ref = nh.get_execution_ref()
257
+ execution_ref.run_id # trace root -- stable across nested scopes
258
+ execution_ref.scope_id # current scope -- changes with each nh.scope()
259
+ execution_ref.step_id # None outside active step execution
190
260
  ```
191
261
 
192
262
  Use `run_id` to correlate distributed agent processes in logs and traces. Use `scope_id` to identify the current logical execution context. See [Specification Section 10](specification.md#10-runtime-scoping) for the full specification and [Verification: observability](verification.md#observability) for tracing integration.
@@ -88,7 +88,7 @@ The condensed coding agent guide (`for-coding-agents.md`) is a derivative docume
88
88
  - `step_system_prompt_template`: system prompt template that defines the step execution protocol.
89
89
  - `step_user_prompt_template`: full user prompt template including section delimiters.
90
90
  - `context_limits`: limits for rendering dynamic context into the prompt.
91
- - `json_renderer_style`: [headson](https://github.com/kantord/headson) rendering style used in prompt context and tool result envelopes. Default: `"default"`. Available values: `"strict"` (valid JSON, no annotations), `"default"` (pseudo-JSON with omission markers like `…`), `"detailed"` (JS-like with inline comments such as `// N more`).
91
+ - `json_renderer_style`: [headson](https://github.com/kantord/headson) rendering style used in prompt context and projected tool-result previews. Default: `"default"`. Available values: `"strict"` (valid JSON, no annotations), `"default"` (pseudo-JSON with omission markers like `…`), `"detailed"` (JS-like with inline comments such as `// N more`).
92
92
  - `system_prompt_suffix_fragments`: optional baseline system prompt suffix fragments for this executor configuration.
93
93
  - `user_prompt_suffix_fragments`: optional baseline user prompt suffix fragments for this executor configuration.
94
94
 
@@ -103,17 +103,18 @@ The condensed coding agent guide (`for-coding-agents.md`) is a derivative docume
103
103
  - Fields: `locals_max_tokens`, `locals_max_items`, `globals_max_tokens`, `globals_max_items`, `value_max_tokens`, `object_max_methods`, `object_max_fields`, `object_field_value_max_tokens`, `tool_result_max_tokens`.
104
104
  - `JsonableValue`
105
105
  - Type alias for JSON-serializable Python values (`dict | list | str | int | float | bool | None`).
106
- - `ExecutionContext`
106
+ - `ExecutionRef`
107
107
  - Frozen dataclass representing runtime execution identity.
108
108
  - `run_id`: the Id of the outermost run (trace root).
109
109
  - `scope_id`: the Id of the current scope.
110
+ - `step_id`: the Id of the current step when available, otherwise `None`.
110
111
 
111
112
  ### 5.4. Runtime accessors
112
113
 
113
114
  - `nighthawk.get_current_step_context() -> StepContext`
114
115
  - Get the `StepContext` for the currently executing Natural block. Raises if no step is active.
115
116
 
116
- See [Section 10](#10-runtime-scoping) for additional runtime accessors (`get_step_executor`, `get_execution_context`).
117
+ See [Section 10](#10-runtime-scoping) for additional runtime accessors (`get_step_executor`, `get_execution_ref`).
117
118
 
118
119
  ## 6. Natural block detection
119
120
 
@@ -239,7 +240,7 @@ The template uses `$program`, `$locals`, and `$globals` placeholders, substitute
239
240
 
240
241
  #### 8.2.2. Locals summary
241
242
 
242
- The locals summary renders selected names from `step_locals`.
243
+ The locals summary renders selected names from `step_locals`. It is a transparent projection of Python-visible local state at the Natural block boundary. Nighthawk MUST NOT automatically suppress otherwise eligible LOCALS entries based on reference frequency, VLM cost, or multimodal payload type; users control prompt cost by reducing the Python locals that exist at the block boundary.
243
244
 
244
245
  Selection:
245
246
 
@@ -268,6 +269,19 @@ Rendering format:
268
269
  - Method expansion is bounded by `context_limits.object_max_methods`; field expansion is bounded by `context_limits.object_max_fields`; each field value preview is bounded by `context_limits.object_field_value_max_tokens`.
269
270
  - If object member limits are exceeded, explicit omission lines are added: `name.<methods>: <snipped N public methods>` and/or `name.<fields>: <snipped N public fields>`.
270
271
  - Other non-callable values: `name: type_name = json_value`, where `json_value` is bounded by `context_limits.value_max_tokens`.
272
+ - Top-level Pydantic AI multimodal values are rendered as `name: TypeName = ` followed by inline user content in the prompt payload.
273
+ - This applies to top-level locals/globals entries.
274
+ - Top-level `list` / `tuple` values are also rendered as inline user content when every item is valid Pydantic AI `UserContent` and at least one item is multimodal.
275
+ - The binding line still renders once, for example `photos: list = `, followed by the original top-level item order.
276
+ - Pure text-only sequences do not use this rule; they remain preview-rendered values.
277
+ - Explicit dotted references whose resolved leaf resolves to hoistable inline user content add a separate section line for that full dotted path (for example `holder.photo: BinaryContent = ` followed by inline user content).
278
+ - Only explicitly referenced dotted paths are added this way.
279
+ - This is a leaf-only rendering rule, not recursive object-graph extraction.
280
+ - Dotted-path resolution is attribute-oriented only. Each segment after the top-level name is resolved against discovered object fields using the same safe field sources as object capability rendering.
281
+ - Dotted-path resolution does not treat `Mapping` keys as attribute segments. For mapping access, use Python expressions or helper functions (for example `nh_eval("payload['photo']")`) rather than `<payload.photo>`.
282
+ - `list` / `tuple` leaves follow the same hoisting rule as top-level bindings: the sequence is hoisted when every item is valid Pydantic AI `UserContent` and at least one item is multimodal.
283
+ - Non-multimodal dotted references remain preview text inside object capability rendering.
284
+ - When a dotted multimodal reference is rendered as a separate section line, the corresponding field is omitted from the owning object capability block to avoid duplication. The field still consumes a slot in the `object_max_fields` budget.
271
285
 
272
286
  Callable disambiguation considers both top-level callable entries and object method entries in the same section.
273
287
 
@@ -289,6 +303,7 @@ Token budgeting:
289
303
  - Section budgets remain governed by `locals_max_tokens` / `globals_max_tokens` and item budgets by `locals_max_items` / `globals_max_items`.
290
304
  - Object method and field expansion is additionally governed by object-specific limits listed above.
291
305
  - Line-level token counting includes newline separators during budget checks.
306
+ - Each rendered multimodal content item is charged a fixed internal budgeting heuristic against the section budget so that multimodal-heavy sections still trigger truncation. This is not a provider-side token estimate.
292
307
 
293
308
  Observability:
294
309
 
@@ -299,6 +314,7 @@ Truncation:
299
314
 
300
315
  - Rendering is bounded by `context_limits.locals_max_tokens` and `context_limits.locals_max_items`.
301
316
  - When the limit is reached before all entries are rendered, a `<snipped>` marker is appended and a diagnostic log message is emitted on the `nighthawk` logger.
317
+ - Because section rendering remains lexicographic, tight item or token budgets can affect which bindings remain visible, including explicitly referenced dotted multimodal leaves.
302
318
 
303
319
  #### 8.2.3. Globals summary
304
320
 
@@ -307,7 +323,7 @@ The globals summary renders module-level names that are referenced in the Natura
307
323
  Reference extraction:
308
324
 
309
325
  - The Natural program text is scanned for unescaped `<name>` tokens (both read bindings `<name>` and dotted references `<name.field>`).
310
- - For dotted references, only the top-level name (before the first `.`) is extracted.
326
+ - For dotted references, the top-level name (before the first `.`) participates in globals selection, and the full dotted path remains available for multimodal leaf rendering.
311
327
  - Escaped references (`\<name>`) are not extracted. The backslash is removed in the program text passed to the model.
312
328
 
313
329
  Selection:
@@ -392,6 +408,7 @@ Eval tool:
392
408
 
393
409
  - `nh_eval(expression: str) -> object`
394
410
  - Evaluate a Python expression and return the result. Use to inspect values, call functions, and mutate objects in-place.
411
+ - In-place mutations performed via `nh_eval` are not runtime-validated.
395
412
  - If the evaluated expression is awaitable, it is awaited before returning.
396
413
 
397
414
  Binding tool:
@@ -421,13 +438,16 @@ Semantics of `nh_assign`:
421
438
  - Traverse attributes for each intermediate segment.
422
439
  - Assign using attribute assignment on the final segment.
423
440
  - Validation:
424
- - Validate only when runtime type metadata is available; otherwise assign without validation.
441
+ - Validate only the final assigned field when runtime type metadata is available; otherwise assign without validation.
425
442
 
426
443
  Commit and mutation notes:
427
444
 
428
445
  - Commit selection is controlled only by `<:name>` bindings.
429
446
  - `<:name>` selects which top-level names are committed from `step_locals` into Python locals at Natural block boundaries.
430
- - Dotted mutation is independent of `<:name>`.
447
+ - Dotted `nh_assign` on a write binding root marks that root as dirty for commit selection.
448
+ - Dotted `nh_assign` on a read binding root does not participate in commit selection.
449
+ - Final validation is applied only to committed write bindings at step finalization.
450
+ - Dotted mutation on read bindings and in-place mutation via `nh_eval` are outside the final validation guarantee.
431
451
 
432
452
  Write tool return value:
433
453
 
@@ -437,16 +457,47 @@ Write tool return value:
437
457
  - validation details (when relevant)
438
458
  - error details (on failure)
439
459
 
440
- Tool result JSON format:
441
-
442
- All tool results are wrapped in a JSON envelope with the following structure:
443
-
444
- - Success: `{"value": <bounded JSON rendering>, "error": null}`
445
- - Failure: `{"value": null, "error": {"kind": "<category>", "message": "<detail>", "guidance": "<recovery hint>"}}`
446
-
447
- Error kind categories: `invalid_input`, `resolution`, `execution`, `transient`, `internal`.
448
-
449
- The `value` field is bounded by `context_limits.tool_result_max_tokens` and may be summarized using [headson](https://github.com/kantord/headson) truncation when the full rendering exceeds the token budget. Headson is a structure-aware JSON summarizer: it parses the full JSON tree, then selects representative nodes to produce a compact preview that preserves the shape and key values of the data within a strict byte budget (analogous to `head`/`tail` but for structured data).
460
+ Tool result transport contract:
461
+
462
+ - Internally, tool execution produces a canonical `ToolOutcome` with:
463
+ - `payload`: the success payload, which may include native Pydantic AI multimodal values
464
+ - `error`: `null` on success or a structured error object on failure
465
+ - The host also derives a projected preview observation for text-only transports, logging, and tracing.
466
+ - This projection may be derived lazily at the transport or tracing boundary rather than eagerly at tool execution time.
467
+ - Text-projected backends that expose Nighthawk tools automatically add system-prompt guidance that tool-result previews may be lossy and are bounded by `context_limits.tool_result_max_tokens`.
468
+ - The projected preview uses the following JSON shape:
469
+ - Success: `{"value": <bounded JSON rendering>, "error": null}`
470
+ - Failure: `{"value": null, "error": {"kind": "<category>", "message": "<detail>", "guidance": "<recovery hint>"}}`
471
+ - Multimodal-capable transports MAY send top-level multimodal payload items natively instead of embedding them into the preview text.
472
+ - When they do, they preserve the original top-level content item order from the tool payload, including text/media adjacency for qualifying mixed `list` / `tuple` payloads.
473
+ - In v0.11.0, the MCP tool-return transport carries only image (`BinaryContent.is_image`) and audio (`BinaryContent.is_audio`) items natively (as `mcp.types.ImageContent` / `mcp.types.AudioContent`). All other `BinaryContent` and `FileUrl` items, including document and video, project to `mcp.types.TextContent` so the transport stays symmetric with text-projected backends. Future expansion of native carriage to document or video items via `mcp.types.EmbeddedResource` is intentionally out of scope in v0.11.0 and is not guaranteed by this specification. Multimodal-capable providers that bypass MCP still send those items natively via `ToolReturnPart.files`.
474
+ - Provider-backed backends that use Pydantic AI's standard tool loop SHOULD surface general `ToolOutcome.error` failures as ordinary tool results rather than retry prompts.
475
+ - In v0.11.0, the dedicated retry-prompt path is reserved for tool argument validation failures before tool execution (for example via `RetryPromptPart.model_response()`).
476
+ - When a provider-backed backend surfaces a tool failure without native multimodal content, it SHOULD use the same structured preview envelope shape described above.
477
+ - User-prompt transport is backend-dependent.
478
+ - Provider-backed executors that accept Pydantic AI `UserContent` MAY send user-prompt multimodal content natively.
479
+ - Text-projected backends (including coding agent backends that must talk to a CLI text channel) project multimodal user prompt content to text placeholders plus local file paths or URL references instead of sending native VLM input.
480
+ - For tool results, backends SHOULD preserve top-level mixed text/multimodal order when the payload is a `list` / `tuple` whose items are all valid `UserContent` and at least one item is multimodal.
481
+ - When that ordered-segmentation rule does not apply, text-projected transports SHOULD derive tool-return text from Pydantic AI's `ToolReturnPart.model_response_str_and_user_content()` and project the returned `user_content` separately.
482
+ - MCP transports SHOULD use the same fallback split when ordered segmentation does not apply: one text observation derived from `tool_response_text`, followed by the extracted `user_content` items in order.
483
+ - If a rich transport fails while projecting an otherwise recoverable tool result, the backend SHOULD degrade to the projected preview text rather than fail the step.
484
+ - When Pydantic AI reports a successful empty split (`tool_response_text == ""` and extracted `user_content` is empty), backends MUST preserve that as an empty success rather than falling back to the projected preview envelope.
485
+ - Text-projected transports render only their normal tool-return framing with no preview JSON body.
486
+ - MCP-style native transports return an empty content list for that tool result.
487
+ - `BinaryContent` values can be staged to local files for text-projected transports because the host already has the bytes.
488
+ - URL-based values such as `ImageUrl` / `FileUrl` MAY be transported natively by multimodal-capable provider backends, but text-projected backends and the MCP tool-return transport treat them as URL references only.
489
+ - In v0.11.0, nested multimodal values inside dict/model/object structure are not hoisted; they remain part of the preview rendering only, except for top-level or explicitly referenced dotted-leaf `list` / `tuple` bindings whose items are valid `UserContent`.
490
+ - `UploadedFile` values are provider-owned references. Backends that cannot resolve the provider file identifier MUST reject them at the user-prompt boundary.
491
+ - For tool results, backends that cannot resolve an `UploadedFile` MUST preserve the rest of the top-level payload and replace only that item with an explanatory text fallback instead of silently dropping it.
492
+
493
+ Error kind categories: `invalid_input`, `resolution`, `execution`, `transient`, `internal`, `oversight`.
494
+
495
+ Boundary rule for tool-call failures:
496
+
497
+ - If the model can recover by changing tool arguments, choosing another tool, or continuing without the tool, the failure MUST be projected back to the model as a structured error observation.
498
+ - Host invariant violations (for example, broken runtime contract or invalid host-side oversight hook contract) MAY propagate as Python exceptions to the host instead of being projected.
499
+
500
+ The projected `value` field is bounded by `context_limits.tool_result_max_tokens` and may be summarized using [headson](https://github.com/kantord/headson) truncation when the full rendering exceeds the token budget. Headson is a structure-aware JSON summarizer: it parses the full JSON tree, then selects representative nodes to produce a compact preview that preserves the shape and key values of the data within a strict byte budget (analogous to `head`/`tail` but for structured data).
450
501
 
451
502
  Atomicity requirement:
452
503
 
@@ -570,10 +621,11 @@ The required runtime object for step execution is:
570
621
 
571
622
  - `step_executor` (required): a strategy object responsible for executing steps (Natural blocks).
572
623
 
573
- Runtime execution identity is modeled separately in `ExecutionContext`:
624
+ Runtime execution identity is modeled separately in `ExecutionRef`:
574
625
 
575
626
  - `run_id`: the Id of the outermost run (trace root). This serves as the golden thread that connects distributed agent processes (e.g. parent, child, grandchild) across process boundaries in observability tools.
576
627
  - `scope_id`: the Id of the current (possibly nested) run scope. This serves as the identity of the current logical execution context.
628
+ - `step_id`: the Id of the current step when available. Outside active step execution it is `None`.
577
629
 
578
630
  Nighthawk does not own workspace filesystem concerns (such as include resolution or host file operations). Those concerns belong to the host application layer that embeds Nighthawk.
579
631
 
@@ -582,14 +634,15 @@ API:
582
634
 
583
635
  - `nighthawk.run(step_executor: StepExecutor, *, run_id: str | None = None)`
584
636
  - Replaces the current context step executor with the provided step executor.
585
- - Generates a new `ExecutionContext` for the duration of the `with`.
637
+ - Generates a new `ExecutionRef` for the duration of the `with`.
586
638
  - Uses provided `run_id` when given; otherwise generates a new `run_id` (trace root).
587
639
  - Always generates a fresh `scope_id`.
588
640
  - Can be used even when no step executor is currently set.
589
- - `nighthawk.scope(*, mode: Literal["inherit", "replace"] = "inherit", step_executor_configuration: StepExecutorConfiguration | None = None, step_executor: StepExecutor | None = None, system_prompt_suffix_fragments: Sequence[str] | None = None, user_prompt_suffix_fragments: Sequence[str] | None = None, implicit_references: Mapping[str, object] | None = None) -> Iterator[StepExecutor]`
641
+ - `nighthawk.scope(*, mode: Literal["inherit", "replace"] = "inherit", step_executor_configuration: StepExecutorConfiguration | None = None, step_executor: StepExecutor | None = None, oversight: Oversight | None = None, system_prompt_suffix_fragments: Sequence[str] | None = None, user_prompt_suffix_fragments: Sequence[str] | None = None, implicit_references: Mapping[str, object] | None = None) -> Iterator[StepExecutor]`
590
642
  - Enter a nested scope within the current run.
591
643
  - Requires an existing step executor.
592
644
  - Generates a new `scope_id` (keeps the current `run_id`).
645
+ - `oversight` omitted means inherit the current hooks; explicit `None` clears them for the nested scope.
593
646
  - `mode="inherit"` (default):
594
647
  - Appends prompt suffix fragment lists.
595
648
  - Merges `implicit_references` additively with conflict checks.
@@ -601,7 +654,7 @@ API:
601
654
  - Yields the resolved `StepExecutor` for the scope.
602
655
  - `nighthawk.get_step_executor() -> StepExecutor`
603
656
  - Get the current step executor. Raises if unset.
604
- - `nighthawk.get_execution_context() -> ExecutionContext`
657
+ - `nighthawk.get_execution_ref() -> ExecutionRef`
605
658
  - Get the current runtime execution identity. Raises if unset.
606
659
 
607
660
  ### 10.1. Observability contract (OpenTelemetry span/event)