nighthawk-python 0.8.0__tar.gz → 0.9.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 (161) hide show
  1. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/.github/workflows/docs.yml +1 -1
  2. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/CHANGELOG.md +11 -0
  3. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/PKG-INFO +1 -1
  4. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/docs/api.md +0 -1
  5. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/docs/for-coding-agents.md +6 -2
  6. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/docs/runtime-configuration.md +89 -18
  7. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/docs/specification.md +9 -7
  8. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/pyproject.toml +1 -1
  9. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/__init__.py +0 -2
  10. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/configuration.py +0 -41
  11. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/runtime/scoping.py +42 -47
  12. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/execution/test_runtime.py +2 -2
  13. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/integration/test_llm_integration.py +2 -2
  14. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/public/test_public_api.py +105 -1
  15. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/uv.lock +75 -75
  16. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/.claude/rules/docs.md +0 -0
  17. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/.claude/rules/promptfoo.md +0 -0
  18. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/.claude/rules/src.md +0 -0
  19. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/.claude/rules/tests.md +0 -0
  20. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/.claude/settings.json +0 -0
  21. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/.claude/unset_envs.sh +0 -0
  22. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/.devcontainer/Dockerfile.devcontainer +0 -0
  23. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/.devcontainer/Dockerfile.litellm +0 -0
  24. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/.devcontainer/devcontainer.json +0 -0
  25. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/.devcontainer/docker-compose.yaml +0 -0
  26. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/.devcontainer/litellm-config.yaml +0 -0
  27. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/.github/dependabot.yml +0 -0
  28. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/.github/workflows/ci.yml +0 -0
  29. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/.github/workflows/publish.yml +0 -0
  30. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/.gitignore +0 -0
  31. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/.python-version +0 -0
  32. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/AGENTS.md +0 -0
  33. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/CLAUDE.md +0 -0
  34. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/CONTRIBUTING.md +0 -0
  35. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/LICENSE +0 -0
  36. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/README.md +0 -0
  37. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/docs/AGENTS.md +0 -0
  38. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/docs/assets/nighthawk_logo-128x128.png +0 -0
  39. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/docs/coding-agent-backends.md +0 -0
  40. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/docs/executors.md +0 -0
  41. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/docs/index.md +0 -0
  42. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/docs/natural-blocks.md +0 -0
  43. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/docs/patterns.md +0 -0
  44. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/docs/philosophy.md +0 -0
  45. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/docs/pydantic-ai-providers.md +0 -0
  46. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/docs/quickstart.md +0 -0
  47. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/docs/roadmap.md +0 -0
  48. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/docs/verification.md +0 -0
  49. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/evals/promptfoo/assertions/__init__.py +0 -0
  50. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/evals/promptfoo/assertions/binding_value.py +0 -0
  51. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/evals/promptfoo/assertions/outcome_kind.py +0 -0
  52. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/evals/promptfoo/assertions/raise_message.py +0 -0
  53. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/evals/promptfoo/evidence/2026-03-26-baseline-prompt-ab.md +0 -0
  54. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/evals/promptfoo/evidence/2026-03-26-baseline-regression.md +0 -0
  55. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/evals/promptfoo/evidence/2026-03-26-baseline-suffix-ab.md +0 -0
  56. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/evals/promptfoo/promptfooconfig-agents.yaml +0 -0
  57. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/evals/promptfoo/promptfooconfig-prompt-ab.yaml +0 -0
  58. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/evals/promptfoo/promptfooconfig-suffix-ab.yaml +0 -0
  59. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/evals/promptfoo/promptfooconfig.yaml +0 -0
  60. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/evals/promptfoo/prompts/eval_coding_agent.txt +0 -0
  61. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/evals/promptfoo/prompts/eval_default.txt +0 -0
  62. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/evals/promptfoo/prompts/eval_mutation_aware.txt +0 -0
  63. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/evals/promptfoo/prompts/eval_sequenced.txt +0 -0
  64. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/evals/promptfoo/provider.py +0 -0
  65. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/evals/promptfoo/research-result.md +0 -0
  66. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/evals/promptfoo/test_cases/binding_operations.yaml +0 -0
  67. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/evals/promptfoo/test_cases/edge_cases.yaml +0 -0
  68. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/evals/promptfoo/test_cases/loop_outcomes.yaml +0 -0
  69. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/evals/promptfoo/test_cases/multi_step.yaml +0 -0
  70. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/evals/promptfoo/test_cases/null_handling.yaml +0 -0
  71. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/evals/promptfoo/test_cases/outcome_kinds.yaml +0 -0
  72. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/evals/promptfoo/test_cases/tool_selection.yaml +0 -0
  73. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/mkdocs.yml +0 -0
  74. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/pyrightconfig.json +0 -0
  75. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/AGENTS.md +0 -0
  76. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/backends/__init__.py +0 -0
  77. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/backends/base.py +0 -0
  78. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/backends/claude_code_cli.py +0 -0
  79. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/backends/claude_code_sdk.py +0 -0
  80. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/backends/claude_code_settings.py +0 -0
  81. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/backends/codex.py +0 -0
  82. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/backends/mcp_boundary.py +0 -0
  83. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/backends/mcp_server.py +0 -0
  84. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/backends/tool_bridge.py +0 -0
  85. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/errors.py +0 -0
  86. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/identifier_path.py +0 -0
  87. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/json_renderer.py +0 -0
  88. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/natural/__init__.py +0 -0
  89. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/natural/blocks.py +0 -0
  90. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/natural/decorator.py +0 -0
  91. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/natural/transform.py +0 -0
  92. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/resilience/__init__.py +0 -0
  93. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/resilience/_budget.py +0 -0
  94. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/resilience/_circuit_breaker.py +0 -0
  95. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/resilience/_fallback.py +0 -0
  96. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/resilience/_retry.py +0 -0
  97. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/resilience/_timeout.py +0 -0
  98. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/resilience/_vote.py +0 -0
  99. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/runtime/__init__.py +0 -0
  100. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/runtime/async_bridge.py +0 -0
  101. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/runtime/prompt.py +0 -0
  102. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/runtime/runner.py +0 -0
  103. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/runtime/step_context.py +0 -0
  104. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/runtime/step_contract.py +0 -0
  105. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/runtime/step_executor.py +0 -0
  106. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/runtime/tool_calls.py +0 -0
  107. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/testing.py +0 -0
  108. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/tools/__init__.py +0 -0
  109. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/tools/assignment.py +0 -0
  110. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/tools/contracts.py +0 -0
  111. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/tools/execution.py +0 -0
  112. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/tools/provided.py +0 -0
  113. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/tools/registry.py +0 -0
  114. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/ulid.py +0 -0
  115. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/AGENTS.md +0 -0
  116. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/__init__.py +0 -0
  117. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/backends/__init__.py +0 -0
  118. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/backends/test_claude_code_cli.py +0 -0
  119. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/backends/test_claude_code_sdk.py +0 -0
  120. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/backends/test_codex.py +0 -0
  121. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/conftest.py +0 -0
  122. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/docs/__init__.py +0 -0
  123. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/docs/test_coding_agent_examples.py +0 -0
  124. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/docs/test_docs_architecture.py +0 -0
  125. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/docs/test_prompt_examples.py +0 -0
  126. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/execution/__init__.py +0 -0
  127. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/execution/prompt_test_helpers.py +0 -0
  128. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/execution/stub_executor.py +0 -0
  129. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/execution/test_execution_outcome_prompt_fragment.py +0 -0
  130. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/execution/test_globals_prompt.py +0 -0
  131. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/execution/test_infer_binding_types.py +0 -0
  132. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/execution/test_natural_block_ordering.py +0 -0
  133. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/execution/test_natural_traceback.py +0 -0
  134. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/execution/test_variables_prompt.py +0 -0
  135. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/integration/__init__.py +0 -0
  136. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/integration/skip_helpers.py +0 -0
  137. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/integration/test_carry_pattern.py +0 -0
  138. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/integration/test_claude_code_cli_integration.py +0 -0
  139. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/integration/test_claude_code_sdk_integration.py +0 -0
  140. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/integration/test_codex_integration.py +0 -0
  141. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/natural/__init__.py +0 -0
  142. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/natural/test_blocks.py +0 -0
  143. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/public/__init__.py +0 -0
  144. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/public/test_readme_example.py +0 -0
  145. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/public/test_usage_meter.py +0 -0
  146. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/resilience/__init__.py +0 -0
  147. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/resilience/test_budget.py +0 -0
  148. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/resilience/test_circuit_breaker.py +0 -0
  149. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/resilience/test_composition.py +0 -0
  150. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/resilience/test_fallback.py +0 -0
  151. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/resilience/test_retry.py +0 -0
  152. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/resilience/test_timeout.py +0 -0
  153. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/resilience/test_vote.py +0 -0
  154. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/test_renderer.py +0 -0
  155. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/test_testing.py +0 -0
  156. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/tools/__init__.py +0 -0
  157. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/tools/test_assignment_async.py +0 -0
  158. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/tools/test_contracts.py +0 -0
  159. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/tools/test_provided_async.py +0 -0
  160. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/tools/test_registry.py +0 -0
  161. {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/tools/test_tool_boundary.py +0 -0
@@ -46,4 +46,4 @@ jobs:
46
46
  url: ${{ steps.deployment.outputs.page_url }}
47
47
  steps:
48
48
  - id: deployment
49
- uses: actions/deploy-pages@v4
49
+ uses: actions/deploy-pages@v5
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ### Changed
11
+ - Redesigned `nh.scope()` around `mode` semantics (`"inherit"` default, `"replace"` for explicit replacement).
12
+ - Scope prompt suffix arguments now use list-based forms: `system_prompt_suffix_fragments` and `user_prompt_suffix_fragments`.
13
+
14
+ ### Removed
15
+ - Removed `step_executor_configuration_patch` from `nh.scope()`.
16
+ - Removed `StepExecutorConfigurationPatch` from the public API.
17
+
18
+ ### Fixed
19
+ - Clarified and aligned docs/tests for scope merge vs replace behavior.
20
+
10
21
  ## [0.8.0]
11
22
 
12
23
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nighthawk-python
3
- Version: 0.8.0
3
+ Version: 0.9.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/
@@ -14,7 +14,6 @@
14
14
  - scope
15
15
  - AgentStepExecutor
16
16
  - StepExecutorConfiguration
17
- - StepExecutorConfigurationPatch
18
17
  - StepPromptTemplates
19
18
  - StepContextLimits
20
19
  - JsonableValue
@@ -146,8 +146,12 @@ with nh.run(fast_executor):
146
146
  report = write_analysis_report(ticket_text, product_summary)
147
147
  ```
148
148
 
149
- `implicit_references` can inject global helper functions as block capabilities.
150
- Nested scopes still merge additively (set union by key).
149
+ - `implicit_references` inject helper functions as block capabilities.
150
+ - `nh.scope()` must run inside `nh.run()`; it keeps `run_id` and creates a new `scope_id`.
151
+ - `mode` affects only `system_prompt_suffix_fragments`, `user_prompt_suffix_fragments`, and `implicit_references`.
152
+ - `inherit`: suffix lists append; `implicit_references` merge with conflict check (same key + different object => `NighthawkError`).
153
+ - `replace`: explicit list/mapping fully replace; `[]`/`{}` clear; `None` keeps inherited value.
154
+ - `step_executor` / `step_executor_configuration` are resolved independently of `mode`.
151
155
 
152
156
  ## 4. The standard contract shape
153
157
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  > This page assumes you have completed [Executors](executors.md).
4
4
 
5
- This page covers how to configure execution at runtime: scoping, configuration patching, prompt suffix fragments, context limits, JSON rendering, and execution identity. These settings are independent of executor choice and apply equally to Pydantic AI providers and coding agent backends.
5
+ This page covers how to configure execution at runtime: scoping modes, prompt suffix fragments, context limits, JSON rendering, and execution identity. These settings are independent of executor choice and apply equally to Pydantic AI providers and coding agent backends.
6
6
 
7
7
  ## Scoped overrides with `nh.scope()`
8
8
 
@@ -11,17 +11,20 @@ Use `nh.scope()` to override execution settings within an existing run. Each sco
11
11
  ```py
12
12
  with nh.run(step_executor):
13
13
 
14
- # Override model for a specific section
14
+ # Inherit mode (default): merge/append into current scope state
15
15
  with nh.scope(
16
- step_executor_configuration_patch=nh.StepExecutorConfigurationPatch(
16
+ step_executor_configuration=nh.StepExecutorConfiguration(
17
17
  model="openai-responses:gpt-5.4-mini",
18
18
  ),
19
19
  ) as scoped_executor:
20
20
  expensive_analysis(data)
21
21
 
22
- # Append a system prompt suffix for a section
22
+ # Replace mode: replace only explicitly provided values
23
+ # None means "no change". [] / {} means "clear".
23
24
  with nh.scope(
24
- system_prompt_suffix_fragment="Always respond in formal English.",
25
+ mode="replace",
26
+ system_prompt_suffix_fragments=["Always respond in formal English."],
27
+ implicit_references={},
25
28
  ):
26
29
  formal_summary(text)
27
30
 
@@ -29,31 +32,35 @@ with nh.run(step_executor):
29
32
  with nh.scope(step_executor=another_executor):
30
33
  specialized_step(data)
31
34
 
32
- # Add implicit global references for this scope (merged across nested scopes)
35
+ # Add implicit global references for this scope
33
36
  with nh.scope(implicit_references={"search_repository": search_repository}):
34
37
  typed_labeling_step(ticket_text)
35
38
  ```
36
39
 
37
40
  Parameters:
38
41
 
42
+ - `mode`: scope composition mode. Default: `"inherit"`.
39
43
  - `step_executor_configuration`: replace the entire configuration.
40
- - `step_executor_configuration_patch`: partially override specific fields.
41
44
  - `step_executor`: replace the step executor entirely.
42
- - `system_prompt_suffix_fragment`: append text to the system prompt for the scope.
43
- - `user_prompt_suffix_fragment`: append text to the user prompt for the scope.
44
- - `implicit_references`: add implicit global references for this scope as a name-to-value mapping. Nested scopes merge references additively (set union by key).
45
+ - `system_prompt_suffix_fragments`: scope-level system suffix fragments.
46
+ - `user_prompt_suffix_fragments`: scope-level user suffix fragments.
47
+ - `implicit_references`: scope-level implicit global references.
45
48
 
46
- Use `step_executor_configuration` when you want a full configuration replacement for a scope.
47
- Use `step_executor_configuration_patch` for targeted changes (for example, switching only the model).
48
- Use `step_executor` to swap the executor implementation for that scope.
49
- Use `system_prompt_suffix_fragment` and `user_prompt_suffix_fragment` to append one-off scope-level prompt text.
50
- Use `implicit_references` when a step should always expose specific globals even without explicit `<name>` bindings.
49
+ Mode semantics:
50
+
51
+ - `mode="inherit"` (default):
52
+ - `system_prompt_suffix_fragments` and `user_prompt_suffix_fragments` are appended.
53
+ - `implicit_references` are merged additively with conflict checks.
54
+ - `mode="replace"`:
55
+ - `None` means no change.
56
+ - Explicit `[]` or `{}` clears inherited list/dict values.
57
+ - Explicit list/dict values (for example `[e1, e2]` or `{k1: v1, k2: v2}`) fully replace inherited values.
51
58
 
52
59
  The context manager yields the resolved `StepExecutor` for the scope.
53
60
 
54
- `StepExecutorConfiguration` also accepts `system_prompt_suffix_fragments` and `user_prompt_suffix_fragments` (tuples of strings) as baseline suffix fragments for the whole run. Scope-level fragments are appended after configuration-level fragments.
61
+ `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.
55
62
 
56
- ## Additive scoped implicit references
63
+ ## Scoped implicit references
57
64
 
58
65
  `implicit_references` can inject global helper functions as step capabilities:
59
66
 
@@ -65,8 +72,72 @@ with nh.run(step_executor):
65
72
  triage_issue(ticket_text)
66
73
  ```
67
74
 
68
- Nested scopes merge names additively (set union by key).
75
+ In `mode="inherit"` (default), nested scopes merge additively with conflict checks.
76
+ In `mode="replace"`, explicit mappings fully replace inherited mappings.
77
+
78
+ ```py
79
+ with nh.run(step_executor), nh.scope(implicit_references={"parent": search_repository}):
80
+ with nh.scope(mode="replace", implicit_references={}):
81
+ triage_issue(ticket_text)
82
+ ```
83
+
84
+ The inner scope above clears inherited implicit references.
85
+
86
+ ```py
87
+ with nh.scope(mode="replace", implicit_references={"child": search_repository}):
88
+ triage_issue(ticket_text)
89
+ ```
90
+
91
+ This fully replaces inherited references with the provided mapping.
92
+
93
+ ```py
94
+ with nh.scope(mode="replace", implicit_references=None):
95
+ triage_issue(ticket_text)
96
+ ```
97
+
98
+ This keeps inherited references unchanged.
99
+
100
+ ## Prompt suffix fragments in scopes
101
+
102
+ Use list values when setting scope-level suffix fragments:
103
+
104
+ ```py
105
+ with nh.run(step_executor):
106
+ with nh.scope(system_prompt_suffix_fragments=["Use concise answers."]):
107
+ summarize_ticket(ticket_text)
108
+ ```
109
+
110
+ `user_prompt_suffix_fragments` follows the same rules.
111
+
112
+ ```py
113
+ with nh.scope(user_prompt_suffix_fragments=["Focus on actionable output."]):
114
+ summarize_ticket(ticket_text)
115
+ ```
116
+
117
+ In `mode="inherit"`, provided lists are appended.
118
+ In `mode="replace"`, provided lists fully replace inherited lists.
119
+
120
+ ```py
121
+ with nh.run(step_executor), nh.scope(system_prompt_suffix_fragments=["parent"]):
122
+ with nh.scope(mode="replace", system_prompt_suffix_fragments=["child_1", "child_2"]):
123
+ summarize_ticket(ticket_text)
124
+ ```
125
+
126
+ Pass `[]` to clear inherited fragments.
127
+
128
+ ```py
129
+ with nh.scope(mode="replace", system_prompt_suffix_fragments=[]):
130
+ summarize_ticket(ticket_text)
131
+ ```
132
+
133
+ Pass `None` to keep inherited fragments unchanged.
134
+
135
+ ```py
136
+ with nh.scope(mode="replace", system_prompt_suffix_fragments=None):
137
+ summarize_ticket(ticket_text)
138
+ ```
69
139
 
140
+ The same `replace` semantics apply to `user_prompt_suffix_fragments`.
70
141
  ## Mixing executors
71
142
 
72
143
  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:
@@ -91,9 +91,6 @@ The condensed coding agent guide (`for-coding-agents.md`) is a derivative docume
91
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`).
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
- - `StepExecutorConfigurationPatch`
95
- - Partial override object for scoped configuration updates.
96
- - Supports patching model, model settings, templates, limits, renderer style, tokenizer encoding, and prompt suffix fragment tuples.
97
94
 
98
95
  ### 5.3. Supporting types
99
96
 
@@ -589,13 +586,18 @@ API:
589
586
  - Uses provided `run_id` when given; otherwise generates a new `run_id` (trace root).
590
587
  - Always generates a fresh `scope_id`.
591
588
  - Can be used even when no step executor is currently set.
592
- - `nighthawk.scope(*, step_executor_configuration: StepExecutorConfiguration | None = None, step_executor_configuration_patch: StepExecutorConfigurationPatch | None = None, step_executor: StepExecutor | None = None, system_prompt_suffix_fragment: str | None = None, user_prompt_suffix_fragment: str | None = None) -> Iterator[StepExecutor]`
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]`
593
590
  - Enter a nested scope within the current run.
594
591
  - Requires an existing step executor.
595
592
  - Generates a new `scope_id` (keeps the current `run_id`).
596
- - Only specified fields are overridden for the duration of the `with`.
597
- - `system_prompt_suffix_fragment`: optional string appended to the system prompt for the duration of the scope.
598
- - `user_prompt_suffix_fragment`: optional string appended to the user prompt for the duration of the scope.
593
+ - `mode="inherit"` (default):
594
+ - Appends prompt suffix fragment lists.
595
+ - Merges `implicit_references` additively with conflict checks.
596
+ - `mode="replace"`:
597
+ - Replaces provided list/mapping values.
598
+ - `None` means no change.
599
+ - Explicit `[]` / `{}` clears inherited list/mapping values.
600
+ - Explicit list/mapping values (for example `[e1, e2]` or `{k1: v1, k2: v2}`) fully replace inherited values.
599
601
  - Yields the resolved `StepExecutor` for the scope.
600
602
  - `nighthawk.get_step_executor() -> StepExecutor`
601
603
  - Get the current step executor. Raises if unset.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "nighthawk-python"
3
- version = "0.8.0"
3
+ version = "0.9.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"
@@ -3,7 +3,6 @@ from __future__ import annotations
3
3
  from .configuration import (
4
4
  StepContextLimits,
5
5
  StepExecutorConfiguration,
6
- StepExecutorConfigurationPatch,
7
6
  StepPromptTemplates,
8
7
  )
9
8
  from .errors import (
@@ -32,7 +31,6 @@ __all__ = [
32
31
  "StepContextLimits",
33
32
  "StepExecutor",
34
33
  "StepExecutorConfiguration",
35
- "StepExecutorConfigurationPatch",
36
34
  "StepPromptTemplates",
37
35
  "ToolEvaluationError",
38
36
  "ToolRegistrationError",
@@ -160,44 +160,3 @@ class StepExecutorConfiguration(BaseModel):
160
160
  return tiktoken.encoding_for_model(model_name)
161
161
  except Exception:
162
162
  return tiktoken.get_encoding("o200k_base")
163
-
164
-
165
- class StepExecutorConfigurationPatch(BaseModel):
166
- """Partial override for StepExecutorConfiguration.
167
-
168
- Non-None fields replace the corresponding fields in the target configuration.
169
-
170
- Attributes:
171
- model: Model identifier override.
172
- model_settings: Model settings override. Accepts a dict or a
173
- backend-specific BaseModel instance (auto-converted to dict).
174
- prompts: Prompt templates override.
175
- context_limits: Context limits override.
176
- json_renderer_style: JSON renderer style override.
177
- tokenizer_encoding: Tokenizer encoding override.
178
- system_prompt_suffix_fragments: System prompt suffix fragments override.
179
- user_prompt_suffix_fragments: User prompt suffix fragments override.
180
- """
181
-
182
- model_config = ConfigDict(extra="forbid", frozen=True)
183
-
184
- model: str | None = None
185
- model_settings: dict[str, Any] | BaseModel | None = None
186
-
187
- @field_validator("model_settings", mode="before")
188
- @classmethod
189
- def _normalize_model_settings(cls, value: Any) -> dict[str, Any] | None:
190
- if isinstance(value, BaseModel):
191
- return value.model_dump()
192
- return value
193
-
194
- prompts: StepPromptTemplates | None = None
195
- context_limits: StepContextLimits | None = None
196
- json_renderer_style: JsonRendererStyle | None = None
197
- tokenizer_encoding: str | None = None
198
- system_prompt_suffix_fragments: tuple[str, ...] | None = None
199
- user_prompt_suffix_fragments: tuple[str, ...] | None = None
200
-
201
- def apply_to(self, configuration: StepExecutorConfiguration) -> StepExecutorConfiguration:
202
- """Apply non-None fields to the given configuration and return a new copy."""
203
- return configuration.model_copy(update=self.model_dump(exclude_none=True))
@@ -2,17 +2,17 @@ from __future__ import annotations
2
2
 
3
3
  import importlib.metadata
4
4
  import threading
5
- from collections.abc import Iterator, Mapping
5
+ from collections.abc import Iterator, Mapping, Sequence
6
6
  from contextlib import contextmanager
7
7
  from contextvars import ContextVar
8
8
  from copy import copy
9
9
  from dataclasses import dataclass, replace
10
- from typing import TYPE_CHECKING, Any
10
+ from typing import TYPE_CHECKING, Any, Literal
11
11
 
12
12
  from opentelemetry.trace import Span, get_tracer_provider
13
13
  from pydantic_ai.usage import RunUsage
14
14
 
15
- from ..configuration import StepExecutorConfiguration, StepExecutorConfigurationPatch
15
+ from ..configuration import StepExecutorConfiguration
16
16
  from ..errors import NighthawkError
17
17
  from ..tools.registry import tool_scope
18
18
  from ..ulid import generate_ulid
@@ -295,24 +295,28 @@ def run(
295
295
  @contextmanager
296
296
  def scope(
297
297
  *,
298
+ mode: Literal["inherit", "replace"] = "inherit",
298
299
  step_executor_configuration: StepExecutorConfiguration | None = None,
299
- step_executor_configuration_patch: StepExecutorConfigurationPatch | None = None,
300
300
  step_executor: StepExecutor | None = None,
301
- system_prompt_suffix_fragment: str | None = None,
302
- user_prompt_suffix_fragment: str | None = None,
301
+ system_prompt_suffix_fragments: Sequence[str] | None = None,
302
+ user_prompt_suffix_fragments: Sequence[str] | None = None,
303
303
  implicit_references: ImplicitReferenceNameToValue | None = None,
304
304
  ) -> Iterator[StepExecutor]:
305
- """Open a nested scope that can override the step executor or its configuration.
305
+ """Open a nested scope that can override the active execution context.
306
306
 
307
- Must be called inside an active run context. Creates a new scope_id while inheriting the run_id from the parent context.
307
+ Must be called inside an active run context. Creates a new ``scope_id`` while inheriting the ``run_id`` from the parent context.
308
308
 
309
309
  Args:
310
+ mode: Scope composition mode. ``"inherit"`` appends/merges values into the current scope. ``"replace"`` fully replaces provided list/dict values.
311
+ In both modes, ``None`` means no change.
310
312
  step_executor_configuration: Full replacement configuration for the step executor.
311
- step_executor_configuration_patch: Partial override applied on top of the current configuration.
312
313
  step_executor: Replacement step executor for this scope.
313
- system_prompt_suffix_fragment: Additional text appended to the system prompt.
314
- user_prompt_suffix_fragment: Additional text appended to the user prompt.
315
- implicit_references: Additional implicit global references merged into this scope and inherited by nested scopes.
314
+ system_prompt_suffix_fragments: Additional system prompt suffix fragments.
315
+ In ``mode="inherit"``, fragments are appended. In ``mode="replace"``, provided fragments fully replace the current fragments.
316
+ user_prompt_suffix_fragments: Additional user prompt suffix fragments.
317
+ In ``mode="inherit"``, fragments are appended. In ``mode="replace"``, provided fragments fully replace the current fragments.
318
+ implicit_references: Implicit global references for this scope.
319
+ In ``mode="inherit"``, values are merged with conflict checks. In ``mode="replace"``, provided mappings fully replace the current mapping.
316
320
 
317
321
  Yields:
318
322
  The step executor active within this scope.
@@ -321,9 +325,9 @@ def scope(
321
325
  ```python
322
326
  with nighthawk.run(executor):
323
327
  with nighthawk.scope(
324
- step_executor_configuration_patch=StepExecutorConfigurationPatch(
325
- model="openai:gpt-5.4-mini",
326
- ),
328
+ mode="replace",
329
+ implicit_references={},
330
+ system_prompt_suffix_fragments=["Use concise answers."],
327
331
  ) as scoped_executor:
328
332
  result = my_natural_function()
329
333
  ```
@@ -332,31 +336,13 @@ def scope(
332
336
  current_execution_context = get_execution_context()
333
337
 
334
338
  next_step_executor = current_step_executor
335
-
336
339
  if step_executor is not None:
337
340
  next_step_executor = step_executor
338
341
 
339
- has_configuration_update = any(
340
- value is not None
341
- for value in (
342
- step_executor_configuration,
343
- step_executor_configuration_patch,
344
- )
345
- )
346
-
347
- if has_configuration_update:
348
- current_agent_step_executor = _resolve_agent_step_executor(next_step_executor)
349
- next_configuration = current_agent_step_executor.configuration
350
-
351
- if step_executor_configuration is not None:
352
- next_configuration = step_executor_configuration
353
-
354
- if step_executor_configuration_patch is not None:
355
- next_configuration = step_executor_configuration_patch.apply_to(next_configuration)
356
-
342
+ if step_executor_configuration is not None:
357
343
  next_step_executor = _replace_step_executor_with_configuration(
358
344
  next_step_executor,
359
- configuration=next_configuration,
345
+ configuration=step_executor_configuration,
360
346
  )
361
347
 
362
348
  next_execution_context = replace(
@@ -364,27 +350,36 @@ def scope(
364
350
  scope_id=generate_ulid(),
365
351
  )
366
352
 
367
- next_system_fragments = _system_prompt_suffix_fragments_var.get()
368
- next_user_fragments = _user_prompt_suffix_fragments_var.get()
353
+ next_system_prompt_suffix_fragments = _system_prompt_suffix_fragments_var.get()
354
+ next_user_prompt_suffix_fragments = _user_prompt_suffix_fragments_var.get()
369
355
  next_implicit_reference_name_to_value = _implicit_reference_name_to_value_var.get()
370
356
 
371
- if system_prompt_suffix_fragment is not None:
372
- next_system_fragments = (*next_system_fragments, system_prompt_suffix_fragment)
357
+ if system_prompt_suffix_fragments is not None:
358
+ if mode == "inherit":
359
+ next_system_prompt_suffix_fragments = (*next_system_prompt_suffix_fragments, *system_prompt_suffix_fragments)
360
+ else:
361
+ next_system_prompt_suffix_fragments = tuple(system_prompt_suffix_fragments)
373
362
 
374
- if user_prompt_suffix_fragment is not None:
375
- next_user_fragments = (*next_user_fragments, user_prompt_suffix_fragment)
363
+ if user_prompt_suffix_fragments is not None:
364
+ if mode == "inherit":
365
+ next_user_prompt_suffix_fragments = (*next_user_prompt_suffix_fragments, *user_prompt_suffix_fragments)
366
+ else:
367
+ next_user_prompt_suffix_fragments = tuple(user_prompt_suffix_fragments)
376
368
 
377
369
  if implicit_references is not None:
378
- next_implicit_reference_name_to_value = _merge_implicit_reference_name_to_value_with_conflict_check(
379
- next_implicit_reference_name_to_value,
380
- implicit_references,
381
- )
370
+ if mode == "inherit":
371
+ next_implicit_reference_name_to_value = _merge_implicit_reference_name_to_value_with_conflict_check(
372
+ next_implicit_reference_name_to_value,
373
+ implicit_references,
374
+ )
375
+ else:
376
+ next_implicit_reference_name_to_value = dict(implicit_references)
382
377
 
383
378
  with tool_scope():
384
379
  step_executor_token = _step_executor_var.set(next_step_executor)
385
380
  execution_context_token = _execution_context_var.set(next_execution_context)
386
- system_fragments_token = _system_prompt_suffix_fragments_var.set(next_system_fragments)
387
- user_fragments_token = _user_prompt_suffix_fragments_var.set(next_user_fragments)
381
+ system_fragments_token = _system_prompt_suffix_fragments_var.set(next_system_prompt_suffix_fragments)
382
+ user_fragments_token = _user_prompt_suffix_fragments_var.set(next_user_prompt_suffix_fragments)
388
383
  implicit_reference_name_to_value_token = _implicit_reference_name_to_value_var.set(next_implicit_reference_name_to_value)
389
384
  try:
390
385
  with span(
@@ -616,7 +616,7 @@ def test_natural_function_can_override_step_executor_configuration_model_within_
616
616
  """
617
617
  first_model_identifier = observed_model_identifier # noqa: F821 # pyright: ignore[reportUndefinedVariable]
618
618
 
619
- with nh.scope(step_executor_configuration_patch=nh.StepExecutorConfigurationPatch(model="openai-responses:gpt-5.4-mini")):
619
+ with nh.scope(step_executor_configuration=nh.StepExecutorConfiguration(model="openai-responses:gpt-5.4-mini")):
620
620
  """natural
621
621
  <:observed_model_identifier>
622
622
  Record the current model identifier.
@@ -653,7 +653,7 @@ def test_natural_function_rejects_step_executor_configuration_updates_for_non_ag
653
653
 
654
654
  @nh.natural_function
655
655
  def f() -> None:
656
- with nh.scope(step_executor_configuration_patch=nh.StepExecutorConfigurationPatch(model="openai-responses:gpt-5.4-mini")):
656
+ with nh.scope(step_executor_configuration=nh.StepExecutorConfiguration(model="openai-responses:gpt-5.4-mini")):
657
657
  pass
658
658
 
659
659
  with pytest.raises(NighthawkError, match="AgentStepExecutor"):
@@ -74,7 +74,7 @@ def test_system_prompt_suffix_fragments():
74
74
  ),
75
75
  )
76
76
 
77
- with nh.run(step_executor), nh.scope(system_prompt_suffix_fragment="Hello suffix"):
77
+ with nh.run(step_executor), nh.scope(system_prompt_suffix_fragments=["Hello suffix"]):
78
78
 
79
79
  @nh.natural_function
80
80
  def f() -> int:
@@ -97,7 +97,7 @@ def test_user_prompt_suffix_fragments():
97
97
  ),
98
98
  )
99
99
 
100
- with nh.run(step_executor), nh.scope(user_prompt_suffix_fragment="Hello suffix"):
100
+ with nh.run(step_executor), nh.scope(user_prompt_suffix_fragments=["Hello suffix"]):
101
101
 
102
102
  @nh.natural_function
103
103
  def f() -> int:
@@ -145,7 +145,7 @@ def test_scope_implicit_references_additive_across_nested_scopes() -> None:
145
145
  assert runtime_scoping.get_implicit_reference_name_to_value() == {}
146
146
 
147
147
 
148
- def test_scope_implicit_references_rejects_conflicting_values() -> None:
148
+ def test_scope_implicit_references_rejects_conflicting_values_in_inherit_mode() -> None:
149
149
  with (
150
150
  nh.run(StubExecutor()),
151
151
  nh.scope(implicit_references={"shared": object()}),
@@ -163,6 +163,110 @@ def test_scope_implicit_references_accepts_mapping() -> None:
163
163
  assert set(runtime_scoping.get_implicit_reference_name_to_value().keys()) == {"one", "two"}
164
164
 
165
165
 
166
+ def test_scope_replace_mode_none_keeps_inherited_implicit_references() -> None:
167
+ parent_function = object()
168
+
169
+ with nh.run(StubExecutor()), nh.scope(implicit_references={"parent": parent_function}), nh.scope(mode="replace", implicit_references=None):
170
+ assert runtime_scoping.get_implicit_reference_name_to_value() == {"parent": parent_function}
171
+
172
+
173
+ def test_scope_replace_mode_explicit_empty_mapping_clears_implicit_references() -> None:
174
+ parent_function = object()
175
+
176
+ with nh.run(StubExecutor()), nh.scope(implicit_references={"parent": parent_function}), nh.scope(mode="replace", implicit_references={}):
177
+ assert runtime_scoping.get_implicit_reference_name_to_value() == {}
178
+
179
+
180
+ def test_scope_replace_mode_replaces_implicit_references_mapping() -> None:
181
+ parent_function = object()
182
+ replacement_function = object()
183
+
184
+ with (
185
+ nh.run(StubExecutor()),
186
+ nh.scope(implicit_references={"parent": parent_function}),
187
+ nh.scope(mode="replace", implicit_references={"replacement": replacement_function}),
188
+ ):
189
+ assert runtime_scoping.get_implicit_reference_name_to_value() == {"replacement": replacement_function}
190
+
191
+
192
+ def test_scope_replace_mode_none_keeps_inherited_prompt_suffix_fragments() -> None:
193
+ with nh.run(StubExecutor()), nh.scope(system_prompt_suffix_fragments=["parent"]), nh.scope(mode="replace", system_prompt_suffix_fragments=None):
194
+ assert runtime_scoping.get_system_prompt_suffix_fragments() == ("parent",)
195
+
196
+
197
+ def test_scope_replace_mode_explicit_empty_prompt_suffix_fragments_clears() -> None:
198
+ with nh.run(StubExecutor()), nh.scope(system_prompt_suffix_fragments=["parent"]), nh.scope(mode="replace", system_prompt_suffix_fragments=[]):
199
+ assert runtime_scoping.get_system_prompt_suffix_fragments() == ()
200
+
201
+
202
+ def test_scope_replace_mode_replaces_prompt_suffix_fragments() -> None:
203
+ with (
204
+ nh.run(StubExecutor()),
205
+ nh.scope(system_prompt_suffix_fragments=["parent"]),
206
+ nh.scope(mode="replace", system_prompt_suffix_fragments=["child_1", "child_2"]),
207
+ ):
208
+ assert runtime_scoping.get_system_prompt_suffix_fragments() == ("child_1", "child_2")
209
+
210
+
211
+ def test_scope_mode_defaults_to_inherit() -> None:
212
+ with nh.run(StubExecutor()), nh.scope(implicit_references={"parent": object()}), nh.scope(implicit_references={"child": object()}):
213
+ assert set(runtime_scoping.get_implicit_reference_name_to_value().keys()) == {"parent", "child"}
214
+
215
+
216
+ def test_scope_replace_mode_replaces_step_executor_configuration() -> None:
217
+ class FakeAgent:
218
+ def run_sync(self, *args, **kwargs): # type: ignore[no-untyped-def]
219
+ _ = args
220
+ _ = kwargs
221
+ raise AssertionError
222
+
223
+ first_configuration = nh.StepExecutorConfiguration(model="openai-responses:gpt-5.4-nano")
224
+ second_configuration = nh.StepExecutorConfiguration(model="openai-responses:gpt-5.4-mini")
225
+
226
+ with (
227
+ nh.run(nh.AgentStepExecutor.from_agent(agent=FakeAgent(), configuration=first_configuration)),
228
+ nh.scope(mode="replace", step_executor_configuration=second_configuration),
229
+ ):
230
+ scoped_step_executor = nh.get_step_executor()
231
+ assert isinstance(scoped_step_executor, AgentStepExecutor)
232
+ assert scoped_step_executor.configuration == second_configuration
233
+
234
+
235
+ def test_scope_replace_mode_none_keeps_inherited_step_executor() -> None:
236
+ with nh.run(StubExecutor()):
237
+ parent_step_executor = nh.get_step_executor()
238
+ with nh.scope(mode="replace", step_executor=None):
239
+ child_step_executor = nh.get_step_executor()
240
+ assert child_step_executor is parent_step_executor
241
+
242
+
243
+ def test_scope_inherit_mode_appends_prompt_suffix_fragments() -> None:
244
+ with (
245
+ nh.run(StubExecutor()),
246
+ nh.scope(system_prompt_suffix_fragments=["parent"]),
247
+ nh.scope(mode="inherit", system_prompt_suffix_fragments=["child"]),
248
+ ):
249
+ assert runtime_scoping.get_system_prompt_suffix_fragments() == ("parent", "child")
250
+
251
+
252
+ def test_scope_inherit_mode_rejects_invalid_implicit_reference_conflicts() -> None:
253
+ with (
254
+ nh.run(StubExecutor()),
255
+ nh.scope(implicit_references={"shared": object()}),
256
+ pytest.raises(NighthawkError, match="Conflict for implicit reference"),
257
+ nh.scope(mode="inherit", implicit_references={"shared": object()}),
258
+ ):
259
+ pass
260
+
261
+
262
+ def test_scope_mode_validation_is_enforced_by_typing_contract() -> None:
263
+ with nh.run(StubExecutor()):
264
+ with nh.scope(mode="inherit"):
265
+ pass
266
+ with nh.scope(mode="replace"):
267
+ pass
268
+
269
+
166
270
  def test_run_configuration_model_default_applies():
167
271
  configuration = nh.StepExecutorConfiguration()
168
272
  assert configuration.model == "openai-responses:gpt-5.4-nano"