nighthawk-python 0.13.0__py3-none-any.whl → 0.14.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 +8 -5
- nighthawk/errors.py +15 -1
- nighthawk/lifecycle.py +129 -0
- nighthawk/natural/decorator.py +33 -42
- nighthawk/natural/transform.py +20 -5
- nighthawk/oversight.py +37 -10
- nighthawk/runtime/execution_reference.py +13 -0
- nighthawk/runtime/prompt.py +6 -6
- nighthawk/runtime/runner.py +285 -187
- nighthawk/runtime/scoping.py +48 -34
- nighthawk/runtime/step_context.py +2 -1
- nighthawk/testing.py +4 -0
- nighthawk/tools/execution.py +6 -11
- {nighthawk_python-0.13.0.dist-info → nighthawk_python-0.14.0.dist-info}/METADATA +1 -1
- {nighthawk_python-0.13.0.dist-info → nighthawk_python-0.14.0.dist-info}/RECORD +17 -15
- {nighthawk_python-0.13.0.dist-info → nighthawk_python-0.14.0.dist-info}/WHEEL +0 -0
- {nighthawk_python-0.13.0.dist-info → nighthawk_python-0.14.0.dist-info}/licenses/LICENSE +0 -0
nighthawk/__init__.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from . import oversight, resilience
|
|
3
|
+
from . import lifecycle, oversight, resilience
|
|
4
4
|
from .composition import UNSET, Extend, Merge, UnsetType
|
|
5
5
|
from .configuration import (
|
|
6
6
|
StepContextLimits,
|
|
@@ -20,11 +20,12 @@ from .errors import (
|
|
|
20
20
|
from .json_renderer import JsonableValue, to_jsonable_value
|
|
21
21
|
from .natural.decorator import get_transformed_function, natural_function
|
|
22
22
|
from .runtime.scoping import (
|
|
23
|
-
|
|
23
|
+
ExecutionReference,
|
|
24
24
|
UsageMeter,
|
|
25
25
|
get_capabilities,
|
|
26
|
-
|
|
26
|
+
get_execution_reference,
|
|
27
27
|
get_implicit_references,
|
|
28
|
+
get_lifecycle,
|
|
28
29
|
get_oversight,
|
|
29
30
|
get_step_executor,
|
|
30
31
|
get_system_prompt_suffix_fragments,
|
|
@@ -46,7 +47,7 @@ __all__ = [
|
|
|
46
47
|
"ToolNameConflictError",
|
|
47
48
|
"AgentStepExecutor",
|
|
48
49
|
"ExecutionError",
|
|
49
|
-
"
|
|
50
|
+
"ExecutionReference",
|
|
50
51
|
"JsonableValue",
|
|
51
52
|
"NaturalParseError",
|
|
52
53
|
"NighthawkError",
|
|
@@ -62,7 +63,7 @@ __all__ = [
|
|
|
62
63
|
"get_capabilities",
|
|
63
64
|
"get_step_context",
|
|
64
65
|
"get_usage_meter",
|
|
65
|
-
"
|
|
66
|
+
"get_execution_reference",
|
|
66
67
|
"get_implicit_references",
|
|
67
68
|
"get_oversight",
|
|
68
69
|
"get_step_executor",
|
|
@@ -70,6 +71,8 @@ __all__ = [
|
|
|
70
71
|
"get_system_prompt_suffix_fragments",
|
|
71
72
|
"get_user_prompt_suffix_fragments",
|
|
72
73
|
"oversight",
|
|
74
|
+
"lifecycle",
|
|
75
|
+
"get_lifecycle",
|
|
73
76
|
"natural_function",
|
|
74
77
|
"get_transformed_function",
|
|
75
78
|
"resilience",
|
nighthawk/errors.py
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from .lifecycle import StepFailed
|
|
7
|
+
|
|
3
8
|
|
|
4
9
|
class NighthawkError(Exception):
|
|
5
10
|
"""Base exception for all Nighthawk errors."""
|
|
@@ -10,7 +15,16 @@ class NaturalParseError(NighthawkError):
|
|
|
10
15
|
|
|
11
16
|
|
|
12
17
|
class ExecutionError(NighthawkError):
|
|
13
|
-
"""
|
|
18
|
+
"""Runtime failure exposing its terminal record and chaining the original error.
|
|
19
|
+
|
|
20
|
+
Internal helpers may supply a message; the runtime boundary attaches a
|
|
21
|
+
StepFailed record to every outward internal execution failure.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, failure: StepFailed | str, *, description: str | None = None) -> None:
|
|
25
|
+
self.step_failed = None if isinstance(failure, str) else failure
|
|
26
|
+
message = failure if isinstance(failure, str) else str(failure.original_exception)
|
|
27
|
+
super().__init__(f"{description}: {message}" if description else message)
|
|
14
28
|
|
|
15
29
|
|
|
16
30
|
class ToolEvaluationError(NighthawkError):
|
nighthawk/lifecycle.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Terminal delivery for host-owned execution ledgers.
|
|
2
|
+
|
|
3
|
+
Records snapshot collection structure, retaining the exact live Python values.
|
|
4
|
+
Hosts own serialization, persistence, and deduplication by step_execution_id.
|
|
5
|
+
Delivery is synchronous and attempted once, after caller binding assignments.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from collections.abc import Callable, Mapping
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from types import MappingProxyType
|
|
11
|
+
from typing import Literal
|
|
12
|
+
|
|
13
|
+
from .errors import NighthawkError
|
|
14
|
+
from .runtime.execution_reference import ExecutionReference
|
|
15
|
+
from .runtime.step_contract import StepKind, StepOutcome
|
|
16
|
+
from .runtime.step_result import Break, Continue, Pass, Raise, Return, StepResult
|
|
17
|
+
|
|
18
|
+
type FailureStage = Literal[
|
|
19
|
+
"preparation",
|
|
20
|
+
"executor",
|
|
21
|
+
"outcome_validation",
|
|
22
|
+
"binding_validation",
|
|
23
|
+
"return_inspection",
|
|
24
|
+
"return_evaluation",
|
|
25
|
+
"return_await",
|
|
26
|
+
"return_validation",
|
|
27
|
+
"commit_inspection",
|
|
28
|
+
"rewrite_validation",
|
|
29
|
+
"oversight_rejection",
|
|
30
|
+
"raise_resolution",
|
|
31
|
+
"raise_construction",
|
|
32
|
+
"binding_assignment",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True, kw_only=True)
|
|
37
|
+
class _StepFinished:
|
|
38
|
+
execution_reference: ExecutionReference
|
|
39
|
+
processed_natural_program: str | None = None
|
|
40
|
+
input_binding_name_to_value: Mapping[str, object] | None = None
|
|
41
|
+
allowed_step_kinds: tuple[StepKind, ...] | None = None
|
|
42
|
+
assigned_binding_name_to_value: Mapping[str, object] = field(default_factory=dict)
|
|
43
|
+
|
|
44
|
+
def __post_init__(self) -> None:
|
|
45
|
+
if self.execution_reference.step_execution_id is None or self.execution_reference.source_location is None:
|
|
46
|
+
raise ValueError("Terminal records require invocation and source identity")
|
|
47
|
+
for name in ("input_binding_name_to_value", "assigned_binding_name_to_value", "validated_binding_name_to_value"):
|
|
48
|
+
value = getattr(self, name, None)
|
|
49
|
+
if value is not None:
|
|
50
|
+
object.__setattr__(self, name, MappingProxyType(dict(value)))
|
|
51
|
+
if self.allowed_step_kinds is not None:
|
|
52
|
+
object.__setattr__(self, "allowed_step_kinds", tuple(self.allowed_step_kinds))
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(frozen=True, kw_only=True)
|
|
56
|
+
class StepCompleted(_StepFinished):
|
|
57
|
+
"""A resolved control result whose generated assignments completed."""
|
|
58
|
+
|
|
59
|
+
outcome: Pass | Return | Break | Continue
|
|
60
|
+
kind: Literal["completed"] = field(default="completed", init=False)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(frozen=True, kw_only=True)
|
|
64
|
+
class StepRaised(_StepFinished):
|
|
65
|
+
"""An approved DSL Raise, with its successfully constructed exception."""
|
|
66
|
+
|
|
67
|
+
outcome: Raise
|
|
68
|
+
exception: BaseException
|
|
69
|
+
kind: Literal["raised"] = field(default="raised", init=False)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass(frozen=True, kw_only=True)
|
|
73
|
+
class StepFailed(_StepFinished):
|
|
74
|
+
"""An ordinary execution failure, distinct from a DSL-selected Raise."""
|
|
75
|
+
|
|
76
|
+
failure_stage: FailureStage
|
|
77
|
+
original_exception: Exception
|
|
78
|
+
attempted_outcome: StepOutcome | StepResult | None = None
|
|
79
|
+
validated_binding_name_to_value: Mapping[str, object] | None = None
|
|
80
|
+
inspection_subject: Literal["return_expression", "step_commit", "tool_call"] | None = None
|
|
81
|
+
rejection_reason: str | None = None
|
|
82
|
+
kind: Literal["failed"] = field(default="failed", init=False)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass(frozen=True, kw_only=True)
|
|
86
|
+
class StepInterrupted(_StepFinished):
|
|
87
|
+
"""Cancellation or process control; propagation takes priority over delivery."""
|
|
88
|
+
|
|
89
|
+
failure_stage: FailureStage
|
|
90
|
+
original_exception: BaseException
|
|
91
|
+
kind: Literal["interrupted"] = field(default="interrupted", init=False)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
type StepFinished = StepCompleted | StepRaised | StepFailed | StepInterrupted
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@dataclass(frozen=True)
|
|
98
|
+
class StepLifecycle:
|
|
99
|
+
"""Scoped synchronous notification, inherited with UNSET and cleared by None.
|
|
100
|
+
|
|
101
|
+
A callback may persist a record and raise a host exception referencing it.
|
|
102
|
+
Callback errors never trigger another execution notification or rollback.
|
|
103
|
+
Rethrowing the exact original exception preserves its existing cause.
|
|
104
|
+
Host deduplication must match both execution identity and the stored event.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
on_step_finished: Callable[[StepFinished], None] | None = None
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class StepDeliveryError(NighthawkError):
|
|
111
|
+
"""Host adapter delivery failure; does not assert that persistence succeeded."""
|
|
112
|
+
|
|
113
|
+
def __init__(self, execution_reference: ExecutionReference, cause: BaseException) -> None:
|
|
114
|
+
self.execution_reference = execution_reference
|
|
115
|
+
self.delivery_cause = cause
|
|
116
|
+
super().__init__(f"Step terminal delivery failed: {cause}")
|
|
117
|
+
self.__cause__ = cause
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
__all__ = [
|
|
121
|
+
"FailureStage",
|
|
122
|
+
"StepCompleted",
|
|
123
|
+
"StepDeliveryError",
|
|
124
|
+
"StepFailed",
|
|
125
|
+
"StepFinished",
|
|
126
|
+
"StepInterrupted",
|
|
127
|
+
"StepLifecycle",
|
|
128
|
+
"StepRaised",
|
|
129
|
+
]
|
nighthawk/natural/decorator.py
CHANGED
|
@@ -6,10 +6,12 @@ import logging
|
|
|
6
6
|
import sys
|
|
7
7
|
import textwrap
|
|
8
8
|
from collections.abc import Awaitable, Callable
|
|
9
|
+
from contextlib import AbstractContextManager
|
|
9
10
|
from functools import wraps
|
|
11
|
+
from types import FunctionType
|
|
10
12
|
from typing import Any, cast
|
|
11
13
|
|
|
12
|
-
from ..runtime.runner import Runner
|
|
14
|
+
from ..runtime.runner import Runner
|
|
13
15
|
from ..runtime.scoping import get_step_executor
|
|
14
16
|
from ..runtime.step_context import python_cell_scope, python_name_scope
|
|
15
17
|
from .blocks import extract_program, find_natural_blocks
|
|
@@ -20,48 +22,10 @@ type NaturalFunctionCallable = Callable[..., Any]
|
|
|
20
22
|
|
|
21
23
|
class _RunnerProxy:
|
|
22
24
|
@staticmethod
|
|
23
|
-
def
|
|
24
|
-
natural_program: str,
|
|
25
|
-
input_binding_names: list[str],
|
|
26
|
-
output_binding_names: list[str],
|
|
27
|
-
binding_name_to_type: dict[str, object],
|
|
28
|
-
return_annotation: object,
|
|
29
|
-
is_in_loop: bool,
|
|
30
|
-
) -> StepEnvelope:
|
|
25
|
+
def execution() -> AbstractContextManager[Runner]:
|
|
31
26
|
caller_frame = sys._getframe(1)
|
|
32
|
-
|
|
33
|
-
runner
|
|
34
|
-
return runner.run_step(
|
|
35
|
-
natural_program,
|
|
36
|
-
input_binding_names,
|
|
37
|
-
output_binding_names,
|
|
38
|
-
binding_name_to_type,
|
|
39
|
-
return_annotation,
|
|
40
|
-
is_in_loop,
|
|
41
|
-
caller_frame=caller_frame,
|
|
42
|
-
)
|
|
43
|
-
|
|
44
|
-
@staticmethod
|
|
45
|
-
async def run_step_async(
|
|
46
|
-
natural_program: str,
|
|
47
|
-
input_binding_names: list[str],
|
|
48
|
-
output_binding_names: list[str],
|
|
49
|
-
binding_name_to_type: dict[str, object],
|
|
50
|
-
return_annotation: object,
|
|
51
|
-
is_in_loop: bool,
|
|
52
|
-
) -> StepEnvelope:
|
|
53
|
-
caller_frame = sys._getframe(1)
|
|
54
|
-
current_step_executor = get_step_executor()
|
|
55
|
-
runner = Runner(current_step_executor)
|
|
56
|
-
return await runner.run_step_async(
|
|
57
|
-
natural_program,
|
|
58
|
-
input_binding_names,
|
|
59
|
-
output_binding_names,
|
|
60
|
-
binding_name_to_type,
|
|
61
|
-
return_annotation,
|
|
62
|
-
is_in_loop,
|
|
63
|
-
caller_frame=caller_frame,
|
|
64
|
-
)
|
|
27
|
+
runner = Runner(get_step_executor())
|
|
28
|
+
return runner.execution(caller_frame=caller_frame)
|
|
65
29
|
|
|
66
30
|
|
|
67
31
|
def _extract_inline_fstring_name_set(function_source: str, *, function_name: str) -> set[str]:
|
|
@@ -222,6 +186,7 @@ def natural_function(func: NaturalFunctionCallable | None = None) -> NaturalFunc
|
|
|
222
186
|
original_module = ast.Module(body=[], type_ignores=[])
|
|
223
187
|
|
|
224
188
|
capture_name_set = _build_capture_name_set(source, func.__name__)
|
|
189
|
+
capture_name_set.update(func.__code__.co_freevars)
|
|
225
190
|
|
|
226
191
|
definition_frame = inspect.currentframe()
|
|
227
192
|
name_to_value: dict[str, object] = {}
|
|
@@ -232,6 +197,13 @@ def natural_function(func: NaturalFunctionCallable | None = None) -> NaturalFunc
|
|
|
232
197
|
if name in caller_frame.f_locals:
|
|
233
198
|
name_to_value[name] = caller_frame.f_locals[name]
|
|
234
199
|
|
|
200
|
+
original_name_to_cell = dict(zip(func.__code__.co_freevars, func.__closure__ or (), strict=True))
|
|
201
|
+
for name, cell in original_name_to_cell.items():
|
|
202
|
+
try:
|
|
203
|
+
name_to_value[name] = cell.cell_contents
|
|
204
|
+
except ValueError:
|
|
205
|
+
name_to_value[name] = None
|
|
206
|
+
|
|
235
207
|
captured_name_tuple = tuple(sorted(capture_name_set))
|
|
236
208
|
|
|
237
209
|
transformed_module = transform_module_ast(original_module, captured_name_tuple=captured_name_tuple)
|
|
@@ -267,6 +239,25 @@ def natural_function(func: NaturalFunctionCallable | None = None) -> NaturalFunc
|
|
|
267
239
|
if not callable(transformed):
|
|
268
240
|
raise RuntimeError("Transformed function not found after factory execution")
|
|
269
241
|
|
|
242
|
+
if transformed.__closure__ is not None:
|
|
243
|
+
# Preserve original Python cells, including nonlocal writes and late rebinding.
|
|
244
|
+
transformed = FunctionType(
|
|
245
|
+
transformed.__code__,
|
|
246
|
+
transformed.__globals__,
|
|
247
|
+
transformed.__name__,
|
|
248
|
+
transformed.__defaults__,
|
|
249
|
+
tuple(
|
|
250
|
+
original_name_to_cell.get(name, cell)
|
|
251
|
+
for name, cell in zip(
|
|
252
|
+
transformed.__code__.co_freevars,
|
|
253
|
+
transformed.__closure__,
|
|
254
|
+
strict=True,
|
|
255
|
+
)
|
|
256
|
+
),
|
|
257
|
+
)
|
|
258
|
+
transformed.__kwdefaults__ = func.__kwdefaults__
|
|
259
|
+
transformed.__annotations__ = func.__annotations__
|
|
260
|
+
|
|
270
261
|
transformed_freevar_name_set = set(transformed.__code__.co_freevars)
|
|
271
262
|
captured_name_set = set(factory_name_to_value.keys())
|
|
272
263
|
|
nighthawk/natural/transform.py
CHANGED
|
@@ -12,6 +12,12 @@ from .blocks import (
|
|
|
12
12
|
)
|
|
13
13
|
|
|
14
14
|
|
|
15
|
+
def _copy_generated_location(statement: ast.stmt, source: ast.AST) -> ast.stmt:
|
|
16
|
+
for child in ast.walk(statement):
|
|
17
|
+
ast.copy_location(child, source)
|
|
18
|
+
return statement
|
|
19
|
+
|
|
20
|
+
|
|
15
21
|
class NaturalTransformer(ast.NodeTransformer):
|
|
16
22
|
def __init__(self, *, captured_name_tuple: tuple[str, ...]) -> None:
|
|
17
23
|
super().__init__()
|
|
@@ -58,7 +64,7 @@ class NaturalTransformer(ast.NodeTransformer):
|
|
|
58
64
|
sentinel_location.end_lineno = sentinel_location.lineno
|
|
59
65
|
sentinel_location.end_col_offset = sentinel_location.col_offset
|
|
60
66
|
|
|
61
|
-
injected_with_location = [
|
|
67
|
+
injected_with_location = [_copy_generated_location(statement, sentinel_location) for statement in injected]
|
|
62
68
|
|
|
63
69
|
body_without_docstring = node.body[1:]
|
|
64
70
|
node.body = injected_with_location + body_without_docstring
|
|
@@ -201,7 +207,7 @@ class NaturalTransformer(ast.NodeTransformer):
|
|
|
201
207
|
sentinel_location.end_lineno = sentinel_location.lineno
|
|
202
208
|
sentinel_location.end_col_offset = sentinel_location.col_offset
|
|
203
209
|
|
|
204
|
-
return [
|
|
210
|
+
return [_copy_generated_location(statement, sentinel_location) for statement in statements] # type: ignore[return-value]
|
|
205
211
|
|
|
206
212
|
if isinstance(value, ast.JoinedStr) and _joined_string_is_natural_sentinel(value):
|
|
207
213
|
_validate_joined_string_bindings_do_not_span_formatted_values(value)
|
|
@@ -233,7 +239,7 @@ class NaturalTransformer(ast.NodeTransformer):
|
|
|
233
239
|
sentinel_location.end_lineno = sentinel_location.lineno
|
|
234
240
|
sentinel_location.end_col_offset = sentinel_location.col_offset
|
|
235
241
|
|
|
236
|
-
return [
|
|
242
|
+
return [_copy_generated_location(statement, sentinel_location) for statement in statements] # type: ignore[return-value]
|
|
237
243
|
|
|
238
244
|
return node
|
|
239
245
|
|
|
@@ -300,7 +306,7 @@ def build_runtime_call_and_assignments(
|
|
|
300
306
|
method_name = "run_step_async" if is_async_function else "run_step"
|
|
301
307
|
call_expression = ast.Call(
|
|
302
308
|
func=ast.Attribute(
|
|
303
|
-
value=ast.Name(id="
|
|
309
|
+
value=ast.Name(id="__nh_execution__", ctx=ast.Load()),
|
|
304
310
|
attr=method_name,
|
|
305
311
|
ctx=ast.Load(),
|
|
306
312
|
),
|
|
@@ -323,7 +329,16 @@ def build_runtime_call_and_assignments(
|
|
|
323
329
|
|
|
324
330
|
# Add binding commit assignments (dynamic per output binding).
|
|
325
331
|
for name in output_binding_names:
|
|
326
|
-
statements.extend(
|
|
332
|
+
statements.extend(
|
|
333
|
+
ast.parse(
|
|
334
|
+
f'if "{name}" in __nh_bindings__:\n {name} = __nh_bindings__["{name}"]\n __nh_execution__.record_assignment("{name}", {name})\n'
|
|
335
|
+
).body
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
guarded = ast.parse("with __nighthawk_runner__.execution() as __nh_execution__:\n pass\n").body[0]
|
|
339
|
+
assert isinstance(guarded, ast.With)
|
|
340
|
+
guarded.body = statements
|
|
341
|
+
statements = [guarded]
|
|
327
342
|
|
|
328
343
|
# Add outcome extraction and dispatch.
|
|
329
344
|
statements.extend(ast.parse('__nh_step_outcome__ = __nh_envelope__["outcome"]').body)
|
nighthawk/oversight.py
CHANGED
|
@@ -8,7 +8,7 @@ from opentelemetry.trace import get_current_span
|
|
|
8
8
|
|
|
9
9
|
from .composition import UNSET, UnsetType
|
|
10
10
|
from .errors import NighthawkError
|
|
11
|
-
from .runtime.
|
|
11
|
+
from .runtime.execution_reference import ExecutionReference
|
|
12
12
|
from .runtime.step_contract import StepKind
|
|
13
13
|
from .runtime.step_result import Break, Continue, Pass, Raise, Return, StepResult
|
|
14
14
|
|
|
@@ -18,12 +18,17 @@ def _reference_mapping(name_to_value: Mapping[str, object]) -> Mapping[str, obje
|
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
class OversightRejectedError(NighthawkError):
|
|
21
|
-
"""Raised when oversight rejects
|
|
21
|
+
"""Raised when oversight explicitly rejects an execution boundary."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, reason: str, *, subject: str = "tool_call") -> None:
|
|
24
|
+
self.reason = reason
|
|
25
|
+
self.subject = subject
|
|
26
|
+
super().__init__(reason)
|
|
22
27
|
|
|
23
28
|
|
|
24
29
|
@dataclass(frozen=True)
|
|
25
30
|
class ToolCall:
|
|
26
|
-
|
|
31
|
+
execution_reference: ExecutionReference
|
|
27
32
|
tool_name: str
|
|
28
33
|
argument_name_to_value: Mapping[str, object]
|
|
29
34
|
processed_natural_program: str
|
|
@@ -46,7 +51,7 @@ class StepCommit:
|
|
|
46
51
|
Rewrite for changes and copy or serialize explicitly for durable history.
|
|
47
52
|
"""
|
|
48
53
|
|
|
49
|
-
|
|
54
|
+
execution_reference: ExecutionReference
|
|
50
55
|
processed_natural_program: str
|
|
51
56
|
input_binding_name_to_value: Mapping[str, object]
|
|
52
57
|
outcome: StepResult
|
|
@@ -78,6 +83,25 @@ class StepCommit:
|
|
|
78
83
|
)
|
|
79
84
|
|
|
80
85
|
|
|
86
|
+
@dataclass(frozen=True)
|
|
87
|
+
class ReturnExpression:
|
|
88
|
+
"""Trusted expression approval before core evaluation, await, and validation.
|
|
89
|
+
|
|
90
|
+
The mapping contains writes supplied and validated for this candidate, not
|
|
91
|
+
every declared write binding. A bare name can still refer to an awaitable;
|
|
92
|
+
return validation and later rewrites can change the relationship to writes.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
execution_reference: ExecutionReference
|
|
96
|
+
expression: str
|
|
97
|
+
expected_type: object
|
|
98
|
+
processed_natural_program: str
|
|
99
|
+
validated_binding_name_to_value: Mapping[str, object]
|
|
100
|
+
|
|
101
|
+
def __post_init__(self) -> None:
|
|
102
|
+
object.__setattr__(self, "validated_binding_name_to_value", _reference_mapping(self.validated_binding_name_to_value))
|
|
103
|
+
|
|
104
|
+
|
|
81
105
|
@dataclass(frozen=True)
|
|
82
106
|
class Accept:
|
|
83
107
|
reason: str | None = None
|
|
@@ -121,6 +145,7 @@ type StepCommitDecision = Accept | Reject | Rewrite
|
|
|
121
145
|
|
|
122
146
|
@dataclass(frozen=True)
|
|
123
147
|
class Oversight:
|
|
148
|
+
inspect_return_expression: Callable[[ReturnExpression], Accept | Reject] | None = None
|
|
124
149
|
inspect_tool_call: Callable[[ToolCall], ToolCallDecision] | None = None
|
|
125
150
|
inspect_step_commit: Callable[[StepCommit], StepCommitDecision] | None = None
|
|
126
151
|
|
|
@@ -129,20 +154,21 @@ def record_oversight_decision(
|
|
|
129
154
|
*,
|
|
130
155
|
subject: str,
|
|
131
156
|
verdict: str,
|
|
132
|
-
|
|
157
|
+
execution_reference: ExecutionReference,
|
|
133
158
|
tool_name: str | None = None,
|
|
134
159
|
reason: str | None = None,
|
|
135
160
|
) -> None:
|
|
136
161
|
current_span = get_current_span()
|
|
137
162
|
if not current_span.is_recording():
|
|
138
163
|
return
|
|
139
|
-
if
|
|
140
|
-
raise NighthawkError("Oversight decision events require
|
|
164
|
+
if execution_reference.step_execution_id is None:
|
|
165
|
+
raise NighthawkError("Oversight decision events require ExecutionReference.step_execution_id")
|
|
141
166
|
|
|
142
167
|
attributes: dict[str, str] = {
|
|
143
|
-
"run.id":
|
|
144
|
-
"scope.id":
|
|
145
|
-
"step.id":
|
|
168
|
+
"run.id": execution_reference.run_id,
|
|
169
|
+
"scope.id": execution_reference.scope_id,
|
|
170
|
+
"step.execution.id": execution_reference.step_execution_id,
|
|
171
|
+
"step.source_location": execution_reference.source_location or "",
|
|
146
172
|
"nighthawk.oversight.subject": subject,
|
|
147
173
|
"nighthawk.oversight.verdict": verdict,
|
|
148
174
|
}
|
|
@@ -167,6 +193,7 @@ __all__ = [
|
|
|
167
193
|
"Reject",
|
|
168
194
|
"Rewrite",
|
|
169
195
|
"StepCommit",
|
|
196
|
+
"ReturnExpression",
|
|
170
197
|
"StepCommitDecision",
|
|
171
198
|
"ToolCall",
|
|
172
199
|
"ToolCallDecision",
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Immutable invocation identity, independent of runtime configuration."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass(frozen=True)
|
|
7
|
+
class ExecutionReference:
|
|
8
|
+
"""Run and scope identity, plus invocation and source identity during a step."""
|
|
9
|
+
|
|
10
|
+
run_id: str
|
|
11
|
+
scope_id: str
|
|
12
|
+
step_execution_id: str | None = None
|
|
13
|
+
source_location: str | None = None
|
nighthawk/runtime/prompt.py
CHANGED
|
@@ -23,9 +23,9 @@ from ._user_content import (
|
|
|
23
23
|
from .scoping import (
|
|
24
24
|
RUN_ID,
|
|
25
25
|
SCOPE_ID,
|
|
26
|
-
|
|
26
|
+
STEP_EXECUTION_ID,
|
|
27
27
|
_current_user_prompt_suffix_fragments,
|
|
28
|
-
|
|
28
|
+
get_execution_reference,
|
|
29
29
|
)
|
|
30
30
|
from .step_context import _MISSING, StepContext, resolve_name_in_step_context
|
|
31
31
|
|
|
@@ -538,7 +538,7 @@ def _render_reference_and_value_list_section(
|
|
|
538
538
|
if token_limit_reached:
|
|
539
539
|
dropped_multimodal = any(try_project_user_prompt_value(value) is not None for _, value in reference_and_value_list[shown_items:])
|
|
540
540
|
log_attributes: dict[str, Any] = {
|
|
541
|
-
|
|
541
|
+
STEP_EXECUTION_ID: step_context.execution_reference.step_execution_id or "",
|
|
542
542
|
"nighthawk.prompt_context.section": section_name,
|
|
543
543
|
"nighthawk.prompt_context.reason": "token_limit",
|
|
544
544
|
"nighthawk.prompt_context.rendered_items": shown_items,
|
|
@@ -549,9 +549,9 @@ def _render_reference_and_value_list_section(
|
|
|
549
549
|
# Spec §8.2.2: token truncation can drop explicit dotted multimodal
|
|
550
550
|
# leaves because section ordering and budgets remain authoritative.
|
|
551
551
|
try:
|
|
552
|
-
|
|
553
|
-
log_attributes[RUN_ID] =
|
|
554
|
-
log_attributes[SCOPE_ID] =
|
|
552
|
+
execution_reference = get_execution_reference()
|
|
553
|
+
log_attributes[RUN_ID] = execution_reference.run_id
|
|
554
|
+
log_attributes[SCOPE_ID] = execution_reference.scope_id
|
|
555
555
|
except Exception:
|
|
556
556
|
pass
|
|
557
557
|
logging.getLogger("nighthawk").info("prompt_context_truncated %s", log_attributes)
|