nighthawk-python 0.1.0__py3-none-any.whl
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/__init__.py +48 -0
- nighthawk/backends/__init__.py +0 -0
- nighthawk/backends/base.py +95 -0
- nighthawk/backends/claude_code_cli.py +342 -0
- nighthawk/backends/claude_code_sdk.py +325 -0
- nighthawk/backends/codex.py +352 -0
- nighthawk/backends/mcp_boundary.py +129 -0
- nighthawk/backends/mcp_server.py +226 -0
- nighthawk/backends/tool_bridge.py +240 -0
- nighthawk/configuration.py +193 -0
- nighthawk/errors.py +25 -0
- nighthawk/identifier_path.py +35 -0
- nighthawk/json_renderer.py +216 -0
- nighthawk/natural/__init__.py +0 -0
- nighthawk/natural/blocks.py +279 -0
- nighthawk/natural/decorator.py +302 -0
- nighthawk/natural/transform.py +346 -0
- nighthawk/runtime/__init__.py +0 -0
- nighthawk/runtime/async_bridge.py +50 -0
- nighthawk/runtime/prompt.py +344 -0
- nighthawk/runtime/runner.py +462 -0
- nighthawk/runtime/scoping.py +288 -0
- nighthawk/runtime/step_context.py +171 -0
- nighthawk/runtime/step_contract.py +231 -0
- nighthawk/runtime/step_executor.py +360 -0
- nighthawk/runtime/tool_calls.py +99 -0
- nighthawk/tools/__init__.py +0 -0
- nighthawk/tools/assignment.py +246 -0
- nighthawk/tools/contracts.py +72 -0
- nighthawk/tools/execution.py +83 -0
- nighthawk/tools/provided.py +80 -0
- nighthawk/tools/registry.py +212 -0
- nighthawk_python-0.1.0.dist-info/METADATA +111 -0
- nighthawk_python-0.1.0.dist-info/RECORD +36 -0
- nighthawk_python-0.1.0.dist-info/WHEEL +4 -0
- nighthawk_python-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
|
|
5
|
+
from .blocks import (
|
|
6
|
+
_joined_string_is_natural_sentinel,
|
|
7
|
+
_joined_string_scan_text,
|
|
8
|
+
_validate_joined_string_bindings_do_not_span_formatted_values,
|
|
9
|
+
extract_bindings,
|
|
10
|
+
extract_program,
|
|
11
|
+
is_natural_sentinel,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class NaturalTransformer(ast.NodeTransformer):
|
|
16
|
+
def __init__(self, *, captured_name_tuple: tuple[str, ...]) -> None:
|
|
17
|
+
super().__init__()
|
|
18
|
+
self._captured_name_tuple = captured_name_tuple
|
|
19
|
+
self._return_annotation_stack: list[ast.expr | None] = []
|
|
20
|
+
self._binding_name_to_type_expression_stack: list[dict[str, ast.expr]] = []
|
|
21
|
+
self._is_async_function_stack: list[bool] = []
|
|
22
|
+
self._loop_depth = 0
|
|
23
|
+
|
|
24
|
+
def _visit_function_like(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> ast.AST:
|
|
25
|
+
self._return_annotation_stack.append(node.returns)
|
|
26
|
+
self._binding_name_to_type_expression_stack.append(self._collect_binding_name_to_type_expression(node))
|
|
27
|
+
self._is_async_function_stack.append(isinstance(node, ast.AsyncFunctionDef))
|
|
28
|
+
saved_loop_depth = self._loop_depth
|
|
29
|
+
self._loop_depth = 0
|
|
30
|
+
try:
|
|
31
|
+
if node.body:
|
|
32
|
+
first_statement = node.body[0]
|
|
33
|
+
if (
|
|
34
|
+
isinstance(first_statement, ast.Expr)
|
|
35
|
+
and isinstance(first_statement.value, ast.Constant)
|
|
36
|
+
and isinstance(first_statement.value.value, str)
|
|
37
|
+
):
|
|
38
|
+
docstring_text = first_statement.value.value
|
|
39
|
+
if is_natural_sentinel(docstring_text):
|
|
40
|
+
program = extract_program(docstring_text)
|
|
41
|
+
input_bindings, output_bindings = extract_bindings(program)
|
|
42
|
+
return_annotation = self._current_return_annotation_expression()
|
|
43
|
+
binding_types_dict_expression = self._current_binding_types_dict_expression(output_bindings)
|
|
44
|
+
injected = build_runtime_call_and_assignments(
|
|
45
|
+
ast.Constant(program),
|
|
46
|
+
input_bindings,
|
|
47
|
+
output_bindings,
|
|
48
|
+
binding_types_dict_expression,
|
|
49
|
+
return_annotation,
|
|
50
|
+
is_in_loop=self._loop_depth > 0,
|
|
51
|
+
is_async_function=self._is_async_function_stack[-1],
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# Preserve user-source location: the injected runtime call and its
|
|
55
|
+
# subsequent assignments should point at the Natural docstring
|
|
56
|
+
# sentinel line (the opening triple-quote line).
|
|
57
|
+
sentinel_location = ast.copy_location(ast.Pass(), first_statement)
|
|
58
|
+
sentinel_location.end_lineno = sentinel_location.lineno
|
|
59
|
+
sentinel_location.end_col_offset = sentinel_location.col_offset
|
|
60
|
+
|
|
61
|
+
injected_with_location = [ast.copy_location(statement, sentinel_location) for statement in injected]
|
|
62
|
+
|
|
63
|
+
body_without_docstring = node.body[1:]
|
|
64
|
+
node.body = injected_with_location + body_without_docstring
|
|
65
|
+
|
|
66
|
+
node = self.generic_visit(node) # type: ignore[assignment]
|
|
67
|
+
|
|
68
|
+
if self._captured_name_tuple:
|
|
69
|
+
anchor_name = "__nh_cell_anchor__"
|
|
70
|
+
name_to_cell_name = "__nh_name_to_cell__"
|
|
71
|
+
|
|
72
|
+
anchor_body: list[ast.stmt] = [
|
|
73
|
+
ast.Return(
|
|
74
|
+
value=ast.Tuple(
|
|
75
|
+
elts=[ast.Name(id=name, ctx=ast.Load()) for name in self._captured_name_tuple],
|
|
76
|
+
ctx=ast.Load(),
|
|
77
|
+
)
|
|
78
|
+
)
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
anchor_function = ast.FunctionDef(
|
|
82
|
+
name=anchor_name,
|
|
83
|
+
args=ast.arguments(
|
|
84
|
+
posonlyargs=[],
|
|
85
|
+
args=[],
|
|
86
|
+
kwonlyargs=[],
|
|
87
|
+
kw_defaults=[],
|
|
88
|
+
defaults=[],
|
|
89
|
+
),
|
|
90
|
+
body=anchor_body,
|
|
91
|
+
decorator_list=[],
|
|
92
|
+
returns=None,
|
|
93
|
+
type_comment=None,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
freevars_expression = ast.Attribute(
|
|
97
|
+
value=ast.Attribute(value=ast.Name(id=anchor_name, ctx=ast.Load()), attr="__code__", ctx=ast.Load()),
|
|
98
|
+
attr="co_freevars",
|
|
99
|
+
ctx=ast.Load(),
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
closure_expression = ast.BoolOp(
|
|
103
|
+
op=ast.Or(),
|
|
104
|
+
values=[
|
|
105
|
+
ast.Attribute(value=ast.Name(id=anchor_name, ctx=ast.Load()), attr="__closure__", ctx=ast.Load()),
|
|
106
|
+
ast.Tuple(elts=[], ctx=ast.Load()),
|
|
107
|
+
],
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
name_to_cell_value = ast.Call(
|
|
111
|
+
func=ast.Name(id="dict", ctx=ast.Load()),
|
|
112
|
+
args=[
|
|
113
|
+
ast.Call(
|
|
114
|
+
func=ast.Name(id="zip", ctx=ast.Load()),
|
|
115
|
+
args=[freevars_expression, closure_expression],
|
|
116
|
+
keywords=[],
|
|
117
|
+
)
|
|
118
|
+
],
|
|
119
|
+
keywords=[],
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
name_to_cell_assign = ast.Assign(
|
|
123
|
+
targets=[ast.Name(id=name_to_cell_name, ctx=ast.Store())],
|
|
124
|
+
value=name_to_cell_value,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
with_statement = ast.With(
|
|
128
|
+
items=[
|
|
129
|
+
ast.withitem(
|
|
130
|
+
context_expr=ast.Call(
|
|
131
|
+
func=ast.Name(id="__nh_python_cell_scope__", ctx=ast.Load()),
|
|
132
|
+
args=[ast.Name(id=name_to_cell_name, ctx=ast.Load())],
|
|
133
|
+
keywords=[],
|
|
134
|
+
),
|
|
135
|
+
optional_vars=None,
|
|
136
|
+
)
|
|
137
|
+
],
|
|
138
|
+
body=node.body,
|
|
139
|
+
type_comment=None,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
node.body = [anchor_function, name_to_cell_assign, with_statement]
|
|
143
|
+
|
|
144
|
+
return node
|
|
145
|
+
finally:
|
|
146
|
+
self._is_async_function_stack.pop()
|
|
147
|
+
self._binding_name_to_type_expression_stack.pop()
|
|
148
|
+
self._return_annotation_stack.pop()
|
|
149
|
+
self._loop_depth = saved_loop_depth
|
|
150
|
+
|
|
151
|
+
def visit_FunctionDef(self, node: ast.FunctionDef) -> ast.AST:
|
|
152
|
+
return self._visit_function_like(node)
|
|
153
|
+
|
|
154
|
+
def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> ast.AST: # noqa: N802
|
|
155
|
+
return self._visit_function_like(node)
|
|
156
|
+
|
|
157
|
+
def visit_For(self, node: ast.For) -> ast.AST:
|
|
158
|
+
self._loop_depth += 1
|
|
159
|
+
try:
|
|
160
|
+
return self.generic_visit(node)
|
|
161
|
+
finally:
|
|
162
|
+
self._loop_depth -= 1
|
|
163
|
+
|
|
164
|
+
def visit_AsyncFor(self, node: ast.AsyncFor) -> ast.AST:
|
|
165
|
+
self._loop_depth += 1
|
|
166
|
+
try:
|
|
167
|
+
return self.generic_visit(node)
|
|
168
|
+
finally:
|
|
169
|
+
self._loop_depth -= 1
|
|
170
|
+
|
|
171
|
+
def visit_While(self, node: ast.While) -> ast.AST:
|
|
172
|
+
self._loop_depth += 1
|
|
173
|
+
try:
|
|
174
|
+
return self.generic_visit(node)
|
|
175
|
+
finally:
|
|
176
|
+
self._loop_depth -= 1
|
|
177
|
+
|
|
178
|
+
def visit_Expr(self, node: ast.Expr) -> ast.AST | list[ast.stmt]:
|
|
179
|
+
self.generic_visit(node)
|
|
180
|
+
value = node.value
|
|
181
|
+
|
|
182
|
+
if isinstance(value, ast.Constant) and isinstance(value.value, str):
|
|
183
|
+
text = value.value
|
|
184
|
+
if is_natural_sentinel(text):
|
|
185
|
+
program = extract_program(text)
|
|
186
|
+
input_bindings, output_bindings = extract_bindings(program)
|
|
187
|
+
return_annotation = self._current_return_annotation_expression()
|
|
188
|
+
binding_types_dict_expression = self._current_binding_types_dict_expression(output_bindings)
|
|
189
|
+
is_in_loop = self._loop_depth > 0
|
|
190
|
+
statements = build_runtime_call_and_assignments(
|
|
191
|
+
ast.Constant(program),
|
|
192
|
+
input_bindings,
|
|
193
|
+
output_bindings,
|
|
194
|
+
binding_types_dict_expression,
|
|
195
|
+
return_annotation,
|
|
196
|
+
is_in_loop=is_in_loop,
|
|
197
|
+
is_async_function=self._is_async_function_stack[-1] if self._is_async_function_stack else False,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
sentinel_location = ast.copy_location(ast.Pass(), node)
|
|
201
|
+
sentinel_location.end_lineno = sentinel_location.lineno
|
|
202
|
+
sentinel_location.end_col_offset = sentinel_location.col_offset
|
|
203
|
+
|
|
204
|
+
return [ast.copy_location(statement, sentinel_location) for statement in statements] # type: ignore[return-value]
|
|
205
|
+
|
|
206
|
+
if isinstance(value, ast.JoinedStr) and _joined_string_is_natural_sentinel(value):
|
|
207
|
+
_validate_joined_string_bindings_do_not_span_formatted_values(value)
|
|
208
|
+
return_annotation = self._current_return_annotation_expression()
|
|
209
|
+
is_in_loop = self._loop_depth > 0
|
|
210
|
+
|
|
211
|
+
scan_text = _joined_string_scan_text(value, formatted_value_placeholder="")
|
|
212
|
+
program = extract_program(scan_text)
|
|
213
|
+
input_bindings, output_bindings = extract_bindings(program)
|
|
214
|
+
binding_types_dict_expression = self._current_binding_types_dict_expression(output_bindings)
|
|
215
|
+
|
|
216
|
+
extracted_program_call = ast.Call(
|
|
217
|
+
func=ast.Name(id="__nh_extract_program__", ctx=ast.Load()),
|
|
218
|
+
args=[value],
|
|
219
|
+
keywords=[],
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
statements = build_runtime_call_and_assignments(
|
|
223
|
+
extracted_program_call,
|
|
224
|
+
input_bindings,
|
|
225
|
+
output_bindings,
|
|
226
|
+
binding_types_dict_expression,
|
|
227
|
+
return_annotation,
|
|
228
|
+
is_in_loop=is_in_loop,
|
|
229
|
+
is_async_function=self._is_async_function_stack[-1] if self._is_async_function_stack else False,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
sentinel_location = ast.copy_location(ast.Pass(), node)
|
|
233
|
+
sentinel_location.end_lineno = sentinel_location.lineno
|
|
234
|
+
sentinel_location.end_col_offset = sentinel_location.col_offset
|
|
235
|
+
|
|
236
|
+
return [ast.copy_location(statement, sentinel_location) for statement in statements] # type: ignore[return-value]
|
|
237
|
+
|
|
238
|
+
return node
|
|
239
|
+
|
|
240
|
+
def _current_return_annotation_expression(self) -> ast.expr:
|
|
241
|
+
if not self._return_annotation_stack:
|
|
242
|
+
return ast.Name(id="object", ctx=ast.Load())
|
|
243
|
+
annotation = self._return_annotation_stack[-1]
|
|
244
|
+
if annotation is None:
|
|
245
|
+
return ast.Name(id="object", ctx=ast.Load())
|
|
246
|
+
return annotation
|
|
247
|
+
|
|
248
|
+
def _collect_binding_name_to_type_expression(
|
|
249
|
+
self,
|
|
250
|
+
node: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
251
|
+
) -> dict[str, ast.expr]:
|
|
252
|
+
binding_name_to_type_expression: dict[str, ast.expr] = {}
|
|
253
|
+
|
|
254
|
+
for argument in [
|
|
255
|
+
*node.args.posonlyargs,
|
|
256
|
+
*node.args.args,
|
|
257
|
+
*node.args.kwonlyargs,
|
|
258
|
+
]:
|
|
259
|
+
if argument.annotation is not None:
|
|
260
|
+
binding_name_to_type_expression[argument.arg] = argument.annotation
|
|
261
|
+
|
|
262
|
+
for statement in node.body:
|
|
263
|
+
if isinstance(statement, ast.AnnAssign):
|
|
264
|
+
target = statement.target
|
|
265
|
+
if isinstance(target, ast.Name):
|
|
266
|
+
binding_name_to_type_expression[target.id] = statement.annotation
|
|
267
|
+
|
|
268
|
+
return binding_name_to_type_expression
|
|
269
|
+
|
|
270
|
+
def _current_binding_types_dict_expression(self, binding_names: tuple[str, ...]) -> ast.expr:
|
|
271
|
+
if not binding_names:
|
|
272
|
+
return ast.Dict(keys=[], values=[])
|
|
273
|
+
|
|
274
|
+
binding_name_to_type_expression: dict[str, ast.expr] = {}
|
|
275
|
+
if self._binding_name_to_type_expression_stack:
|
|
276
|
+
binding_name_to_type_expression = self._binding_name_to_type_expression_stack[-1]
|
|
277
|
+
|
|
278
|
+
keys: list[ast.expr | None] = []
|
|
279
|
+
values: list[ast.expr] = []
|
|
280
|
+
for name in binding_names:
|
|
281
|
+
keys.append(ast.Constant(name))
|
|
282
|
+
values.append(binding_name_to_type_expression.get(name, ast.Name(id="object", ctx=ast.Load())))
|
|
283
|
+
|
|
284
|
+
return ast.Dict(keys=keys, values=values)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def build_runtime_call_and_assignments(
|
|
288
|
+
natural_program_expression: ast.expr,
|
|
289
|
+
input_binding_names: tuple[str, ...],
|
|
290
|
+
output_binding_names: tuple[str, ...],
|
|
291
|
+
binding_types_dict_expression: ast.expr,
|
|
292
|
+
return_annotation: ast.expr,
|
|
293
|
+
*,
|
|
294
|
+
is_in_loop: bool,
|
|
295
|
+
is_async_function: bool,
|
|
296
|
+
) -> list[ast.stmt]:
|
|
297
|
+
# Build the runner method call (the only part requiring hand-built AST
|
|
298
|
+
# because natural_program_expression, binding_types_dict_expression, and
|
|
299
|
+
# return_annotation are dynamic AST nodes from the source).
|
|
300
|
+
method_name = "run_step_async" if is_async_function else "run_step"
|
|
301
|
+
call_expression = ast.Call(
|
|
302
|
+
func=ast.Attribute(
|
|
303
|
+
value=ast.Name(id="__nighthawk_runner__", ctx=ast.Load()),
|
|
304
|
+
attr=method_name,
|
|
305
|
+
ctx=ast.Load(),
|
|
306
|
+
),
|
|
307
|
+
args=[
|
|
308
|
+
natural_program_expression,
|
|
309
|
+
ast.List(elts=[ast.Constant(name) for name in input_binding_names], ctx=ast.Load()),
|
|
310
|
+
ast.List(elts=[ast.Constant(name) for name in output_binding_names], ctx=ast.Load()),
|
|
311
|
+
binding_types_dict_expression,
|
|
312
|
+
return_annotation,
|
|
313
|
+
ast.Constant(is_in_loop),
|
|
314
|
+
],
|
|
315
|
+
keywords=[],
|
|
316
|
+
)
|
|
317
|
+
envelope_value: ast.expr = ast.Await(value=call_expression) if is_async_function else call_expression
|
|
318
|
+
|
|
319
|
+
# Parse template for the common envelope-unpacking structure.
|
|
320
|
+
statements: list[ast.stmt] = ast.parse('__nh_envelope__ = None\n__nh_bindings__ = __nh_envelope__["bindings"]\n').body
|
|
321
|
+
# Replace the None placeholder with the actual call expression.
|
|
322
|
+
statements[0].value = envelope_value # type: ignore[attr-defined]
|
|
323
|
+
|
|
324
|
+
# Add binding commit assignments (dynamic per output binding).
|
|
325
|
+
for name in output_binding_names:
|
|
326
|
+
statements.extend(ast.parse(f'if "{name}" in __nh_bindings__:\n {name} = __nh_bindings__["{name}"]\n').body)
|
|
327
|
+
|
|
328
|
+
# Add outcome extraction and dispatch.
|
|
329
|
+
statements.extend(ast.parse('__nh_step_outcome__ = __nh_envelope__["step_outcome"]').body)
|
|
330
|
+
|
|
331
|
+
outcome_source = (
|
|
332
|
+
'if __nh_step_outcome__ is not None:\n if __nh_step_outcome__.kind == "return":\n return __nh_envelope__["return_value"]\n'
|
|
333
|
+
)
|
|
334
|
+
if is_in_loop:
|
|
335
|
+
outcome_source += (
|
|
336
|
+
' if __nh_step_outcome__.kind == "break":\n break\n if __nh_step_outcome__.kind == "continue":\n continue\n'
|
|
337
|
+
)
|
|
338
|
+
statements.extend(ast.parse(outcome_source).body)
|
|
339
|
+
|
|
340
|
+
return statements
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def transform_module_ast(module: ast.Module, *, captured_name_tuple: tuple[str, ...] = ()) -> ast.Module:
|
|
344
|
+
module = NaturalTransformer(captured_name_tuple=captured_name_tuple).visit(module) # type: ignore[assignment]
|
|
345
|
+
ast.fix_missing_locations(module)
|
|
346
|
+
return module
|
|
File without changes
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import contextvars
|
|
5
|
+
import inspect
|
|
6
|
+
import threading
|
|
7
|
+
from collections.abc import Awaitable, Callable, Coroutine
|
|
8
|
+
from typing import Any, cast
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def run_coroutine_synchronously(coroutine_call: Callable[[], Coroutine[Any, Any, Any]]) -> Any:
|
|
12
|
+
try:
|
|
13
|
+
asyncio.get_running_loop()
|
|
14
|
+
except RuntimeError:
|
|
15
|
+
return asyncio.run(coroutine_call())
|
|
16
|
+
|
|
17
|
+
execution_context = contextvars.copy_context()
|
|
18
|
+
result_container: dict[str, Any] = {}
|
|
19
|
+
exception_container: dict[str, BaseException] = {}
|
|
20
|
+
|
|
21
|
+
def run_coroutine_call_in_thread() -> None:
|
|
22
|
+
try:
|
|
23
|
+
result_container["result"] = execution_context.run(lambda: asyncio.run(coroutine_call()))
|
|
24
|
+
except BaseException as exception:
|
|
25
|
+
exception_container["exception"] = exception
|
|
26
|
+
|
|
27
|
+
thread = threading.Thread(
|
|
28
|
+
target=run_coroutine_call_in_thread,
|
|
29
|
+
name="nighthawk-sync-bridge",
|
|
30
|
+
)
|
|
31
|
+
thread.start()
|
|
32
|
+
thread.join()
|
|
33
|
+
|
|
34
|
+
exception = exception_container.get("exception")
|
|
35
|
+
if exception is not None:
|
|
36
|
+
raise exception.with_traceback(exception.__traceback__)
|
|
37
|
+
|
|
38
|
+
return result_container["result"]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def run_awaitable_value_synchronously(value: object) -> object:
|
|
42
|
+
if not inspect.isawaitable(value):
|
|
43
|
+
return value
|
|
44
|
+
|
|
45
|
+
typed_awaitable_value = cast(Awaitable[Any], value)
|
|
46
|
+
|
|
47
|
+
async def _await_value() -> Any:
|
|
48
|
+
return await typed_awaitable_value
|
|
49
|
+
|
|
50
|
+
return run_coroutine_synchronously(_await_value)
|