nighthawk-python 0.1.0__tar.gz → 0.3.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 (104) hide show
  1. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/.claude/rules/docs.md +2 -0
  2. nighthawk_python-0.3.0/.claude/settings.json +5 -0
  3. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/.github/workflows/ci.yml +3 -3
  4. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/.github/workflows/docs.yml +1 -1
  5. nighthawk_python-0.3.0/CHANGELOG.md +47 -0
  6. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/CONTRIBUTING.md +17 -0
  7. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/PKG-INFO +5 -21
  8. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/README.md +3 -19
  9. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/docs/coding-agent-backends.md +2 -2
  10. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/docs/design.md +142 -111
  11. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/docs/for-coding-agents.md +3 -33
  12. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/docs/index.md +2 -2
  13. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/pyproject.toml +3 -2
  14. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/src/nighthawk/runtime/runner.py +127 -82
  15. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/src/nighthawk/runtime/scoping.py +33 -6
  16. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/src/nighthawk/runtime/step_executor.py +11 -25
  17. nighthawk_python-0.3.0/tests/public/test_public_api.py +471 -0
  18. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/uv.lock +169 -45
  19. nighthawk_python-0.1.0/tests/public/test_public_api.py +0 -133
  20. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/.claude/rules/coding.md +0 -0
  21. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/.claude/unset_envs.sh +0 -0
  22. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/.devcontainer/Dockerfile.devcontainer +0 -0
  23. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/.devcontainer/Dockerfile.litellm +0 -0
  24. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/.devcontainer/devcontainer.json +0 -0
  25. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/.devcontainer/docker-compose.yaml +0 -0
  26. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/.devcontainer/litellm-config.yaml +0 -0
  27. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/.github/dependabot.yml +0 -0
  28. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/.github/workflows/publish.yml +0 -0
  29. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/.gitignore +0 -0
  30. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/.python-version +0 -0
  31. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/AGENTS.md +0 -0
  32. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/CLAUDE.md +0 -0
  33. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/LICENSE +0 -0
  34. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/docs/api.md +0 -0
  35. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/docs/assets/nighthawk_logo-128x128.png +0 -0
  36. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/docs/providers.md +0 -0
  37. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/docs/quickstart.md +0 -0
  38. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/docs/roadmap.md +0 -0
  39. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/docs/tutorial.md +0 -0
  40. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/mkdocs.yml +0 -0
  41. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/pyrightconfig.json +0 -0
  42. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/src/nighthawk/__init__.py +0 -0
  43. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/src/nighthawk/backends/__init__.py +0 -0
  44. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/src/nighthawk/backends/base.py +0 -0
  45. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/src/nighthawk/backends/claude_code_cli.py +0 -0
  46. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/src/nighthawk/backends/claude_code_sdk.py +0 -0
  47. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/src/nighthawk/backends/codex.py +0 -0
  48. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/src/nighthawk/backends/mcp_boundary.py +0 -0
  49. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/src/nighthawk/backends/mcp_server.py +0 -0
  50. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/src/nighthawk/backends/tool_bridge.py +0 -0
  51. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/src/nighthawk/configuration.py +0 -0
  52. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/src/nighthawk/errors.py +0 -0
  53. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/src/nighthawk/identifier_path.py +0 -0
  54. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/src/nighthawk/json_renderer.py +0 -0
  55. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/src/nighthawk/natural/__init__.py +0 -0
  56. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/src/nighthawk/natural/blocks.py +0 -0
  57. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/src/nighthawk/natural/decorator.py +0 -0
  58. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/src/nighthawk/natural/transform.py +0 -0
  59. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/src/nighthawk/runtime/__init__.py +0 -0
  60. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/src/nighthawk/runtime/async_bridge.py +0 -0
  61. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/src/nighthawk/runtime/prompt.py +0 -0
  62. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/src/nighthawk/runtime/step_context.py +0 -0
  63. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/src/nighthawk/runtime/step_contract.py +0 -0
  64. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/src/nighthawk/runtime/tool_calls.py +0 -0
  65. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/src/nighthawk/tools/__init__.py +0 -0
  66. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/src/nighthawk/tools/assignment.py +0 -0
  67. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/src/nighthawk/tools/contracts.py +0 -0
  68. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/src/nighthawk/tools/execution.py +0 -0
  69. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/src/nighthawk/tools/provided.py +0 -0
  70. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/src/nighthawk/tools/registry.py +0 -0
  71. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/tests/__init__.py +0 -0
  72. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/tests/backends/__init__.py +0 -0
  73. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/tests/backends/test_claude_code_cli.py +0 -0
  74. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/tests/backends/test_claude_code_sdk.py +0 -0
  75. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/tests/backends/test_codex.py +0 -0
  76. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/tests/conftest.py +0 -0
  77. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/tests/docs/__init__.py +0 -0
  78. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/tests/docs/test_prompt_examples.py +0 -0
  79. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/tests/execution/__init__.py +0 -0
  80. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/tests/execution/prompt_test_helpers.py +0 -0
  81. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/tests/execution/stub_executor.py +0 -0
  82. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/tests/execution/test_execution_outcome_prompt_fragment.py +0 -0
  83. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/tests/execution/test_globals_prompt.py +0 -0
  84. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/tests/execution/test_natural_block_ordering.py +0 -0
  85. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/tests/execution/test_natural_traceback.py +0 -0
  86. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/tests/execution/test_runtime.py +0 -0
  87. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/tests/execution/test_variables_prompt.py +0 -0
  88. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/tests/integration/__init__.py +0 -0
  89. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/tests/integration/skip_helpers.py +0 -0
  90. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/tests/integration/test_carry_pattern.py +0 -0
  91. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/tests/integration/test_claude_code_cli_integration.py +0 -0
  92. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/tests/integration/test_claude_code_sdk_integration.py +0 -0
  93. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/tests/integration/test_codex_integration.py +0 -0
  94. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/tests/integration/test_llm_integration.py +0 -0
  95. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/tests/natural/__init__.py +0 -0
  96. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/tests/natural/test_blocks.py +0 -0
  97. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/tests/public/__init__.py +0 -0
  98. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/tests/public/test_readme_example.py +0 -0
  99. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/tests/test_renderer.py +0 -0
  100. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/tests/tools/__init__.py +0 -0
  101. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/tests/tools/test_assignment_async.py +0 -0
  102. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/tests/tools/test_contracts.py +0 -0
  103. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/tests/tools/test_registry.py +0 -0
  104. {nighthawk_python-0.1.0 → nighthawk_python-0.3.0}/tests/tools/test_tool_boundary.py +0 -0
@@ -91,6 +91,8 @@ Each file has a distinct audience and scope. Content belongs in exactly one file
91
91
  - This file should be self-contained: a coding agent reading only this file should be able to write correct Nighthawk code without consulting other docs.
92
92
  - This file is consumed standalone (`@docs/for-coding-agents.md` in CLAUDE.md/AGENTS.md, GitHub raw URL, etc.). Do not assume sibling files exist at relative paths.
93
93
  - All external references to other docs use absolute URLs based on `site_url` from `mkdocs.yml` (currently `https://kurusugawa-computer.github.io/nighthawk-python/`). If `site_url` changes, update the URLs in this file.
94
+ - `@nh.tool` is deprecated. Do not add examples, recommendations, or references to `@nh.tool` in this file. Binding functions are the only recommended callable exposure mechanism.
95
+ - Filter content for coding-agent relevance. Omit infrastructure-level concerns (scoped overrides parameter lists, exception hierarchy beyond `ExecutionError`, observability/tracing) that do not affect how an agent writes Natural blocks or binding functions. Mention existence and link to Tutorial or Design for details.
94
96
 
95
97
  ### api.md specifics
96
98
 
@@ -0,0 +1,5 @@
1
+ {
2
+ "enabledPlugins": {
3
+ "pyright-lsp@claude-plugins-official": true
4
+ }
5
+ }
@@ -23,7 +23,7 @@ jobs:
23
23
  with:
24
24
  persist-credentials: false
25
25
 
26
- - uses: astral-sh/setup-uv@v6
26
+ - uses: astral-sh/setup-uv@v7
27
27
  with:
28
28
  python-version: "3.13"
29
29
 
@@ -52,7 +52,7 @@ jobs:
52
52
  with:
53
53
  persist-credentials: false
54
54
 
55
- - uses: astral-sh/setup-uv@v6
55
+ - uses: astral-sh/setup-uv@v7
56
56
  with:
57
57
  python-version: ${{ matrix.python-version }}
58
58
 
@@ -71,7 +71,7 @@ jobs:
71
71
  with:
72
72
  persist-credentials: false
73
73
 
74
- - uses: astral-sh/setup-uv@v6
74
+ - uses: astral-sh/setup-uv@v7
75
75
 
76
76
  - name: Build sdist and wheel
77
77
  run: uv build
@@ -26,7 +26,7 @@ jobs:
26
26
  steps:
27
27
  - uses: actions/checkout@v6
28
28
 
29
- - uses: astral-sh/setup-uv@v6
29
+ - uses: astral-sh/setup-uv@v7
30
30
 
31
31
  - name: Install dependencies
32
32
  run: uv sync --group docs
@@ -0,0 +1,47 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.3.0] - 2026-03-18
11
+
12
+ ### Added
13
+ - `system_prompt_suffix_fragment_scope` and `user_prompt_suffix_fragment_scope` context managers for lightweight prompt fragment management without full scope overhead.
14
+ - OpenTelemetry tracer now reports `instrumenting_library_version`.
15
+
16
+ ### Changed
17
+ - Simplified OpenTelemetry span hierarchy: removed implicit `nighthawk.scope` spans and `nighthawk.step_executor` spans. `nighthawk.scope` spans are now emitted only for explicit `nh.scope()` calls.
18
+ - `nighthawk.run` span no longer includes `scope.id` attribute; only `run.id` is emitted.
19
+ - Trimmed `for-coding-agents.md` for coding-agent relevance: removed deprecated `@nh.tool` references, condensed exception hierarchy, scoped overrides, and added debugging context to `StepContextLimits`.
20
+
21
+ ## [0.2.0] - 2026-03-16
22
+
23
+ ### Added
24
+ - Added compact step trace support for Natural block execution attempts:
25
+ - `StepTrace`
26
+ - `StepTraceError`
27
+ - `nighthawk.get_step_traces()`
28
+
29
+ ### Changed
30
+ - Updated CI workflow setup (`setup-uv`) in project automation.
31
+
32
+ ### Fixed
33
+ - License badge reference in README.
34
+ - Documentation formatting inconsistencies.
35
+
36
+ ## [0.1.0] - 2026-03-13
37
+
38
+ ### Added
39
+ - Initial public release of `nighthawk-python`.
40
+ - Natural DSL execution runtime with run/scope execution context model.
41
+ - Step executor abstraction and provider integration foundation.
42
+ - Core documentation and project scaffolding.
43
+
44
+ [Unreleased]: https://github.com/kurusugawa-computer/nighthawk-python/compare/v0.3.0...HEAD
45
+ [0.3.0]: https://github.com/kurusugawa-computer/nighthawk-python/compare/v0.2.0...v0.3.0
46
+ [0.2.0]: https://github.com/kurusugawa-computer/nighthawk-python/compare/v0.1.0...v0.2.0
47
+ [0.1.0]: https://github.com/kurusugawa-computer/nighthawk-python/tree/v0.1.0
@@ -53,6 +53,23 @@ Then run with your `.env` loaded:
53
53
  set -a; source .env; set +a; uv run pytest -q
54
54
  ```
55
55
 
56
+ ### Trace inspection with otel-tui
57
+
58
+ Nighthawk emits OpenTelemetry traces for step execution. You can inspect them
59
+ locally using [otel-tui](https://github.com/ymtdzzz/otel-tui):
60
+
61
+ ```bash
62
+ docker run --rm -it -p 4318:4318 --name otel-tui ymtdzzz/otel-tui:latest
63
+ ```
64
+
65
+ Then run integration tests with the collector endpoint:
66
+
67
+ ```bash
68
+ OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 uv run pytest -q tests/integration/
69
+ ```
70
+
71
+ Traces appear in the otel-tui terminal UI in real time.
72
+
56
73
  ### Environment variables
57
74
 
58
75
  - `OPENAI_API_KEY`: Required for OpenAI integration tests (also requires `pydantic-ai-slim[openai]`).
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nighthawk-python
3
- Version: 0.1.0
3
+ Version: 0.3.0
4
4
  Summary: An experimental Python library that embeds Natural blocks inside Python functions and executes them using an LLM.
5
5
  Project-URL: Repository, https://github.com/kurusugawa-computer/nighthawk-python
6
6
  Project-URL: Documentation, https://kurusugawa-computer.github.io/nighthawk-python/
@@ -19,7 +19,7 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
19
  Classifier: Typing :: Typed
20
20
  Requires-Python: >=3.13
21
21
  Requires-Dist: headson>=0.16.1
22
- Requires-Dist: pydantic-ai-slim>=1.59
22
+ Requires-Dist: pydantic-ai-slim>=1.68
23
23
  Requires-Dist: pydantic>=2
24
24
  Requires-Dist: pyyaml>=6
25
25
  Requires-Dist: tiktoken>=0.12
@@ -33,7 +33,7 @@ Description-Content-Type: text/markdown
33
33
 
34
34
  [![PyPI](https://img.shields.io/pypi/v/nighthawk-python)](https://pypi.org/project/nighthawk-python)
35
35
  ![PyPI - Downloads](https://img.shields.io/pypi/dm/nighthawk-python)
36
- [![license](https://img.shields.io/github/license/psg-mit/nighthawk-python.svg)](https://github.com/kurusugawa-computer/nighthawk-python/tree/main/LICENSE)
36
+ [![license](https://img.shields.io/github/license/kurusugawa-computer/nighthawk-python.svg)](https://github.com/kurusugawa-computer/nighthawk-python/blob/main/LICENSE)
37
37
  [![issue resolution](https://img.shields.io/github/issues-closed-raw/kurusugawa-computer/nighthawk-python)](https://github.com/kurusugawa-computer/nighthawk-python/issues)
38
38
 
39
39
  # Nighthawk
@@ -86,25 +86,9 @@ python quickstart.py
86
86
 
87
87
  For backends, credentials, model identifiers, and detailed guidance, see the [documentation site](https://kurusugawa-computer.github.io/nighthawk-python/).
88
88
 
89
- ## Development
89
+ ## Development & Contributing
90
90
 
91
- Run tests:
92
-
93
- ```bash
94
- uv run pytest -q
95
- ```
96
-
97
- Run an OTel collector UI (otel-tui) for observability:
98
-
99
- ```bash
100
- docker run --rm -it -p 4318:4318 --name otel-tui ymtdzzz/otel-tui:latest
101
- ```
102
-
103
- Then run integration tests with `OTEL_EXPORTER_OTLP_ENDPOINT` set:
104
-
105
- ```bash
106
- OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 uv run pytest -q tests/integration/test_llm_integration.py
107
- ```
91
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for setup, development commands, and contribution guidelines.
108
92
 
109
93
  ## References
110
94
 
@@ -1,6 +1,6 @@
1
1
  [![PyPI](https://img.shields.io/pypi/v/nighthawk-python)](https://pypi.org/project/nighthawk-python)
2
2
  ![PyPI - Downloads](https://img.shields.io/pypi/dm/nighthawk-python)
3
- [![license](https://img.shields.io/github/license/psg-mit/nighthawk-python.svg)](https://github.com/kurusugawa-computer/nighthawk-python/tree/main/LICENSE)
3
+ [![license](https://img.shields.io/github/license/kurusugawa-computer/nighthawk-python.svg)](https://github.com/kurusugawa-computer/nighthawk-python/blob/main/LICENSE)
4
4
  [![issue resolution](https://img.shields.io/github/issues-closed-raw/kurusugawa-computer/nighthawk-python)](https://github.com/kurusugawa-computer/nighthawk-python/issues)
5
5
 
6
6
  # Nighthawk
@@ -53,25 +53,9 @@ python quickstart.py
53
53
 
54
54
  For backends, credentials, model identifiers, and detailed guidance, see the [documentation site](https://kurusugawa-computer.github.io/nighthawk-python/).
55
55
 
56
- ## Development
56
+ ## Development & Contributing
57
57
 
58
- Run tests:
59
-
60
- ```bash
61
- uv run pytest -q
62
- ```
63
-
64
- Run an OTel collector UI (otel-tui) for observability:
65
-
66
- ```bash
67
- docker run --rm -it -p 4318:4318 --name otel-tui ymtdzzz/otel-tui:latest
68
- ```
69
-
70
- Then run integration tests with `OTEL_EXPORTER_OTLP_ENDPOINT` set:
71
-
72
- ```bash
73
- OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 uv run pytest -q tests/integration/test_llm_integration.py
74
- ```
58
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for setup, development commands, and contribution guidelines.
75
59
 
76
60
  ## References
77
61
 
@@ -196,8 +196,8 @@ The symlink approach above lets a single skill definition serve both backends.
196
196
 
197
197
  Skill configuration differs between backends:
198
198
 
199
- - **Claude Code** (SDK and CLI): supports SKILL.md frontmatter fields such as `context`, `agent`, `allowed-tools`, and `disable-model-invocation`. See the [Claude Code skills documentation](https://code.claude.com/docs/skills) for available options.
200
- - **Codex**: uses a separate `agents/openai.yaml` file for invocation policy and tool dependencies. See the [Codex skills documentation](https://developers.openai.com/codex/guides/skills) for available options.
199
+ - **Claude Code** (SDK and CLI): supports SKILL.md frontmatter fields such as `context`, `agent`, `allowed-tools`, and `disable-model-invocation`. See the [Claude Code skills documentation](https://code.claude.com/docs/en/skills) for available options.
200
+ - **Codex**: uses a separate `agents/openai.yaml` file for invocation policy and tool dependencies. See the [Codex skills documentation](https://developers.openai.com/codex/skills/) for available options.
201
201
 
202
202
  Example `SKILL.md`:
203
203
 
@@ -26,12 +26,12 @@ This file intentionally does not maintain a persistent divergence ledger.
26
26
 
27
27
  - Provide a compact reimplementation of nightjarpy-like Natural blocks in Python.
28
28
  - Support a hybrid style where Python controls flow, while the LLM executes a Natural DSL embedded as:
29
- - Function docstring Natural blocks
30
- - Inline Natural blocks (standalone string literal statements)
29
+ - Function docstring Natural blocks
30
+ - Inline Natural blocks (standalone string literal statements)
31
31
  - Reduce the "LLM is a black box" problem by actively mapping LLM-relevant state into the Python interpreter:
32
- - expose a summary of step locals to the LLM
33
- - allow the LLM to synchronize intermediate state into a step locals mapping during reasoning
34
- - commit selected state back into Python locals at Natural block boundaries
32
+ - expose a summary of step locals to the LLM
33
+ - allow the LLM to synchronize intermediate state into a step locals mapping during reasoning
34
+ - commit selected state back into Python locals at Natural block boundaries
35
35
  - Provide a coherent execution model where all state is ordinary Python values in step locals, and persistence (if desired) is user-managed via ordinary bindings.
36
36
 
37
37
  ## 2. Non-goals
@@ -57,8 +57,8 @@ This file intentionally does not maintain a persistent divergence ledger.
57
57
  - Step locals (`step_locals`): a locals mapping used as the execution environment for LLM expressions; updated during reasoning via tools.
58
58
  - Step globals (`step_globals`): a limited globals mapping used as the execution environment for LLM expressions.
59
59
  - StepContext: a mutable, per-step object (one Natural block execution) passed to tools and executors.
60
- - Required fields include `step_id` (unique Id for the step).
61
- - Model selection and prompt policy are owned by `StepExecutorConfiguration`; StepContext does not carry model configuration.
60
+ - Required fields include `step_id` (unique Id for the step).
61
+ - Model selection and prompt policy are owned by `StepExecutorConfiguration`; StepContext does not carry model configuration.
62
62
  - Locals summary: a bounded text rendering of selected values from `step_locals`, included in the LLM prompt.
63
63
  - Prompt suffix fragment: additional prompt text appended to the end of the effective system prompt or user prompt for the duration of a scoped override.
64
64
  - Outcome: the single, unambiguous result of executing a Natural block.
@@ -71,54 +71,50 @@ This file intentionally does not maintain a persistent divergence ledger.
71
71
  ### 5.1. Decorator
72
72
 
73
73
  - `nighthawk.natural_function`
74
- - Decorator that compiles a function containing Natural blocks into an LLM-backed implementation.
75
- - Compilation happens at decoration time, and Natural blocks are executed at function call time.
76
- - Note: The decorator requires the function source to be available for inspection.
74
+ - Decorator that compiles a function containing Natural blocks into an LLM-backed implementation.
75
+ - Compilation happens at decoration time, and Natural blocks are executed at function call time.
76
+ - Note: The decorator requires the function source to be available for inspection.
77
77
 
78
78
  ### 5.2. Configuration
79
79
 
80
80
  - `StepExecutorConfiguration`
81
- - `model`: Model identifier in `provider:model` format. Default: `openai-responses:gpt-5-nano` (see [Providers](providers.md)).
82
- - Examples: `openai-responses:gpt-5-mini`, `openai-responses:gpt-5-nano`.
83
- - Special cases:
84
- - `claude-code-sdk:default`, `claude-code-cli:default`, and `codex:default` select the backend/provider default model (no explicit model selection is sent to the backend).
85
- - `model_settings`: optional model/backend settings. Accepts a `dict[str, Any]` or a backend-specific `BaseModel` instance (auto-converted to dict). Forwarded to Pydantic AI Agent calls.
86
- - `tokenizer_encoding`: tokenizer encoding identifier for approximate token budgeting. `None` means auto-resolve by model name, then fallback to `o200k_base`.
87
- - `prompts`: prompt templates used for execution.
88
- - `step_system_prompt_template`: system prompt template that defines the step execution protocol.
89
- - `step_user_prompt_template`: full user prompt template including section delimiters.
90
- - `context_limits`: limits for rendering dynamic context into the prompt.
91
- - `json_renderer_style`: [headson](https://github.com/kantord/headson) rendering style used in prompt context and tool result envelopes. 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
- - `system_prompt_suffix_fragments`: optional baseline system prompt suffix fragments for this executor configuration.
93
- - `user_prompt_suffix_fragments`: optional baseline user prompt suffix fragments for this executor configuration.
94
-
81
+ - `model`: Model identifier in `provider:model` format. Default: `openai-responses:gpt-5-nano` (see [Providers](providers.md)).
82
+ - Examples: `openai-responses:gpt-5-mini`, `openai-responses:gpt-5-nano`.
83
+ - Special cases:
84
+ - `claude-code-sdk:default`, `claude-code-cli:default`, and `codex:default` select the backend/provider default model (no explicit model selection is sent to the backend).
85
+ - `model_settings`: optional model/backend settings. Accepts a `dict[str, Any]` or a backend-specific `BaseModel` instance (auto-converted to dict). Forwarded to Pydantic AI Agent calls.
86
+ - `tokenizer_encoding`: tokenizer encoding identifier for approximate token budgeting. `None` means auto-resolve by model name, then fallback to `o200k_base`.
87
+ - `prompts`: prompt templates used for execution.
88
+ - `step_system_prompt_template`: system prompt template that defines the step execution protocol.
89
+ - `step_user_prompt_template`: full user prompt template including section delimiters.
90
+ - `context_limits`: limits for rendering dynamic context into the prompt.
91
+ - `json_renderer_style`: [headson](https://github.com/kantord/headson) rendering style used in prompt context and tool result envelopes. 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
+ - `system_prompt_suffix_fragments`: optional baseline system prompt suffix fragments for this executor configuration.
93
+ - `user_prompt_suffix_fragments`: optional baseline user prompt suffix fragments for this executor configuration.
95
94
  - `StepExecutorConfigurationPatch`
96
- - Partial override object for scoped configuration updates.
97
- - Supports patching model, model settings, templates, limits, renderer style, tokenizer encoding, and prompt suffix fragment tuples.
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.
98
97
 
99
98
  ### 5.3. Supporting types
100
99
 
101
100
  - `StepPromptTemplates`
102
- - Prompt templates used for step execution.
103
- - `step_system_prompt_template`: system prompt template.
104
- - `step_user_prompt_template`: user prompt template.
105
-
101
+ - Prompt templates used for step execution.
102
+ - `step_system_prompt_template`: system prompt template.
103
+ - `step_user_prompt_template`: user prompt template.
106
104
  - `StepContextLimits`
107
- - Limits for rendering dynamic context into the LLM prompt.
108
- - Fields: `locals_max_tokens`, `locals_max_items`, `globals_max_tokens`, `globals_max_items`, `value_max_tokens`, `tool_result_max_tokens`.
109
-
105
+ - Limits for rendering dynamic context into the LLM prompt.
106
+ - Fields: `locals_max_tokens`, `locals_max_items`, `globals_max_tokens`, `globals_max_items`, `value_max_tokens`, `tool_result_max_tokens`.
110
107
  - `JsonableValue`
111
- - Type alias for JSON-serializable Python values (`dict | list | str | int | float | bool | None`).
112
-
108
+ - Type alias for JSON-serializable Python values (`dict | list | str | int | float | bool | None`).
113
109
  - `ExecutionContext`
114
- - Frozen dataclass representing runtime execution identity.
115
- - `run_id`: the Id of the outermost run (trace root).
116
- - `scope_id`: the Id of the current scope.
110
+ - Frozen dataclass representing runtime execution identity.
111
+ - `run_id`: the Id of the outermost run (trace root).
112
+ - `scope_id`: the Id of the current scope.
117
113
 
118
114
  ### 5.4. Runtime accessors
119
115
 
120
116
  - `nighthawk.get_current_step_context() -> StepContext`
121
- - Get the `StepContext` for the currently executing Natural block. Raises if no step is active.
117
+ - Get the `StepContext` for the currently executing Natural block. Raises if no step is active.
122
118
 
123
119
  See [Section 10](#10-runtime-scoping) for additional runtime accessors (`get_step_executor`, `get_execution_context`).
124
120
 
@@ -194,7 +190,7 @@ Constraints:
194
190
 
195
191
  - `name` is a simple identifier (no dotted paths).
196
192
  - `<:name>` does not require prior declaration.
197
- - Practical note: if subsequent Python code reads a variable that has not been assigned yet, Python will raise before any LLM behavior can help. Initialize variables in Python when needed.
193
+ - Practical note: if subsequent Python code reads a variable that has not been assigned yet, Python will raise before any LLM behavior can help. Initialize variables in Python when needed.
198
194
 
199
195
  Type note:
200
196
 
@@ -222,9 +218,9 @@ Nighthawk uses multiple state layers.
222
218
 
223
219
  - `step_locals` is a mapping used as the locals environment for LLM expression evaluation.
224
220
  - It is initialized at the start of each Natural block execution, in the following order:
225
- 1. If a parent step context exists on the step context stack, start from its `step_locals` values.
226
- 2. Overlay the caller frame's current `python_locals` (so current Python locals always win over inherited step-context state).
227
- 3. For each read binding (`<name>`), resolve the name using Python lexical rules (locals, enclosing cell scopes, name scopes, globals, builtins) and place the resolved value into `step_locals`.
221
+ 1. If a parent step context exists on the step context stack, start from its `step_locals` values.
222
+ 2. Overlay the caller frame's current `python_locals` (so current Python locals always win over inherited step-context state).
223
+ 3. For each read binding (`<name>`), resolve the name using Python lexical rules (locals, enclosing cell scopes, name scopes, globals, builtins) and place the resolved value into `step_locals`.
228
224
  - During execution, the LLM can update `step_locals` via tools (Section 8.3).
229
225
  - At the end of execution, values for `<:name>` bindings are committed into Python locals.
230
226
 
@@ -259,9 +255,9 @@ Rendering format:
259
255
 
260
256
  - Non-callable values: `name: type_name = json_value`, where `json_value` is the compact JSON rendering of the value (bounded by `context_limits.value_max_tokens`).
261
257
  - Callable values: `name: (signature)`, where `(signature)` is the result of `inspect.signature`. Type annotations are included when available (e.g., `(base: int, bonus: int) -> int`).
262
- - If the callable has a meaningful docstring, the first line is appended as `# intent: first_line`.
263
- - If multiple callable entries share the same signature text, each is annotated with `# disambiguation: use name` to help the LLM distinguish them.
264
- - If the signature cannot be resolved (e.g., `__signature__` raises), the entry renders as `name: <callable; signature-unavailable>`.
258
+ - If the callable has a meaningful docstring, the first line is appended as `# intent: first_line`.
259
+ - If multiple callable entries share the same signature text, each is annotated with `# disambiguation: use name` to help the LLM distinguish them.
260
+ - If the signature cannot be resolved (e.g., `__signature__` raises), the entry renders as `name: <callable; signature-unavailable>`.
265
261
  - `TypeAliasType` values (PEP 695): `name: type = underlying_type`.
266
262
 
267
263
  Truncation:
@@ -319,10 +315,10 @@ User-defined tools:
319
315
  Registration API:
320
316
 
321
317
  - `@nighthawk.tool`: Decorator that registers a callable as a Nighthawk tool.
322
- - `name`: Optional name override. Defaults to the function `__name__`.
323
- - `overwrite`: If True, replaces any existing tool with the same name.
324
- - `description`: Optional description override. Defaults to the function docstring.
325
- - `metadata`: Arbitrary metadata dict attached to the tool definition.
318
+ - `name`: Optional name override. Defaults to the function `__name__`.
319
+ - `overwrite`: If True, replaces any existing tool with the same name.
320
+ - `description`: Optional description override. Defaults to the function docstring.
321
+ - `metadata`: Arbitrary metadata dict attached to the tool definition.
326
322
  - Tool names must be ASCII and match `^[A-Za-z_][A-Za-z0-9_]*$`.
327
323
  - Tool registration targets the innermost active scope (call scope > tool scope > global).
328
324
  - Name conflicts raise `ToolRegistrationError` unless `overwrite=True`.
@@ -361,14 +357,14 @@ Expressions are evaluated against `step_globals` + `step_locals`.
361
357
  Inspect tool:
362
358
 
363
359
  - `nh_eval(expression: str) -> object`
364
- - Evaluate a Python expression and return the result. Use to inspect values and call functions.
365
- - If the evaluated expression is awaitable, it is awaited before returning.
360
+ - Evaluate a Python expression and return the result. Use to inspect values and call functions.
361
+ - If the evaluated expression is awaitable, it is awaited before returning.
366
362
 
367
363
  Mutation tool:
368
364
 
369
365
  - `nh_exec(expression: str) -> object`
370
- - Execute a Python expression for its side effect on mutable objects (e.g., `list.append()`, `dict.update()`, `set.add()`).
371
- - Returns the expression result.
366
+ - Execute a Python expression for its side effect on mutable objects (e.g., `list.append()`, `dict.update()`, `set.add()`).
367
+ - Returns the expression result.
372
368
 
373
369
  Implementation note: `nh_eval` and `nh_exec` share the same underlying implementation (Python `eval()`). The two-tool split exists as a semantic signal to the LLM (inspect intent vs mutate intent), not a runtime distinction.
374
370
 
@@ -390,16 +386,16 @@ Semantics of `nh_assign`:
390
386
  - Evaluate `expression` as a Python expression using `step_globals` and `step_locals`.
391
387
  - If the evaluated expression is awaitable, it is awaited before assignment.
392
388
  - If `target_path` is a bare `name`:
393
- - Assign into `step_locals[name]`.
394
- - Validation:
395
- - If extracted type information is available for the corresponding `<:name>` binding, validate/coerce to that type.
396
- - Otherwise, assign without validation.
389
+ - Assign into `step_locals[name]`.
390
+ - Validation:
391
+ - If extracted type information is available for the corresponding `<:name>` binding, validate/coerce to that type.
392
+ - Otherwise, assign without validation.
397
393
  - If `target_path` is dotted (`name.field...`):
398
- - Resolve the root object from `step_locals[name]`.
399
- - Traverse attributes for each intermediate segment.
400
- - Assign using attribute assignment on the final segment.
401
- - Validation:
402
- - Validate only when runtime type metadata is available; otherwise assign without validation.
394
+ - Resolve the root object from `step_locals[name]`.
395
+ - Traverse attributes for each intermediate segment.
396
+ - Assign using attribute assignment on the final segment.
397
+ - Validation:
398
+ - Validate only when runtime type metadata is available; otherwise assign without validation.
403
399
 
404
400
  Commit and mutation notes:
405
401
 
@@ -410,10 +406,10 @@ Commit and mutation notes:
410
406
  Write tool return value:
411
407
 
412
408
  - The tool returns a diagnostic object describing:
413
- - whether it succeeded
414
- - a bounded summary of the updated value (on success)
415
- - validation details (when relevant)
416
- - error details (on failure)
409
+ - whether it succeeded
410
+ - a bounded summary of the updated value (on success)
411
+ - validation details (when relevant)
412
+ - error details (on failure)
417
413
 
418
414
  Tool result JSON format:
419
415
 
@@ -445,29 +441,26 @@ The outcome is a discriminated union keyed by the required field `kind`.
445
441
  Outcome kinds:
446
442
 
447
443
  - `pass`:
448
- - Success with no control-flow change.
449
- - Payload keys: `kind` only.
450
-
444
+ - Success with no control-flow change.
445
+ - Payload keys: `kind` only.
451
446
  - `return`:
452
- - Return from the surrounding Python function immediately.
453
- - Payload keys: `kind`, and required `return_reference_path`.
454
- - `return_reference_path` must be a dot-separated identifier path into step locals.
455
- - The host resolves `return_reference_path` within step locals only, using attribute access only.
456
- - If the surrounding function is async and the resolved value is awaitable, the host awaits it before validation.
457
- - The host then validates/coerces the resolved Python value to the function's return type annotation.
458
- - If the surrounding function is sync and the resolved value is awaitable, execution fails.
459
-
447
+ - Return from the surrounding Python function immediately.
448
+ - Payload keys: `kind`, and required `return_reference_path`.
449
+ - `return_reference_path` must be a dot-separated identifier path into step locals.
450
+ - The host resolves `return_reference_path` within step locals only, using attribute access only.
451
+ - If the surrounding function is async and the resolved value is awaitable, the host awaits it before validation.
452
+ - The host then validates/coerces the resolved Python value to the function's return type annotation.
453
+ - If the surrounding function is sync and the resolved value is awaitable, execution fails.
460
454
  - `break` / `continue`:
461
- - Loop control.
462
- - Payload keys: `kind` only.
463
- - These outcomes are valid only when the Natural block appears syntactically inside a Python `for` or `while` loop. If requested outside a loop, execution fails.
464
-
455
+ - Loop control.
456
+ - Payload keys: `kind` only.
457
+ - These outcomes are valid only when the Natural block appears syntactically inside a Python `for` or `while` loop. If requested outside a loop, execution fails.
465
458
  - `raise`:
466
- - Failure.
467
- - Payload keys: `kind`, `raise_message`, and optional `raise_error_type`.
468
- - `raise_error_type` is optional. If provided, it MUST be one of the exception type names listed in the prompt.
469
- - The host enforces this using the structured output JSON Schema: when `raise_error_type` is allowed for a block, its schema is an `enum` over the allowed exception type names.
470
- - When `raise_error_type` is provided, the host raises that exception type with the provided `raise_message`.
459
+ - Failure.
460
+ - Payload keys: `kind`, `raise_message`, and optional `raise_error_type`.
461
+ - `raise_error_type` is optional. If provided, it MUST be one of the exception type names listed in the prompt.
462
+ - The host enforces this using the structured output JSON Schema: when `raise_error_type` is allowed for a block, its schema is an `enum` over the allowed exception type names.
463
+ - When `raise_error_type` is provided, the host raises that exception type with the provided `raise_message`.
471
464
 
472
465
  The implementation chooses strict parsing. Any non-JSON final response is an error.
473
466
 
@@ -512,8 +505,8 @@ Allowed outcome type names in `deny` are a subset of the baseline outcome types:
512
505
  Semantics:
513
506
 
514
507
  - Syntactic context defines a hard cap on allowed outcomes:
515
- - Outside a loop: `pass`, `return`, `raise`.
516
- - Inside a loop: `pass`, `return`, `break`, `continue`, `raise`.
508
+ - Outside a loop: `pass`, `return`, `raise`.
509
+ - Inside a loop: `pass`, `return`, `break`, `continue`, `raise`.
517
510
  - Frontmatter deny declarations may only exclude outcome types; they must not expand the syntactic cap.
518
511
  - If frontmatter denies an outcome type, and the model returns that outcome type, the host raises an `ExecutionError`.
519
512
 
@@ -548,23 +541,61 @@ Working directory selection for provider backends is configured via `ModelSettin
548
541
  API:
549
542
 
550
543
  - `nighthawk.run(step_executor: StepExecutor, *, run_id: str | None = None)`
551
- - Replaces the current context step executor with the provided step executor.
552
- - Generates a new `ExecutionContext` for the duration of the `with`.
553
- - Uses provided `run_id` when given; otherwise generates a new `run_id` (trace root).
554
- - Always generates a fresh `scope_id`.
555
- - Can be used even when no step executor is currently set.
544
+ - Replaces the current context step executor with the provided step executor.
545
+ - Generates a new `ExecutionContext` for the duration of the `with`.
546
+ - Uses provided `run_id` when given; otherwise generates a new `run_id` (trace root).
547
+ - Always generates a fresh `scope_id`.
548
+ - Can be used even when no step executor is currently set.
556
549
  - `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]`
557
- - Enter a nested scope within the current run.
558
- - Requires an existing step executor.
559
- - Generates a new `scope_id` (keeps the current `run_id`).
560
- - Only specified fields are overridden for the duration of the `with`.
561
- - `system_prompt_suffix_fragment`: optional string appended to the system prompt for the duration of the scope.
562
- - `user_prompt_suffix_fragment`: optional string appended to the user prompt for the duration of the scope.
563
- - Yields the resolved `StepExecutor` for the scope.
550
+ - Enter a nested scope within the current run.
551
+ - Requires an existing step executor.
552
+ - Generates a new `scope_id` (keeps the current `run_id`).
553
+ - Only specified fields are overridden for the duration of the `with`.
554
+ - `system_prompt_suffix_fragment`: optional string appended to the system prompt for the duration of the scope.
555
+ - `user_prompt_suffix_fragment`: optional string appended to the user prompt for the duration of the scope.
556
+ - Yields the resolved `StepExecutor` for the scope.
564
557
  - `nighthawk.get_step_executor() -> StepExecutor`
565
- - Get the current step executor. Raises if unset.
558
+ - Get the current step executor. Raises if unset.
566
559
  - `nighthawk.get_execution_context() -> ExecutionContext`
567
- - Get the current runtime execution identity. Raises if unset.
560
+ - Get the current runtime execution identity. Raises if unset.
561
+
562
+ ### 10.1. Observability contract (OpenTelemetry span/event)
563
+
564
+ Nighthawk uses OpenTelemetry spans as the sole runtime trace model.
565
+
566
+ Runtime spans:
567
+
568
+ - `nighthawk.run`
569
+ - `nighthawk.scope`
570
+ - `nighthawk.step`
571
+ - `nighthawk.step_executor`
572
+
573
+ Identity attributes:
574
+
575
+ - `run.id: str`
576
+ - `scope.id: str`
577
+ - `step.id: str` (exact format: `python_module:line`, on `nighthawk.step`)
578
+
579
+ Step events (emitted on `nighthawk.step`):
580
+
581
+ - `nighthawk.step.completed`
582
+ - attributes:
583
+ - `nighthawk.step.outcome_kind`
584
+ - `nighthawk.step.raised`
585
+ - attributes:
586
+ - `nighthawk.step.outcome_kind`
587
+ - `nighthawk.step.raise_message`
588
+ - `nighthawk.step.raise_error_type` (when provided)
589
+ - `nighthawk.step.failed`
590
+ - attributes:
591
+ - `nighthawk.step.error_kind`
592
+ - `nighthawk.step.error_message`
593
+
594
+ Semantics:
595
+
596
+ - `raise` outcome is treated as domain-level behavior, represented by `nighthawk.step.raised`.
597
+ - Nighthawk-side internal failures are represented by `nighthawk.step.failed`, and the span records exception + error status.
598
+ - There is no in-memory step trace API.
568
599
 
569
600
  ## 11. Interpolation (opt-in, f-strings only)
570
601
 
@@ -577,8 +608,8 @@ Natural blocks often need to embed computed values (for example, paths or JSON e
577
608
  - Docstring Natural blocks are always literal. They are never interpolated.
578
609
  - Interpolated Natural blocks are inline f-string Natural blocks (standalone f-string expression statements).
579
610
  - Interpolation follows standard Python f-string semantics.
580
- - Expression evaluation rules are those of Python.
581
- - Brace escaping uses `{{` and `}}` in the f-string source to produce literal `{` and `}` in the rendered text.
611
+ - Expression evaluation rules are those of Python.
612
+ - Brace escaping uses `{{` and `}}` in the f-string source to produce literal `{` and `}` in the rendered text.
582
613
 
583
614
  Note:
584
615
 
@@ -608,17 +639,17 @@ Nighthawk defines a hierarchy of exceptions rooted at `NighthawkError`.
608
639
  Exception hierarchy:
609
640
 
610
641
  - `NighthawkError`: Base class for all Nighthawk exceptions.
611
- - Raised when: runtime preconditions fail (e.g. no active run context, missing step executor).
642
+ - Raised when: runtime preconditions fail (e.g. no active run context, missing step executor).
612
643
  - `NaturalParseError(NighthawkError)`: Natural block parsing or frontmatter parsing failed.
613
- - Raised when: the sentinel is missing, bindings are invalid, frontmatter YAML is malformed, or AST extraction fails.
644
+ - Raised when: the sentinel is missing, bindings are invalid, frontmatter YAML is malformed, or AST extraction fails.
614
645
  - `ExecutionError(NighthawkError)`: Natural block execution failed.
615
- - Raised when: the LLM returns invalid JSON, an outcome kind is disallowed, return value validation fails, or `raise` outcome is triggered without a matching exception type.
646
+ - Raised when: the LLM returns invalid JSON, an outcome kind is disallowed, return value validation fails, or `raise` outcome is triggered without a matching exception type.
616
647
  - `ToolEvaluationError(NighthawkError)`: Expression evaluation inside a tool call failed.
617
- - Raised when: `eval()` raises during `nh_eval`, `nh_exec`, or `nh_assign` expression evaluation.
648
+ - Raised when: `eval()` raises during `nh_eval`, `nh_exec`, or `nh_assign` expression evaluation.
618
649
  - `ToolValidationError(NighthawkError)`: Type validation/coercion failed during `nh_assign`.
619
- - Raised when: the assigned value does not match the expected binding type.
650
+ - Raised when: the assigned value does not match the expected binding type.
620
651
  - `ToolRegistrationError(NighthawkError)`: Tool registration failed.
621
- - Raised when: a tool name is invalid, or a name conflict occurs without `overwrite=True`.
652
+ - Raised when: a tool name is invalid, or a name conflict occurs without `overwrite=True`.
622
653
 
623
654
  All exceptions are surfaced as Python exceptions and can be caught with standard `try`/`except`.
624
655