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.
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/.github/workflows/docs.yml +1 -1
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/CHANGELOG.md +11 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/PKG-INFO +1 -1
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/docs/api.md +0 -1
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/docs/for-coding-agents.md +6 -2
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/docs/runtime-configuration.md +89 -18
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/docs/specification.md +9 -7
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/pyproject.toml +1 -1
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/__init__.py +0 -2
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/configuration.py +0 -41
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/runtime/scoping.py +42 -47
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/execution/test_runtime.py +2 -2
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/integration/test_llm_integration.py +2 -2
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/public/test_public_api.py +105 -1
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/uv.lock +75 -75
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/.claude/rules/docs.md +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/.claude/rules/promptfoo.md +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/.claude/rules/src.md +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/.claude/rules/tests.md +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/.claude/settings.json +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/.claude/unset_envs.sh +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/.devcontainer/Dockerfile.devcontainer +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/.devcontainer/Dockerfile.litellm +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/.devcontainer/devcontainer.json +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/.devcontainer/docker-compose.yaml +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/.devcontainer/litellm-config.yaml +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/.github/dependabot.yml +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/.github/workflows/ci.yml +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/.github/workflows/publish.yml +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/.gitignore +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/.python-version +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/AGENTS.md +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/CLAUDE.md +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/CONTRIBUTING.md +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/LICENSE +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/README.md +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/docs/AGENTS.md +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/docs/assets/nighthawk_logo-128x128.png +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/docs/coding-agent-backends.md +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/docs/executors.md +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/docs/index.md +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/docs/natural-blocks.md +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/docs/patterns.md +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/docs/philosophy.md +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/docs/pydantic-ai-providers.md +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/docs/quickstart.md +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/docs/roadmap.md +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/docs/verification.md +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/evals/promptfoo/assertions/__init__.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/evals/promptfoo/assertions/binding_value.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/evals/promptfoo/assertions/outcome_kind.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/evals/promptfoo/assertions/raise_message.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/evals/promptfoo/evidence/2026-03-26-baseline-prompt-ab.md +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/evals/promptfoo/evidence/2026-03-26-baseline-regression.md +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/evals/promptfoo/evidence/2026-03-26-baseline-suffix-ab.md +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/evals/promptfoo/promptfooconfig-agents.yaml +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/evals/promptfoo/promptfooconfig-prompt-ab.yaml +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/evals/promptfoo/promptfooconfig-suffix-ab.yaml +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/evals/promptfoo/promptfooconfig.yaml +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/evals/promptfoo/prompts/eval_coding_agent.txt +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/evals/promptfoo/prompts/eval_default.txt +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/evals/promptfoo/prompts/eval_mutation_aware.txt +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/evals/promptfoo/prompts/eval_sequenced.txt +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/evals/promptfoo/provider.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/evals/promptfoo/research-result.md +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/evals/promptfoo/test_cases/binding_operations.yaml +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/evals/promptfoo/test_cases/edge_cases.yaml +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/evals/promptfoo/test_cases/loop_outcomes.yaml +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/evals/promptfoo/test_cases/multi_step.yaml +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/evals/promptfoo/test_cases/null_handling.yaml +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/evals/promptfoo/test_cases/outcome_kinds.yaml +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/evals/promptfoo/test_cases/tool_selection.yaml +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/mkdocs.yml +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/pyrightconfig.json +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/AGENTS.md +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/backends/__init__.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/backends/base.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/backends/claude_code_cli.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/backends/claude_code_sdk.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/backends/claude_code_settings.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/backends/codex.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/backends/mcp_boundary.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/backends/mcp_server.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/backends/tool_bridge.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/errors.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/identifier_path.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/json_renderer.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/natural/__init__.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/natural/blocks.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/natural/decorator.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/natural/transform.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/resilience/__init__.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/resilience/_budget.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/resilience/_circuit_breaker.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/resilience/_fallback.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/resilience/_retry.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/resilience/_timeout.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/resilience/_vote.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/runtime/__init__.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/runtime/async_bridge.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/runtime/prompt.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/runtime/runner.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/runtime/step_context.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/runtime/step_contract.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/runtime/step_executor.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/runtime/tool_calls.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/testing.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/tools/__init__.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/tools/assignment.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/tools/contracts.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/tools/execution.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/tools/provided.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/tools/registry.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/src/nighthawk/ulid.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/AGENTS.md +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/__init__.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/backends/__init__.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/backends/test_claude_code_cli.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/backends/test_claude_code_sdk.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/backends/test_codex.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/conftest.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/docs/__init__.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/docs/test_coding_agent_examples.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/docs/test_docs_architecture.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/docs/test_prompt_examples.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/execution/__init__.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/execution/prompt_test_helpers.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/execution/stub_executor.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/execution/test_execution_outcome_prompt_fragment.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/execution/test_globals_prompt.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/execution/test_infer_binding_types.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/execution/test_natural_block_ordering.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/execution/test_natural_traceback.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/execution/test_variables_prompt.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/integration/__init__.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/integration/skip_helpers.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/integration/test_carry_pattern.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/integration/test_claude_code_cli_integration.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/integration/test_claude_code_sdk_integration.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/integration/test_codex_integration.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/natural/__init__.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/natural/test_blocks.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/public/__init__.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/public/test_readme_example.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/public/test_usage_meter.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/resilience/__init__.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/resilience/test_budget.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/resilience/test_circuit_breaker.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/resilience/test_composition.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/resilience/test_fallback.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/resilience/test_retry.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/resilience/test_timeout.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/resilience/test_vote.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/test_renderer.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/test_testing.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/tools/__init__.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/tools/test_assignment_async.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/tools/test_contracts.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/tools/test_provided_async.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/tools/test_registry.py +0 -0
- {nighthawk_python-0.8.0 → nighthawk_python-0.9.0}/tests/tools/test_tool_boundary.py +0 -0
|
@@ -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.
|
|
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/
|
|
@@ -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`
|
|
150
|
-
|
|
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
|
|
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
|
-
#
|
|
14
|
+
# Inherit mode (default): merge/append into current scope state
|
|
15
15
|
with nh.scope(
|
|
16
|
-
|
|
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
|
-
#
|
|
22
|
+
# Replace mode: replace only explicitly provided values
|
|
23
|
+
# None means "no change". [] / {} means "clear".
|
|
23
24
|
with nh.scope(
|
|
24
|
-
|
|
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
|
|
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
|
-
- `
|
|
43
|
-
- `
|
|
44
|
-
- `implicit_references`:
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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.
|
|
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
|
-
##
|
|
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
|
-
|
|
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,
|
|
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
|
-
-
|
|
597
|
-
|
|
598
|
-
|
|
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.
|
|
@@ -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
|
|
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
|
-
|
|
302
|
-
|
|
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
|
|
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
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
-
|
|
325
|
-
|
|
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
|
-
|
|
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=
|
|
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
|
-
|
|
368
|
-
|
|
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
|
|
372
|
-
|
|
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
|
|
375
|
-
|
|
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
|
-
|
|
379
|
-
next_implicit_reference_name_to_value
|
|
380
|
-
|
|
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(
|
|
387
|
-
user_fragments_token = _user_prompt_suffix_fragments_var.set(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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"
|