nighthawk-python 0.9.0__tar.gz → 0.10.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 (165) hide show
  1. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/CHANGELOG.md +28 -1
  2. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/PKG-INFO +1 -1
  3. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/docs/api.md +4 -3
  4. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/docs/for-coding-agents.md +1 -1
  5. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/docs/natural-blocks.md +1 -1
  6. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/docs/patterns.md +2 -1
  7. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/docs/philosophy.md +1 -1
  8. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/docs/runtime-configuration.md +47 -4
  9. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/docs/specification.md +20 -8
  10. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/pyproject.toml +1 -1
  11. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/src/nighthawk/__init__.py +6 -3
  12. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/src/nighthawk/backends/mcp_server.py +2 -1
  13. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/src/nighthawk/backends/tool_bridge.py +12 -10
  14. nighthawk_python-0.10.0/src/nighthawk/oversight.py +180 -0
  15. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/src/nighthawk/resilience/__init__.py +2 -3
  16. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/src/nighthawk/resilience/_vote.py +6 -5
  17. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/src/nighthawk/runtime/prompt.py +4 -4
  18. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/src/nighthawk/runtime/runner.py +209 -78
  19. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/src/nighthawk/runtime/scoping.py +65 -31
  20. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/src/nighthawk/runtime/step_context.py +8 -1
  21. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/src/nighthawk/runtime/step_executor.py +6 -6
  22. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/src/nighthawk/testing.py +11 -16
  23. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/src/nighthawk/tools/assignment.py +7 -4
  24. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/src/nighthawk/tools/contracts.py +7 -8
  25. nighthawk_python-0.10.0/src/nighthawk/tools/execution.py +160 -0
  26. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/tests/execution/stub_executor.py +2 -2
  27. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/tests/execution/test_natural_block_ordering.py +3 -3
  28. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/tests/execution/test_natural_traceback.py +2 -1
  29. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/tests/execution/test_runtime.py +199 -6
  30. nighthawk_python-0.10.0/tests/governance/test_governance.py +198 -0
  31. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/tests/public/test_public_api.py +8 -7
  32. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/tests/resilience/test_vote.py +8 -0
  33. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/tests/tools/test_assignment_async.py +37 -0
  34. nighthawk_python-0.10.0/tests/tools/test_tool_boundary.py +513 -0
  35. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/uv.lock +61 -61
  36. nighthawk_python-0.9.0/src/nighthawk/tools/execution.py +0 -83
  37. nighthawk_python-0.9.0/tests/tools/test_tool_boundary.py +0 -218
  38. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/.claude/rules/docs.md +0 -0
  39. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/.claude/rules/promptfoo.md +0 -0
  40. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/.claude/rules/src.md +0 -0
  41. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/.claude/rules/tests.md +0 -0
  42. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/.claude/settings.json +0 -0
  43. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/.claude/unset_envs.sh +0 -0
  44. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/.devcontainer/Dockerfile.devcontainer +0 -0
  45. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/.devcontainer/Dockerfile.litellm +0 -0
  46. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/.devcontainer/devcontainer.json +0 -0
  47. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/.devcontainer/docker-compose.yaml +0 -0
  48. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/.devcontainer/litellm-config.yaml +0 -0
  49. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/.github/dependabot.yml +0 -0
  50. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/.github/workflows/ci.yml +0 -0
  51. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/.github/workflows/docs.yml +0 -0
  52. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/.github/workflows/publish.yml +0 -0
  53. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/.gitignore +0 -0
  54. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/.python-version +0 -0
  55. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/AGENTS.md +0 -0
  56. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/CLAUDE.md +0 -0
  57. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/CONTRIBUTING.md +0 -0
  58. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/LICENSE +0 -0
  59. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/README.md +0 -0
  60. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/docs/AGENTS.md +0 -0
  61. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/docs/assets/nighthawk_logo-128x128.png +0 -0
  62. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/docs/coding-agent-backends.md +0 -0
  63. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/docs/executors.md +0 -0
  64. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/docs/index.md +0 -0
  65. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/docs/pydantic-ai-providers.md +0 -0
  66. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/docs/quickstart.md +0 -0
  67. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/docs/roadmap.md +0 -0
  68. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/docs/verification.md +0 -0
  69. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/evals/promptfoo/assertions/__init__.py +0 -0
  70. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/evals/promptfoo/assertions/binding_value.py +0 -0
  71. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/evals/promptfoo/assertions/outcome_kind.py +0 -0
  72. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/evals/promptfoo/assertions/raise_message.py +0 -0
  73. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/evals/promptfoo/evidence/2026-03-26-baseline-prompt-ab.md +0 -0
  74. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/evals/promptfoo/evidence/2026-03-26-baseline-regression.md +0 -0
  75. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/evals/promptfoo/evidence/2026-03-26-baseline-suffix-ab.md +0 -0
  76. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/evals/promptfoo/promptfooconfig-agents.yaml +0 -0
  77. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/evals/promptfoo/promptfooconfig-prompt-ab.yaml +0 -0
  78. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/evals/promptfoo/promptfooconfig-suffix-ab.yaml +0 -0
  79. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/evals/promptfoo/promptfooconfig.yaml +0 -0
  80. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/evals/promptfoo/prompts/eval_coding_agent.txt +0 -0
  81. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/evals/promptfoo/prompts/eval_default.txt +0 -0
  82. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/evals/promptfoo/prompts/eval_mutation_aware.txt +0 -0
  83. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/evals/promptfoo/prompts/eval_sequenced.txt +0 -0
  84. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/evals/promptfoo/provider.py +0 -0
  85. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/evals/promptfoo/research-result.md +0 -0
  86. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/evals/promptfoo/test_cases/binding_operations.yaml +0 -0
  87. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/evals/promptfoo/test_cases/edge_cases.yaml +0 -0
  88. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/evals/promptfoo/test_cases/loop_outcomes.yaml +0 -0
  89. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/evals/promptfoo/test_cases/multi_step.yaml +0 -0
  90. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/evals/promptfoo/test_cases/null_handling.yaml +0 -0
  91. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/evals/promptfoo/test_cases/outcome_kinds.yaml +0 -0
  92. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/evals/promptfoo/test_cases/tool_selection.yaml +0 -0
  93. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/mkdocs.yml +0 -0
  94. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/pyrightconfig.json +0 -0
  95. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/src/AGENTS.md +0 -0
  96. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/src/nighthawk/backends/__init__.py +0 -0
  97. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/src/nighthawk/backends/base.py +0 -0
  98. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/src/nighthawk/backends/claude_code_cli.py +0 -0
  99. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/src/nighthawk/backends/claude_code_sdk.py +0 -0
  100. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/src/nighthawk/backends/claude_code_settings.py +0 -0
  101. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/src/nighthawk/backends/codex.py +0 -0
  102. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/src/nighthawk/backends/mcp_boundary.py +0 -0
  103. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/src/nighthawk/configuration.py +0 -0
  104. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/src/nighthawk/errors.py +0 -0
  105. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/src/nighthawk/identifier_path.py +0 -0
  106. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/src/nighthawk/json_renderer.py +0 -0
  107. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/src/nighthawk/natural/__init__.py +0 -0
  108. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/src/nighthawk/natural/blocks.py +0 -0
  109. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/src/nighthawk/natural/decorator.py +0 -0
  110. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/src/nighthawk/natural/transform.py +0 -0
  111. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/src/nighthawk/resilience/_budget.py +0 -0
  112. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/src/nighthawk/resilience/_circuit_breaker.py +0 -0
  113. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/src/nighthawk/resilience/_fallback.py +0 -0
  114. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/src/nighthawk/resilience/_retry.py +0 -0
  115. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/src/nighthawk/resilience/_timeout.py +0 -0
  116. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/src/nighthawk/runtime/__init__.py +0 -0
  117. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/src/nighthawk/runtime/async_bridge.py +0 -0
  118. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/src/nighthawk/runtime/step_contract.py +0 -0
  119. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/src/nighthawk/runtime/tool_calls.py +0 -0
  120. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/src/nighthawk/tools/__init__.py +0 -0
  121. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/src/nighthawk/tools/provided.py +0 -0
  122. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/src/nighthawk/tools/registry.py +0 -0
  123. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/src/nighthawk/ulid.py +0 -0
  124. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/tests/AGENTS.md +0 -0
  125. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/tests/__init__.py +0 -0
  126. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/tests/backends/__init__.py +0 -0
  127. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/tests/backends/test_claude_code_cli.py +0 -0
  128. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/tests/backends/test_claude_code_sdk.py +0 -0
  129. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/tests/backends/test_codex.py +0 -0
  130. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/tests/conftest.py +0 -0
  131. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/tests/docs/__init__.py +0 -0
  132. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/tests/docs/test_coding_agent_examples.py +0 -0
  133. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/tests/docs/test_docs_architecture.py +0 -0
  134. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/tests/docs/test_prompt_examples.py +0 -0
  135. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/tests/execution/__init__.py +0 -0
  136. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/tests/execution/prompt_test_helpers.py +0 -0
  137. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/tests/execution/test_execution_outcome_prompt_fragment.py +0 -0
  138. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/tests/execution/test_globals_prompt.py +0 -0
  139. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/tests/execution/test_infer_binding_types.py +0 -0
  140. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/tests/execution/test_variables_prompt.py +0 -0
  141. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/tests/integration/__init__.py +0 -0
  142. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/tests/integration/skip_helpers.py +0 -0
  143. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/tests/integration/test_carry_pattern.py +0 -0
  144. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/tests/integration/test_claude_code_cli_integration.py +0 -0
  145. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/tests/integration/test_claude_code_sdk_integration.py +0 -0
  146. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/tests/integration/test_codex_integration.py +0 -0
  147. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/tests/integration/test_llm_integration.py +0 -0
  148. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/tests/natural/__init__.py +0 -0
  149. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/tests/natural/test_blocks.py +0 -0
  150. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/tests/public/__init__.py +0 -0
  151. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/tests/public/test_readme_example.py +0 -0
  152. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/tests/public/test_usage_meter.py +0 -0
  153. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/tests/resilience/__init__.py +0 -0
  154. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/tests/resilience/test_budget.py +0 -0
  155. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/tests/resilience/test_circuit_breaker.py +0 -0
  156. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/tests/resilience/test_composition.py +0 -0
  157. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/tests/resilience/test_fallback.py +0 -0
  158. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/tests/resilience/test_retry.py +0 -0
  159. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/tests/resilience/test_timeout.py +0 -0
  160. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/tests/test_renderer.py +0 -0
  161. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/tests/test_testing.py +0 -0
  162. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/tests/tools/__init__.py +0 -0
  163. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/tests/tools/test_contracts.py +0 -0
  164. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/tests/tools/test_provided_async.py +0 -0
  165. {nighthawk_python-0.9.0 → nighthawk_python-0.10.0}/tests/tools/test_registry.py +0 -0
@@ -7,7 +7,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.10.0]
11
+
12
+ ### Added
13
+ - Scope-level oversight hooks in `nh.scope(oversight=...)` for synchronous tool-call inspection and step-commit inspection.
14
+ - Oversight trace events and backend/wrapper coverage for accept/reject decision paths.
15
+ - Validation for `vote(min_success=...)` bounds (`>= 1` and `<= count`) with matching tests.
16
+ - Tool-boundary regression test ensuring recoverable `ToolBoundaryError` is wrapped into JSON payload.
17
+
10
18
  ### Changed
19
+ - 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.
20
+ - Introduced `ExecutionRef` (`run_id`, `scope_id`, optional `step_id`) and renamed runtime accessor to `get_execution_ref`.
21
+ - Added top-level module access for `oversight` and `resilience`, with docs and public API tests aligned.
22
+ - Consolidated tool-call oversight inspection to a single wrapper-side entry point and removed duplicate inspection guard state.
23
+ - Unified tool result handling to TypedDict-based envelope types (`ToolResult`, `ToolError`) instead of Pydantic models.
24
+ - Clarified tool-boundary policy: recoverable tool-call failures are JSON-envelope responses, host invariant violations propagate as Python exceptions.
25
+ - Updated specification and runtime configuration docs to codify envelope-vs-raise boundary semantics for tool-call failures.
26
+ - Updated API and patterns docs for `ExecutionRef`, `get_execution_ref`, and oversight/resilience namespace exposure.
27
+
28
+ ### Fixed
29
+ - Backend handler now preserves wrapped tool error payloads (`{"value": ..., "error": ...}`) without dropping error details.
30
+ - Tool-call context propagation now preserves step identity in execution ref during step execution.
31
+
32
+ ## [0.9.0]
33
+
34
+ ### Changed
35
+
11
36
  - Redesigned `nh.scope()` around `mode` semantics (`"inherit"` default, `"replace"` for explicit replacement).
12
37
  - Scope prompt suffix arguments now use list-based forms: `system_prompt_suffix_fragments` and `user_prompt_suffix_fragments`.
13
38
 
@@ -138,7 +163,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
138
163
  - Step executor abstraction and provider integration foundation.
139
164
  - Core documentation and project scaffolding.
140
165
 
141
- [Unreleased]: https://github.com/kurusugawa-computer/nighthawk-python/compare/v0.8.0...HEAD
166
+ [Unreleased]: https://github.com/kurusugawa-computer/nighthawk-python/compare/v0.10.0...HEAD
167
+ [0.10.0]: https://github.com/kurusugawa-computer/nighthawk-python/compare/v0.9.0...v0.10.0
168
+ [0.9.0]: https://github.com/kurusugawa-computer/nighthawk-python/compare/v0.8.0...v0.9.0
142
169
  [0.8.0]: https://github.com/kurusugawa-computer/nighthawk-python/compare/v0.7.0...v0.8.0
143
170
  [0.7.0]: https://github.com/kurusugawa-computer/nighthawk-python/compare/v0.6.1...v0.7.0
144
171
  [0.6.1]: https://github.com/kurusugawa-computer/nighthawk-python/compare/v0.6.0...v0.6.1
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nighthawk-python
3
- Version: 0.9.0
3
+ Version: 0.10.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/
@@ -18,12 +18,14 @@
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
25
  - get_step_executor
26
+ - resilience
26
27
  - UsageMeter
28
+ - oversight
27
29
 
28
30
  ## Errors
29
31
 
@@ -123,4 +125,3 @@
123
125
  ## Testing
124
126
 
125
127
  ::: nighthawk.testing
126
-
@@ -327,7 +327,7 @@ Async rule:
327
327
  Resilience rule:
328
328
 
329
329
  - 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`):
330
+ - Import primitives from `nighthawk.resilience`. After `import nighthawk as nh`, the module namespace is also available as `nh.resilience`:
331
331
 
332
332
  ```py
333
333
  from nighthawk.resilience import retrying
@@ -129,7 +129,7 @@ def detect_language(text: str):
129
129
  return language
130
130
  ```
131
131
 
132
- Type annotations on write bindings enable validation and coercion at commit time.
132
+ Type annotations on write bindings enable validation and coercion at step finalization for values that are actually committed.
133
133
 
134
134
  ### Pydantic model write bindings
135
135
 
@@ -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,6 +66,8 @@ 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.
@@ -138,6 +150,36 @@ with nh.scope(mode="replace", system_prompt_suffix_fragments=None):
138
150
  ```
139
151
 
140
152
  The same `replace` semantics apply to `user_prompt_suffix_fragments`.
153
+
154
+ ## Synchronous oversight in scopes
155
+
156
+ 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.
157
+
158
+ ```py
159
+ def inspect_tool_call(tool_call: nh.oversight.ToolCall) -> nh.oversight.ToolCallDecision:
160
+ if tool_call.tool_name == "delete_file":
161
+ return nh.oversight.Reject("Deletion must be approved by a human.")
162
+ return nh.oversight.Accept()
163
+
164
+
165
+ def inspect_step_commit(proposal: nh.oversight.StepCommitProposal) -> nh.oversight.StepCommitDecision:
166
+ if "result" in proposal.proposed_binding_name_to_value:
167
+ return nh.oversight.Rewrite(rewritten_binding_name_to_value={"result": "reviewed"})
168
+ return nh.oversight.Accept()
169
+
170
+
171
+ with nh.run(step_executor):
172
+ with nh.scope(
173
+ oversight=nh.oversight.Oversight(
174
+ inspect_tool_call=inspect_tool_call,
175
+ inspect_step_commit=inspect_step_commit,
176
+ )
177
+ ):
178
+ inspected_step(ticket_text)
179
+ ```
180
+
181
+ Tool rejections are returned to the model as a normal tool result envelope with `error.kind == "oversight"`. 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 envelope-wrapped versus propagated as host exceptions, see [Specification Section 8.3](specification.md#83-tool-boundary-contract-built-in-tooling).
182
+
141
183
  ## Mixing executors
142
184
 
143
185
  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 +223,13 @@ See [Specification Section 8.2](specification.md#82-prompt-context) for the full
181
223
 
182
224
  ## Runtime execution identity
183
225
 
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`.
226
+ 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
227
 
186
228
  ```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()
229
+ execution_ref = nh.get_execution_ref()
230
+ execution_ref.run_id # trace root -- stable across nested scopes
231
+ execution_ref.scope_id # current scope -- changes with each nh.scope()
232
+ execution_ref.step_id # None outside active step execution
190
233
  ```
191
234
 
192
235
  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.
@@ -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
 
@@ -392,6 +393,7 @@ Eval tool:
392
393
 
393
394
  - `nh_eval(expression: str) -> object`
394
395
  - Evaluate a Python expression and return the result. Use to inspect values, call functions, and mutate objects in-place.
396
+ - In-place mutations performed via `nh_eval` are not runtime-validated.
395
397
  - If the evaluated expression is awaitable, it is awaited before returning.
396
398
 
397
399
  Binding tool:
@@ -421,13 +423,16 @@ Semantics of `nh_assign`:
421
423
  - Traverse attributes for each intermediate segment.
422
424
  - Assign using attribute assignment on the final segment.
423
425
  - Validation:
424
- - Validate only when runtime type metadata is available; otherwise assign without validation.
426
+ - Validate only the final assigned field when runtime type metadata is available; otherwise assign without validation.
425
427
 
426
428
  Commit and mutation notes:
427
429
 
428
430
  - Commit selection is controlled only by `<:name>` bindings.
429
431
  - `<: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>`.
432
+ - Dotted `nh_assign` on a write binding root marks that root as dirty for commit selection.
433
+ - Dotted `nh_assign` on a read binding root does not participate in commit selection.
434
+ - Final validation is applied only to committed write bindings at step finalization.
435
+ - Dotted mutation on read bindings and in-place mutation via `nh_eval` are outside the final validation guarantee.
431
436
 
432
437
  Write tool return value:
433
438
 
@@ -446,6 +451,11 @@ All tool results are wrapped in a JSON envelope with the following structure:
446
451
 
447
452
  Error kind categories: `invalid_input`, `resolution`, `execution`, `transient`, `internal`.
448
453
 
454
+ Boundary rule for tool-call failures:
455
+
456
+ - If the model can recover by changing tool arguments, choosing another tool, or continuing without the tool, the failure MUST be returned in the JSON envelope (`{"value": null, "error": ...}`).
457
+ - 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 envelope-wrapped.
458
+
449
459
  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).
450
460
 
451
461
  Atomicity requirement:
@@ -570,10 +580,11 @@ The required runtime object for step execution is:
570
580
 
571
581
  - `step_executor` (required): a strategy object responsible for executing steps (Natural blocks).
572
582
 
573
- Runtime execution identity is modeled separately in `ExecutionContext`:
583
+ Runtime execution identity is modeled separately in `ExecutionRef`:
574
584
 
575
585
  - `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
586
  - `scope_id`: the Id of the current (possibly nested) run scope. This serves as the identity of the current logical execution context.
587
+ - `step_id`: the Id of the current step when available. Outside active step execution it is `None`.
577
588
 
578
589
  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
590
 
@@ -582,14 +593,15 @@ API:
582
593
 
583
594
  - `nighthawk.run(step_executor: StepExecutor, *, run_id: str | None = None)`
584
595
  - Replaces the current context step executor with the provided step executor.
585
- - Generates a new `ExecutionContext` for the duration of the `with`.
596
+ - Generates a new `ExecutionRef` for the duration of the `with`.
586
597
  - Uses provided `run_id` when given; otherwise generates a new `run_id` (trace root).
587
598
  - Always generates a fresh `scope_id`.
588
599
  - 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]`
600
+ - `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
601
  - Enter a nested scope within the current run.
591
602
  - Requires an existing step executor.
592
603
  - Generates a new `scope_id` (keeps the current `run_id`).
604
+ - `oversight` omitted means inherit the current hooks; explicit `None` clears them for the nested scope.
593
605
  - `mode="inherit"` (default):
594
606
  - Appends prompt suffix fragment lists.
595
607
  - Merges `implicit_references` additively with conflict checks.
@@ -601,7 +613,7 @@ API:
601
613
  - Yields the resolved `StepExecutor` for the scope.
602
614
  - `nighthawk.get_step_executor() -> StepExecutor`
603
615
  - Get the current step executor. Raises if unset.
604
- - `nighthawk.get_execution_context() -> ExecutionContext`
616
+ - `nighthawk.get_execution_ref() -> ExecutionRef`
605
617
  - Get the current runtime execution identity. Raises if unset.
606
618
 
607
619
  ### 10.1. Observability contract (OpenTelemetry span/event)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "nighthawk-python"
3
- version = "0.9.0"
3
+ version = "0.10.0"
4
4
  description = "A Python library where Python controls flow and LLMs or coding agents reason within constrained Natural blocks."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.13"
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from . import oversight, resilience
3
4
  from .configuration import (
4
5
  StepContextLimits,
5
6
  StepExecutorConfiguration,
@@ -15,15 +16,15 @@ from .errors import (
15
16
  )
16
17
  from .json_renderer import JsonableValue, to_jsonable_value
17
18
  from .natural.decorator import natural_function
18
- from .runtime.scoping import ExecutionContext, UsageMeter, get_current_usage_meter, get_execution_context, get_step_executor, run, scope
19
+ from .runtime.scoping import ExecutionRef, UsageMeter, get_current_usage_meter, get_execution_ref, get_step_executor, run, scope
19
20
  from .runtime.step_context import StepContext, get_current_step_context
20
21
  from .runtime.step_executor import AgentStepExecutor, StepExecutor
21
22
  from .tools.registry import tool
22
23
 
23
24
  __all__ = [
24
25
  "AgentStepExecutor",
25
- "ExecutionContext",
26
26
  "ExecutionError",
27
+ "ExecutionRef",
27
28
  "JsonableValue",
28
29
  "NaturalParseError",
29
30
  "NighthawkError",
@@ -38,9 +39,11 @@ __all__ = [
38
39
  "UsageMeter",
39
40
  "get_current_step_context",
40
41
  "get_current_usage_meter",
41
- "get_execution_context",
42
+ "get_execution_ref",
42
43
  "get_step_executor",
44
+ "oversight",
43
45
  "natural_function",
46
+ "resilience",
44
47
  "run",
45
48
  "scope",
46
49
  "to_jsonable_value",
@@ -23,6 +23,7 @@ from pydantic_ai import RunContext
23
23
  from pydantic_ai.exceptions import UnexpectedModelBehavior
24
24
  from pydantic_ai.tools import ToolDefinition
25
25
 
26
+ from ..errors import NighthawkError
26
27
  from ..runtime.step_context import (
27
28
  DEFAULT_TOOL_RESULT_RENDERING_POLICY,
28
29
  StepContext,
@@ -203,7 +204,7 @@ async def mcp_server_if_needed(
203
204
  parent_otel_context = otel_context.get_current()
204
205
  parent_run_context = get_current_run_context()
205
206
  if parent_run_context is None:
206
- raise RuntimeError("Codex MCP tool server requires an active RunContext")
207
+ raise NighthawkError("Codex MCP tool server requires an active RunContext")
207
208
  typed_parent_run_context = cast(RunContext[StepContext], parent_run_context)
208
209
  tool_result_rendering_policy = typed_parent_run_context.deps.tool_result_rendering_policy
209
210
  if tool_result_rendering_policy is None:
@@ -20,13 +20,10 @@ from pydantic_ai.messages import RetryPromptPart
20
20
  from pydantic_ai.models import ModelRequestParameters
21
21
  from pydantic_ai.toolsets.function import FunctionToolset
22
22
 
23
+ from ..errors import NighthawkError
23
24
  from ..runtime.step_context import DEFAULT_TOOL_RESULT_RENDERING_POLICY, StepContext, ToolResultRenderingPolicy
24
25
  from ..runtime.tool_calls import generate_tool_call_id, run_tool_instrumented
25
- from ..tools.contracts import (
26
- ToolBoundaryError,
27
- ToolResult,
28
- render_tool_result_json_text,
29
- )
26
+ from ..tools.contracts import ToolBoundaryError, render_tool_result_json_text
30
27
  from ..tools.execution import ToolResultWrapperToolset
31
28
 
32
29
  type ToolHandler = Callable[[dict[str, Any]], Awaitable[str]]
@@ -44,7 +41,7 @@ def get_current_run_context_required() -> RunContext[StepContext]:
44
41
 
45
42
  run_context = get_current_run_context()
46
43
  if run_context is None:
47
- raise RuntimeError("Nighthawk tool boundaries require an active RunContext")
44
+ raise NighthawkError("Nighthawk tool boundaries require an active RunContext")
48
45
  return cast(RunContext[StepContext], run_context)
49
46
 
50
47
 
@@ -71,6 +68,7 @@ async def execute_tool_call(
71
68
  *,
72
69
  tool_name: str,
73
70
  tool: Any,
71
+ toolset: ToolResultWrapperToolset,
74
72
  arguments: dict[str, Any],
75
73
  run_context: RunContext[StepContext],
76
74
  tool_call_id: str,
@@ -102,7 +100,7 @@ async def execute_tool_call(
102
100
  ).model_response()
103
101
 
104
102
  try:
105
- tool_result = await tool.toolset.call_tool(tool_name, validated_arguments, run_context, tool)
103
+ tool_result = await toolset.call_tool(tool_name, validated_arguments, run_context, tool)
106
104
  except (ModelRetry, CallDeferred, ApprovalRequired):
107
105
  raise
108
106
  except ToolBoundaryError as exception:
@@ -121,6 +119,9 @@ async def execute_tool_call(
121
119
  encoding=encoding,
122
120
  style=rendering_policy.json_renderer_style,
123
121
  )
122
+ except NighthawkError:
123
+ # Preserve host-invariant failures as Python exceptions; these are not LLM-recoverable tool-call errors and must propagate to the host.
124
+ raise
124
125
  except Exception as exception:
125
126
  return render_tool_result_json_text(
126
127
  value=None,
@@ -134,10 +135,10 @@ async def execute_tool_call(
134
135
  style=rendering_policy.json_renderer_style,
135
136
  )
136
137
 
137
- if isinstance(tool_result, ToolResult):
138
+ if isinstance(tool_result, dict) and set(tool_result.keys()) == {"value", "error"}:
138
139
  return render_tool_result_json_text(
139
- value=tool_result.value,
140
- error=None,
140
+ value=tool_result["value"],
141
+ error=tool_result["error"],
141
142
  max_tokens=rendering_policy.tool_result_max_tokens,
142
143
  encoding=encoding,
143
144
  style=rendering_policy.json_renderer_style,
@@ -193,6 +194,7 @@ async def build_tool_name_to_handler(
193
194
  return await execute_tool_call(
194
195
  tool_name=tool_name,
195
196
  tool=tool,
197
+ toolset=toolset,
196
198
  arguments=arguments,
197
199
  run_context=tool_run_context,
198
200
  tool_call_id=tool_call_id,
@@ -0,0 +1,180 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable, Mapping
4
+ from dataclasses import dataclass
5
+ from types import MappingProxyType
6
+
7
+ from opentelemetry.trace import get_current_span
8
+ from pydantic import BaseModel
9
+
10
+ from .errors import NighthawkError
11
+ from .runtime.scoping import ExecutionRef
12
+ from .runtime.step_contract import StepKind, StepOutcome
13
+
14
+
15
+ def _snapshot_value(value: object) -> object:
16
+ if isinstance(value, BaseModel):
17
+ return value.model_copy(deep=True)
18
+ if isinstance(value, Mapping):
19
+ return MappingProxyType({key: _snapshot_value(item) for key, item in value.items()})
20
+ if isinstance(value, tuple):
21
+ return tuple(_snapshot_value(item) for item in value)
22
+ if isinstance(value, list):
23
+ return tuple(_snapshot_value(item) for item in value)
24
+ if isinstance(value, set):
25
+ return frozenset(_snapshot_value(item) for item in value)
26
+ if isinstance(value, frozenset):
27
+ return frozenset(_snapshot_value(item) for item in value)
28
+ return value
29
+
30
+
31
+ def _snapshot_mapping(name_to_value: Mapping[str, object]) -> Mapping[str, object]:
32
+ return MappingProxyType({name: _snapshot_value(value) for name, value in name_to_value.items()})
33
+
34
+
35
+ def _snapshot_optional_step_outcome(step_outcome: StepOutcome | None) -> StepOutcome | None:
36
+ if step_outcome is None:
37
+ return None
38
+ return _snapshot_value(step_outcome) # type: ignore[return-value]
39
+
40
+
41
+ class OversightRejectedError(NighthawkError):
42
+ """Raised when oversight rejects a tool call or step commit."""
43
+
44
+
45
+ @dataclass(frozen=True)
46
+ class ToolCall:
47
+ execution_ref: ExecutionRef
48
+ tool_name: str
49
+ argument_name_to_value: Mapping[str, object]
50
+ processed_natural_program: str
51
+
52
+ def __post_init__(self) -> None:
53
+ object.__setattr__(
54
+ self,
55
+ "argument_name_to_value",
56
+ _snapshot_mapping(self.argument_name_to_value),
57
+ )
58
+
59
+
60
+ @dataclass(frozen=True)
61
+ class StepCommitProposal:
62
+ execution_ref: ExecutionRef
63
+ processed_natural_program: str
64
+ input_binding_name_to_value: Mapping[str, object]
65
+ proposed_step_outcome: StepOutcome
66
+ proposed_binding_name_to_value: Mapping[str, object]
67
+ allowed_step_kinds: tuple[StepKind, ...]
68
+ output_binding_name_set: frozenset[str]
69
+ binding_name_to_type: Mapping[str, object]
70
+
71
+ def __post_init__(self) -> None:
72
+ object.__setattr__(
73
+ self,
74
+ "input_binding_name_to_value",
75
+ _snapshot_mapping(self.input_binding_name_to_value),
76
+ )
77
+ object.__setattr__(
78
+ self,
79
+ "proposed_step_outcome",
80
+ _snapshot_value(self.proposed_step_outcome),
81
+ )
82
+ object.__setattr__(
83
+ self,
84
+ "proposed_binding_name_to_value",
85
+ _snapshot_mapping(self.proposed_binding_name_to_value),
86
+ )
87
+ object.__setattr__(
88
+ self,
89
+ "output_binding_name_set",
90
+ frozenset(self.output_binding_name_set),
91
+ )
92
+ object.__setattr__(
93
+ self,
94
+ "binding_name_to_type",
95
+ _snapshot_mapping(self.binding_name_to_type),
96
+ )
97
+
98
+
99
+ @dataclass(frozen=True)
100
+ class Accept:
101
+ reason: str | None = None
102
+
103
+
104
+ @dataclass(frozen=True)
105
+ class Reject:
106
+ reason: str
107
+
108
+
109
+ @dataclass(frozen=True)
110
+ class Rewrite:
111
+ rewritten_step_outcome: StepOutcome | None = None
112
+ rewritten_binding_name_to_value: Mapping[str, object] | None = None
113
+ reason: str | None = None
114
+
115
+ def __post_init__(self) -> None:
116
+ object.__setattr__(
117
+ self,
118
+ "rewritten_step_outcome",
119
+ _snapshot_optional_step_outcome(self.rewritten_step_outcome),
120
+ )
121
+ if self.rewritten_binding_name_to_value is not None:
122
+ object.__setattr__(
123
+ self,
124
+ "rewritten_binding_name_to_value",
125
+ _snapshot_mapping(self.rewritten_binding_name_to_value),
126
+ )
127
+ if self.rewritten_step_outcome is None and self.rewritten_binding_name_to_value is None:
128
+ raise ValueError("Rewrite must change rewritten_step_outcome or rewritten_binding_name_to_value")
129
+
130
+
131
+ type ToolCallDecision = Accept | Reject
132
+ type StepCommitDecision = Accept | Reject | Rewrite
133
+
134
+
135
+ @dataclass(frozen=True)
136
+ class Oversight:
137
+ inspect_tool_call: Callable[[ToolCall], ToolCallDecision] | None = None
138
+ inspect_step_commit: Callable[[StepCommitProposal], StepCommitDecision] | None = None
139
+
140
+
141
+ def record_oversight_decision(
142
+ *,
143
+ subject: str,
144
+ verdict: str,
145
+ execution_ref: ExecutionRef,
146
+ tool_name: str | None = None,
147
+ reason: str | None = None,
148
+ ) -> None:
149
+ current_span = get_current_span()
150
+ if not current_span.is_recording():
151
+ return
152
+ if execution_ref.step_id is None:
153
+ raise NighthawkError("Oversight decision events require ExecutionRef.step_id")
154
+
155
+ attributes: dict[str, str] = {
156
+ "run.id": execution_ref.run_id,
157
+ "scope.id": execution_ref.scope_id,
158
+ "step.id": execution_ref.step_id,
159
+ "nighthawk.oversight.subject": subject,
160
+ "nighthawk.oversight.verdict": verdict,
161
+ }
162
+ if tool_name:
163
+ attributes["tool.name"] = tool_name
164
+ if reason:
165
+ attributes["nighthawk.oversight.reason"] = reason
166
+
167
+ current_span.add_event("nighthawk.oversight.decision", attributes)
168
+
169
+
170
+ __all__ = [
171
+ "Accept",
172
+ "Oversight",
173
+ "OversightRejectedError",
174
+ "Reject",
175
+ "Rewrite",
176
+ "StepCommitDecision",
177
+ "StepCommitProposal",
178
+ "ToolCall",
179
+ "ToolCallDecision",
180
+ ]
@@ -8,9 +8,8 @@ Import directly from this module::
8
8
 
9
9
  from nighthawk.resilience import retrying, fallback, vote, timeout, budget, circuit_breaker
10
10
 
11
- These primitives are **not** re-exported from the top-level ``nighthawk``
12
- namespace. See [Patterns: Resilience patterns](https://kurusugawa-computer.github.io/nighthawk-python/patterns/#resilience-patterns)
13
- for usage patterns and composition examples.
11
+ The ``nighthawk.resilience`` module is available as ``nh.resilience`` after ``import nighthawk as nh``. Individual resilience primitives are **not** re-exported from the top-level ``nighthawk`` namespace.
12
+ See [Patterns: Resilience patterns](https://kurusugawa-computer.github.io/nighthawk-python/patterns/#resilience-patterns) for usage patterns and composition examples.
14
13
  """
15
14
 
16
15
  from __future__ import annotations