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