nighthawk-python 0.15.0__py3-none-any.whl → 0.16.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/composition.py +6 -2
- nighthawk/configuration.py +5 -4
- nighthawk/function_call.py +16 -4
- nighthawk/oversight.py +6 -3
- nighthawk/runtime/environment.py +50 -29
- nighthawk/runtime/expression.py +1 -1
- nighthawk/runtime/model_candidate.py +12 -4
- nighthawk/runtime/preparation.py +1 -4
- nighthawk/runtime/prompt.py +3 -17
- nighthawk/runtime/runner.py +12 -17
- nighthawk/runtime/scoping.py +13 -7
- nighthawk/runtime/step_context.py +2 -5
- nighthawk/runtime/step_executor.py +17 -11
- nighthawk/runtime/step_request.py +45 -7
- nighthawk/testing.py +25 -23
- {nighthawk_python-0.15.0.dist-info → nighthawk_python-0.16.0.dist-info}/METADATA +1 -1
- {nighthawk_python-0.15.0.dist-info → nighthawk_python-0.16.0.dist-info}/RECORD +19 -19
- {nighthawk_python-0.15.0.dist-info → nighthawk_python-0.16.0.dist-info}/WHEEL +0 -0
- {nighthawk_python-0.15.0.dist-info → nighthawk_python-0.16.0.dist-info}/licenses/LICENSE +0 -0
nighthawk/composition.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Omitted arguments and explicit collection composition operations."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
@@ -30,7 +30,11 @@ class UnsetType:
|
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
UNSET = UnsetType()
|
|
33
|
-
"""
|
|
33
|
+
"""An omitted argument: use the receiving API's documented default behavior.
|
|
34
|
+
|
|
35
|
+
Scopes inherit, runs create fresh resources, and rewrites retain the current field.
|
|
36
|
+
Ordinary values, including empty collections, remain explicit values.
|
|
37
|
+
"""
|
|
34
38
|
|
|
35
39
|
|
|
36
40
|
@dataclass(frozen=True, init=False)
|
nighthawk/configuration.py
CHANGED
|
@@ -121,8 +121,9 @@ class StepExecutorConfiguration(BaseModel):
|
|
|
121
121
|
Instances retain identity; the host owns client lifetime. Serialization
|
|
122
122
|
must explicitly exclude a live model field, including in nested records.
|
|
123
123
|
Configuration representations omit the model to avoid traversing credentials.
|
|
124
|
-
model_settings: Provider-specific
|
|
125
|
-
backend-specific BaseModel
|
|
124
|
+
model_settings: Provider-specific overrides. Defaults to an empty dict;
|
|
125
|
+
accepts a dict or a backend-specific BaseModel (converted to dict).
|
|
126
|
+
None and UNSET are invalid. An empty dict preserves model-owned defaults.
|
|
126
127
|
prompts: Prompt templates for step execution.
|
|
127
128
|
context_limits: Token and item limits for context rendering.
|
|
128
129
|
json_renderer_style: Headson rendering style for JSON summarization.
|
|
@@ -136,11 +137,11 @@ class StepExecutorConfiguration(BaseModel):
|
|
|
136
137
|
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
137
138
|
|
|
138
139
|
model: str | InstanceOf[Model] = Field(default="openai-responses:gpt-5.6-luna", repr=False)
|
|
139
|
-
model_settings: dict[str, Any] | BaseModel
|
|
140
|
+
model_settings: dict[str, Any] | BaseModel = Field(default_factory=dict)
|
|
140
141
|
|
|
141
142
|
@field_validator("model_settings", mode="before")
|
|
142
143
|
@classmethod
|
|
143
|
-
def _normalize_model_settings(cls, value: Any) -> dict[str, Any]
|
|
144
|
+
def _normalize_model_settings(cls, value: Any) -> dict[str, Any]:
|
|
144
145
|
if isinstance(value, BaseModel):
|
|
145
146
|
return value.model_dump()
|
|
146
147
|
return value
|
nighthawk/function_call.py
CHANGED
|
@@ -10,8 +10,9 @@ from contextlib import contextmanager, nullcontext
|
|
|
10
10
|
from contextvars import ContextVar
|
|
11
11
|
from dataclasses import dataclass
|
|
12
12
|
from types import MappingProxyType
|
|
13
|
-
from typing import Any, Protocol
|
|
13
|
+
from typing import Any, Protocol, runtime_checkable
|
|
14
14
|
|
|
15
|
+
from .composition import UNSET, UnsetType
|
|
15
16
|
from .configuration import ModelSelection
|
|
16
17
|
from .errors import NighthawkError
|
|
17
18
|
from .natural.definition import NaturalFunctionDefinition
|
|
@@ -31,6 +32,7 @@ class FunctionCall:
|
|
|
31
32
|
model_selection: ModelSelection | None
|
|
32
33
|
|
|
33
34
|
|
|
35
|
+
@runtime_checkable
|
|
34
36
|
class FunctionCallHandler(Protocol):
|
|
35
37
|
"""Application-owned admission, grouping, and observation of function calls."""
|
|
36
38
|
|
|
@@ -48,9 +50,19 @@ _call: ContextVar[FunctionCall | None] = ContextVar("nighthawk_function_call", d
|
|
|
48
50
|
|
|
49
51
|
|
|
50
52
|
@contextmanager
|
|
51
|
-
def function_call_scope(handler: FunctionCallHandler | None) -> Iterator[None]:
|
|
52
|
-
"""
|
|
53
|
-
|
|
53
|
+
def function_call_scope(handler: FunctionCallHandler | None | UnsetType = UNSET) -> Iterator[None]:
|
|
54
|
+
"""Inherit, replace, or clear interception independently of executor runs.
|
|
55
|
+
|
|
56
|
+
Omission or UNSET inherits; None clears; a handler replaces the current one.
|
|
57
|
+
Invalid handlers fail before context installation.
|
|
58
|
+
"""
|
|
59
|
+
if (
|
|
60
|
+
not isinstance(handler, UnsetType)
|
|
61
|
+
and handler is not None
|
|
62
|
+
and (not isinstance(handler, FunctionCallHandler) or not callable(handler.invoke) or not callable(handler.invoke_async))
|
|
63
|
+
):
|
|
64
|
+
raise TypeError("handler must implement FunctionCallHandler, be None, or be UNSET")
|
|
65
|
+
token = _handler.set(_handler.get() if isinstance(handler, UnsetType) else handler)
|
|
54
66
|
try:
|
|
55
67
|
yield
|
|
56
68
|
finally:
|
nighthawk/oversight.py
CHANGED
|
@@ -81,11 +81,12 @@ class Reject:
|
|
|
81
81
|
|
|
82
82
|
@dataclass(frozen=True)
|
|
83
83
|
class Rewrite:
|
|
84
|
-
"""Replace supplied commit fields; UNSET
|
|
84
|
+
"""Replace supplied commit fields; UNSET retains and Return(None) returns None.
|
|
85
85
|
|
|
86
86
|
A mapping replaces all writes. Use outcome=Return(value=...) to supply
|
|
87
87
|
a new return value or change another allowed outcome into a return.
|
|
88
88
|
Replacements are validated before commit without replaying return expressions.
|
|
89
|
+
A resolved Raise cannot be paired with an explicit nonempty output mapping.
|
|
89
90
|
"""
|
|
90
91
|
|
|
91
92
|
outcome: StepResult | UnsetType = UNSET
|
|
@@ -96,11 +97,13 @@ class Rewrite:
|
|
|
96
97
|
if not isinstance(self.outcome, (UnsetType, Pass, Return, Break, Continue, Raise)):
|
|
97
98
|
raise TypeError("Rewrite outcome must be a resolved StepResult or UNSET")
|
|
98
99
|
if not isinstance(self.binding_name_to_value, UnsetType):
|
|
99
|
-
if not isinstance(self.binding_name_to_value, Mapping):
|
|
100
|
-
raise TypeError("Rewrite binding_name_to_value must be a mapping or UNSET")
|
|
100
|
+
if not isinstance(self.binding_name_to_value, Mapping) or any(not isinstance(name, str) for name in self.binding_name_to_value):
|
|
101
|
+
raise TypeError("Rewrite binding_name_to_value must be a string-keyed mapping or UNSET")
|
|
101
102
|
object.__setattr__(self, "binding_name_to_value", _reference_mapping(self.binding_name_to_value))
|
|
102
103
|
if self.outcome is UNSET and self.binding_name_to_value is UNSET:
|
|
103
104
|
raise ValueError("Rewrite must change outcome or binding_name_to_value")
|
|
105
|
+
if isinstance(self.outcome, Raise) and not isinstance(self.binding_name_to_value, UnsetType) and len(self.binding_name_to_value):
|
|
106
|
+
raise ValueError("Rewrite Raise cannot include output bindings")
|
|
104
107
|
|
|
105
108
|
|
|
106
109
|
type StepCommitDecision = Accept | Reject | Rewrite
|
nighthawk/runtime/environment.py
CHANGED
|
@@ -5,13 +5,21 @@ from __future__ import annotations
|
|
|
5
5
|
from collections.abc import Iterator, Mapping
|
|
6
6
|
from dataclasses import dataclass
|
|
7
7
|
from types import CellType, MappingProxyType
|
|
8
|
-
from typing import Literal
|
|
8
|
+
from typing import Literal, Never
|
|
9
9
|
|
|
10
10
|
from .bindings import MISSING_ANNOTATION
|
|
11
11
|
from .description import annotation_text
|
|
12
12
|
from .discovery import DiscoveryResult, page_names
|
|
13
13
|
|
|
14
|
-
type EnvironmentSource = Literal["effective", "python", "
|
|
14
|
+
type EnvironmentSource = Literal["effective", "python", "task", "step"]
|
|
15
|
+
|
|
16
|
+
_EMPTY_NAME_TO_VALUE: Mapping[str, Never] = MappingProxyType({})
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _copy_name_to_value[T](name_to_value: Mapping[str, T], parameter: str) -> dict[str, T]:
|
|
20
|
+
if not isinstance(name_to_value, Mapping) or any(not isinstance(name, str) for name in name_to_value):
|
|
21
|
+
raise TypeError(f"{parameter} must be a string-keyed mapping")
|
|
22
|
+
return dict(name_to_value)
|
|
15
23
|
|
|
16
24
|
|
|
17
25
|
class MissingEnvironmentNameError(NameError):
|
|
@@ -63,6 +71,16 @@ class _ExpressionNamespace(dict[str, object]):
|
|
|
63
71
|
return self._override_name_to_value[name]
|
|
64
72
|
return self._environment.resolve(name)
|
|
65
73
|
|
|
74
|
+
def __contains__(self, name: object) -> bool:
|
|
75
|
+
# Python 3.14 forward references check membership before looking up a name.
|
|
76
|
+
if not isinstance(name, str):
|
|
77
|
+
return False
|
|
78
|
+
try:
|
|
79
|
+
self[name]
|
|
80
|
+
except MissingEnvironmentNameError:
|
|
81
|
+
return False
|
|
82
|
+
return True
|
|
83
|
+
|
|
66
84
|
|
|
67
85
|
@dataclass(frozen=True)
|
|
68
86
|
class _RuntimeAccess:
|
|
@@ -78,7 +96,7 @@ class _RuntimeAccess:
|
|
|
78
96
|
class StepEnvironment:
|
|
79
97
|
"""Read-only environment views with a separate, tracked working namespace.
|
|
80
98
|
|
|
81
|
-
|
|
99
|
+
Python working associations and scoped task references are captured at entry.
|
|
82
100
|
Application values remain live; the views do not freeze their object graphs.
|
|
83
101
|
"""
|
|
84
102
|
|
|
@@ -88,28 +106,29 @@ class StepEnvironment:
|
|
|
88
106
|
python_name_to_value: Mapping[str, object],
|
|
89
107
|
python_globals: dict[str, object],
|
|
90
108
|
local_name_set: frozenset[str] = frozenset(),
|
|
91
|
-
python_name_to_cell: Mapping[str, CellType]
|
|
92
|
-
|
|
93
|
-
task_name_to_value: Mapping[str, object] | None = None,
|
|
109
|
+
python_name_to_cell: Mapping[str, CellType] = _EMPTY_NAME_TO_VALUE,
|
|
110
|
+
task_name_to_value: Mapping[str, object] = _EMPTY_NAME_TO_VALUE,
|
|
94
111
|
output_name_set: frozenset[str] = frozenset(),
|
|
95
|
-
binding_name_to_annotation: Mapping[str, object]
|
|
96
|
-
binding_name_to_type: Mapping[str, object]
|
|
112
|
+
binding_name_to_annotation: Mapping[str, object] = _EMPTY_NAME_TO_VALUE,
|
|
113
|
+
binding_name_to_type: Mapping[str, object] = _EMPTY_NAME_TO_VALUE,
|
|
97
114
|
runtime_name_set: frozenset[str] = frozenset(),
|
|
98
115
|
) -> None:
|
|
116
|
+
python_name_to_value = _copy_name_to_value(python_name_to_value, "python_name_to_value")
|
|
117
|
+
if not isinstance(python_globals, dict) or any(not isinstance(name, str) for name in python_globals):
|
|
118
|
+
raise TypeError("python_globals must be a string-keyed dict")
|
|
99
119
|
self._runtime_name_set = frozenset(runtime_name_set)
|
|
100
120
|
self._python_name_to_value = {name: value for name, value in python_name_to_value.items() if not self._is_runtime_name(name)}
|
|
101
121
|
self._python_global_name_to_value = python_globals
|
|
102
122
|
builtins_value = python_globals.get("__builtins__", __builtins__)
|
|
103
123
|
self._python_builtin_name_to_value: dict[str, object] = builtins_value if isinstance(builtins_value, dict) else vars(builtins_value)
|
|
104
124
|
self._local_name_set = local_name_set
|
|
105
|
-
self._python_name_to_cell =
|
|
125
|
+
self._python_name_to_cell = _copy_name_to_value(python_name_to_cell, "python_name_to_cell")
|
|
106
126
|
self._output_name_set = output_name_set
|
|
107
127
|
self._step_name_to_value: dict[str, object] = {}
|
|
108
|
-
self._binding_name_to_annotation =
|
|
109
|
-
self._binding_name_to_type =
|
|
128
|
+
self._binding_name_to_annotation = _copy_name_to_value(binding_name_to_annotation, "binding_name_to_annotation")
|
|
129
|
+
self._binding_name_to_type = _copy_name_to_value(binding_name_to_type, "binding_name_to_type")
|
|
110
130
|
self.python_name_to_value: Mapping[str, object] = _PythonEnvironmentView(self)
|
|
111
|
-
self.
|
|
112
|
-
self.task_name_to_value = MappingProxyType(dict(task_name_to_value or {}))
|
|
131
|
+
self.task_name_to_value = MappingProxyType(_copy_name_to_value(task_name_to_value, "task_name_to_value"))
|
|
113
132
|
self.step_name_to_value = MappingProxyType(self._step_name_to_value)
|
|
114
133
|
known_names = (
|
|
115
134
|
python_name_to_value.keys()
|
|
@@ -117,7 +136,6 @@ class StepEnvironment:
|
|
|
117
136
|
| self._python_builtin_name_to_value.keys()
|
|
118
137
|
| self._local_name_set
|
|
119
138
|
| self._python_name_to_cell.keys()
|
|
120
|
-
| self.parent_name_to_value.keys()
|
|
121
139
|
| self.task_name_to_value.keys()
|
|
122
140
|
| self._runtime_name_set
|
|
123
141
|
)
|
|
@@ -166,7 +184,6 @@ class StepEnvironment:
|
|
|
166
184
|
source_to_name_to_value = {
|
|
167
185
|
"step": self.step_name_to_value,
|
|
168
186
|
"python": self.python_name_to_value,
|
|
169
|
-
"parent": self.parent_name_to_value,
|
|
170
187
|
"task": self.task_name_to_value,
|
|
171
188
|
}
|
|
172
189
|
if source != "effective" and source not in source_to_name_to_value:
|
|
@@ -188,19 +205,25 @@ class StepEnvironment:
|
|
|
188
205
|
self._step_name_to_value[name] = value
|
|
189
206
|
|
|
190
207
|
def snapshot_working_state(self) -> dict[str, object]:
|
|
191
|
-
|
|
208
|
+
"""Copy this step's bound working names for explicit application-owned sharing.
|
|
209
|
+
|
|
210
|
+
Include Python locals, live cells, and tracked assignments.
|
|
211
|
+
Exclude module globals, builtins, scoped task references, and runtime names.
|
|
212
|
+
Referenced objects retain identity; the returned mapping is independent.
|
|
213
|
+
"""
|
|
214
|
+
name_to_value: dict[str, object] = {}
|
|
192
215
|
for name in self._python_name_to_value.keys() | self._local_name_set | self._python_name_to_cell.keys():
|
|
193
216
|
if self._is_runtime_name(name):
|
|
194
217
|
continue
|
|
195
218
|
try:
|
|
196
|
-
|
|
219
|
+
name_to_value[name] = self._resolve_python(name)
|
|
197
220
|
except (KeyError, UnboundLocalError):
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
return
|
|
221
|
+
continue
|
|
222
|
+
name_to_value.update(self.step_name_to_value)
|
|
223
|
+
return name_to_value
|
|
201
224
|
|
|
202
|
-
def expression_namespace(self, overrides: Mapping[str, object]
|
|
203
|
-
return _ExpressionNamespace(self, overrides
|
|
225
|
+
def expression_namespace(self, overrides: Mapping[str, object] = _EMPTY_NAME_TO_VALUE) -> dict[str, object]:
|
|
226
|
+
return _ExpressionNamespace(self, _copy_name_to_value(overrides, "overrides"))
|
|
204
227
|
|
|
205
228
|
def reference(self, name: str, *, source: EnvironmentSource = "effective") -> str:
|
|
206
229
|
"""Return a Python expression resolving this binding when evaluated."""
|
|
@@ -208,18 +231,17 @@ class StepEnvironment:
|
|
|
208
231
|
|
|
209
232
|
def discover(self, *, source: EnvironmentSource = "effective", cursor: str | None = None, max_items: int = 8) -> dict[str, object]:
|
|
210
233
|
"""List names with provenance, usable references, and progressing pages."""
|
|
211
|
-
|
|
234
|
+
source_to_name_to_value = {
|
|
212
235
|
"python": self.python_name_to_value,
|
|
213
|
-
"parent": self.parent_name_to_value,
|
|
214
236
|
"task": self.task_name_to_value,
|
|
215
237
|
"step": self.step_name_to_value,
|
|
216
238
|
}
|
|
217
|
-
if source != "effective" and source not in
|
|
218
|
-
return {"error": "Unknown source", "recovery": "Choose effective, python,
|
|
239
|
+
if source != "effective" and source not in source_to_name_to_value:
|
|
240
|
+
return {"error": "Unknown source", "recovery": "Choose effective, python, task, or step."}
|
|
219
241
|
names = (
|
|
220
|
-
tuple(sorted(set().union(*[set(view) for view in
|
|
242
|
+
tuple(sorted(set().union(*[set(view) for view in source_to_name_to_value.values()])))
|
|
221
243
|
if source == "effective"
|
|
222
|
-
else tuple(sorted(
|
|
244
|
+
else tuple(sorted(source_to_name_to_value[source]))
|
|
223
245
|
)
|
|
224
246
|
page = page_names(names, source=source, cursor=cursor, max_items=max_items)
|
|
225
247
|
if page.error is not None:
|
|
@@ -244,7 +266,6 @@ class StepEnvironment:
|
|
|
244
266
|
for source, view in (
|
|
245
267
|
("step", self.step_name_to_value),
|
|
246
268
|
("python", self.python_name_to_value),
|
|
247
|
-
("parent", self.parent_name_to_value),
|
|
248
269
|
("task", self.task_name_to_value),
|
|
249
270
|
)
|
|
250
271
|
if name in view
|
nighthawk/runtime/expression.py
CHANGED
|
@@ -38,7 +38,7 @@ async def evaluate_compiled_expression(
|
|
|
38
38
|
allow_awaitable_result: bool = True,
|
|
39
39
|
override_name_to_value: Mapping[str, object] | None = None,
|
|
40
40
|
) -> object:
|
|
41
|
-
namespace = step_context.environment.expression_namespace(override_name_to_value)
|
|
41
|
+
namespace = step_context.environment.expression_namespace({} if override_name_to_value is None else override_name_to_value)
|
|
42
42
|
try:
|
|
43
43
|
value = eval(compiled, namespace, namespace)
|
|
44
44
|
if inspect.isawaitable(value):
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from collections.abc import Mapping
|
|
4
4
|
|
|
5
|
+
from ..composition import UNSET, UnsetType
|
|
5
6
|
from ..errors import ExecutionError
|
|
6
7
|
from .expression import evaluate_expression_operation
|
|
7
8
|
from .operation_contract import OperationError
|
|
@@ -13,14 +14,21 @@ from .step_result import Break, Continue, Pass, Raise, Return, StepResult
|
|
|
13
14
|
async def resolve_model_candidate(
|
|
14
15
|
request: StepRequest,
|
|
15
16
|
outcome: StepOutcome,
|
|
16
|
-
binding_name_to_value: Mapping[str, object],
|
|
17
|
+
binding_name_to_value: Mapping[str, object] | UnsetType = UNSET,
|
|
17
18
|
) -> StepCandidate:
|
|
18
19
|
"""Resolve a provider response once; leave commit validation to the runner."""
|
|
19
20
|
if outcome.kind not in request.allowed_step_kinds:
|
|
20
21
|
raise ExecutionError(f"Step '{outcome.kind}' is not allowed for this step. Allowed kinds: {request.allowed_step_kinds}")
|
|
21
|
-
|
|
22
|
+
if not isinstance(binding_name_to_value, UnsetType) and (
|
|
23
|
+
not isinstance(binding_name_to_value, Mapping) or any(not isinstance(name, str) for name in binding_name_to_value)
|
|
24
|
+
):
|
|
25
|
+
raise TypeError("binding_name_to_value must be a string-keyed mapping or UNSET")
|
|
26
|
+
supplied_name_to_value = {} if isinstance(binding_name_to_value, UnsetType) else dict(binding_name_to_value)
|
|
27
|
+
undeclared = supplied_name_to_value.keys() - set(request.output_binding_names)
|
|
22
28
|
if undeclared:
|
|
23
29
|
raise ExecutionError(f"Undeclared output binding names: {sorted(undeclared)}")
|
|
30
|
+
if isinstance(outcome, RaiseStepOutcome) and supplied_name_to_value:
|
|
31
|
+
raise ExecutionError("Raise cannot include output bindings")
|
|
24
32
|
resolved: StepResult
|
|
25
33
|
if isinstance(outcome, ReturnStepOutcome):
|
|
26
34
|
value = await evaluate_expression_operation(
|
|
@@ -29,7 +37,7 @@ async def resolve_model_candidate(
|
|
|
29
37
|
purpose="return",
|
|
30
38
|
allow_awaitable_result=request.allow_awaitable_return,
|
|
31
39
|
expected_type=request.return_annotation,
|
|
32
|
-
override_name_to_value=
|
|
40
|
+
override_name_to_value=supplied_name_to_value,
|
|
33
41
|
)
|
|
34
42
|
resolved = Return(value)
|
|
35
43
|
elif isinstance(outcome, RaiseStepOutcome):
|
|
@@ -46,4 +54,4 @@ async def resolve_model_candidate(
|
|
|
46
54
|
resolved = Raise(outcome.raise_message, error_type)
|
|
47
55
|
else:
|
|
48
56
|
resolved = {"pass": Pass, "break": Break, "continue": Continue}[outcome.kind]()
|
|
49
|
-
return StepCandidate(outcome=resolved, binding_name_to_value=binding_name_to_value)
|
|
57
|
+
return StepCandidate(outcome=resolved, binding_name_to_value=UNSET if isinstance(binding_name_to_value, UnsetType) else supplied_name_to_value)
|
nighthawk/runtime/preparation.py
CHANGED
|
@@ -13,7 +13,7 @@ from .bindings import MISSING_ANNOTATION, infer_binding_types, normalize_binding
|
|
|
13
13
|
from .description import discover_type_alias_reference_names
|
|
14
14
|
from .environment import StepEnvironment
|
|
15
15
|
from .scoping import _current_task_references, get_execution_reference
|
|
16
|
-
from .step_context import StepContext, ToolResultRenderingPolicy, get_python_cell_scope_stack
|
|
16
|
+
from .step_context import StepContext, ToolResultRenderingPolicy, get_python_cell_scope_stack
|
|
17
17
|
from .step_contract import StepKind
|
|
18
18
|
from .step_executor import StepExecutor
|
|
19
19
|
|
|
@@ -85,8 +85,6 @@ class StepPreparation:
|
|
|
85
85
|
python_locals = caller_frame.f_locals
|
|
86
86
|
python_globals = caller_frame.f_globals
|
|
87
87
|
step_locals = {name: value for name, value in python_locals.items() if name not in self.runtime_name_set}
|
|
88
|
-
parent_stack = get_step_context_stack()
|
|
89
|
-
parent_name_to_value = parent_stack[-1].environment.snapshot_working_state() if parent_stack else {}
|
|
90
88
|
cell_scope_stack = get_python_cell_scope_stack()
|
|
91
89
|
python_name_to_cell = cell_scope_stack[-1] if cell_scope_stack else {}
|
|
92
90
|
|
|
@@ -119,7 +117,6 @@ class StepPreparation:
|
|
|
119
117
|
python_globals=python_globals,
|
|
120
118
|
local_name_set=frozenset(local_name_set),
|
|
121
119
|
python_name_to_cell=python_name_to_cell,
|
|
122
|
-
parent_name_to_value=parent_name_to_value,
|
|
123
120
|
task_name_to_value=scoped_task_name_to_value,
|
|
124
121
|
output_name_set=frozenset(output_binding_names),
|
|
125
122
|
binding_name_to_annotation=binding_name_to_annotation,
|
nighthawk/runtime/prompt.py
CHANGED
|
@@ -452,12 +452,7 @@ def _collect_explicit_multimodal_reference_and_value_list(
|
|
|
452
452
|
continue
|
|
453
453
|
|
|
454
454
|
root_reference = reference_path.split(".", 1)[0]
|
|
455
|
-
local_names = (
|
|
456
|
-
step_context.step_locals.keys()
|
|
457
|
-
| step_context.environment.parent_name_to_value.keys()
|
|
458
|
-
| step_context.environment.step_name_to_value.keys()
|
|
459
|
-
| step_context.binding_commit_targets
|
|
460
|
-
)
|
|
455
|
+
local_names = step_context.step_locals.keys() | step_context.environment.step_name_to_value.keys() | step_context.binding_commit_targets
|
|
461
456
|
if (root_reference in local_names) != include_locals:
|
|
462
457
|
continue
|
|
463
458
|
|
|
@@ -553,7 +548,6 @@ def _render_globals_section(
|
|
|
553
548
|
for reference in sorted(references):
|
|
554
549
|
if (
|
|
555
550
|
reference in step_context.step_locals
|
|
556
|
-
or reference in step_context.environment.parent_name_to_value
|
|
557
551
|
or reference in step_context.environment.step_name_to_value
|
|
558
552
|
or reference in step_context.binding_commit_targets
|
|
559
553
|
):
|
|
@@ -695,10 +689,7 @@ def _build_user_prompt_content(
|
|
|
695
689
|
|
|
696
690
|
locals_content = _render_locals_section(
|
|
697
691
|
step_context=step_context,
|
|
698
|
-
references=step_context.step_locals.keys()
|
|
699
|
-
| step_context.environment.parent_name_to_value.keys()
|
|
700
|
-
| step_context.environment.step_name_to_value.keys()
|
|
701
|
-
| step_context.binding_commit_targets,
|
|
692
|
+
references=step_context.step_locals.keys() | step_context.environment.step_name_to_value.keys() | step_context.binding_commit_targets,
|
|
702
693
|
explicit_reference_paths=explicit_reference_paths,
|
|
703
694
|
render_context=render_context,
|
|
704
695
|
)
|
|
@@ -728,12 +719,7 @@ def _build_user_prompt_content(
|
|
|
728
719
|
"Displayed annotations are best-effort metadata and may be unresolved source spelling; they are not evaluated for presentation.",
|
|
729
720
|
]
|
|
730
721
|
relevant_names = (
|
|
731
|
-
set(references)
|
|
732
|
-
| step_context.binding_commit_targets
|
|
733
|
-
| set(step_context.step_locals)
|
|
734
|
-
| set(environment.parent_name_to_value)
|
|
735
|
-
| set(environment.task_name_to_value)
|
|
736
|
-
| {"inspect"}
|
|
722
|
+
set(references) | step_context.binding_commit_targets | set(step_context.step_locals) | set(environment.task_name_to_value) | {"inspect"}
|
|
737
723
|
)
|
|
738
724
|
collision_names = [
|
|
739
725
|
name
|
nighthawk/runtime/runner.py
CHANGED
|
@@ -24,7 +24,6 @@ from ..oversight import (
|
|
|
24
24
|
)
|
|
25
25
|
from .async_bridge import run_coroutine_synchronously
|
|
26
26
|
from .bindings import BindingValidationError, require_declared_binding_names, validate_output_bindings
|
|
27
|
-
from .environment import UnboundEnvironmentNameError
|
|
28
27
|
from .execution_reference import ExecutionReference
|
|
29
28
|
from .interruption import chain_cleanup_errors
|
|
30
29
|
from .notifications import await_notification
|
|
@@ -45,7 +44,7 @@ from .step_context import (
|
|
|
45
44
|
)
|
|
46
45
|
from .step_contract import StepKind
|
|
47
46
|
from .step_executor import AsyncStepExecutor, StepExecutor, SyncStepExecutor
|
|
48
|
-
from .step_request import StepCandidate, StepRequest
|
|
47
|
+
from .step_request import StepCandidate, StepRequest, _snapshot_output_bindings
|
|
49
48
|
from .step_result import Break, Continue, Pass, Raise, Return, StepResult
|
|
50
49
|
|
|
51
50
|
|
|
@@ -301,22 +300,19 @@ class Runner:
|
|
|
301
300
|
*,
|
|
302
301
|
preparation: PreparedStep,
|
|
303
302
|
step_outcome: StepResult,
|
|
304
|
-
bindings:
|
|
303
|
+
bindings: Mapping[str, object] | UnsetType,
|
|
305
304
|
) -> dict[str, object]:
|
|
306
|
-
|
|
305
|
+
if not isinstance(bindings, UnsetType):
|
|
306
|
+
require_declared_binding_names(bindings, preparation.output_binding_names)
|
|
307
307
|
if step_outcome.kind == "raise":
|
|
308
|
+
if not isinstance(bindings, UnsetType) and len(bindings):
|
|
309
|
+
raise NighthawkError("StepCandidate Raise cannot include output bindings")
|
|
308
310
|
return {}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
continue
|
|
315
|
-
try:
|
|
316
|
-
selected_binding_name_to_value[name] = environment.python_name_to_value[name]
|
|
317
|
-
except (KeyError, UnboundEnvironmentNameError):
|
|
318
|
-
continue
|
|
319
|
-
selected_binding_name_to_value.update(bindings)
|
|
311
|
+
selected_binding_name_to_value = (
|
|
312
|
+
_snapshot_output_bindings(preparation.step_context.environment, preparation.output_binding_names)
|
|
313
|
+
if isinstance(bindings, UnsetType)
|
|
314
|
+
else dict(bindings)
|
|
315
|
+
)
|
|
320
316
|
return self._validate_and_coerce_output_bindings(preparation=preparation, bindings=selected_binding_name_to_value)
|
|
321
317
|
|
|
322
318
|
async def _apply_step_oversight_if_needed(
|
|
@@ -403,14 +399,13 @@ class Runner:
|
|
|
403
399
|
if not isinstance(candidate, StepCandidate):
|
|
404
400
|
raise ExecutionError("Step executor must return StepCandidate")
|
|
405
401
|
step_outcome = candidate.outcome
|
|
406
|
-
bindings = dict(candidate.binding_name_to_value)
|
|
407
402
|
self.attempted_outcome = step_outcome
|
|
408
403
|
self._require_allowed_step_kind(step_outcome=step_outcome, allowed_step_kinds=preparation.allowed_step_kinds)
|
|
409
404
|
self.failure_stage = "binding_validation"
|
|
410
405
|
validated_bindings = self._validate_bindings_for_outcome(
|
|
411
406
|
preparation=preparation,
|
|
412
407
|
step_outcome=step_outcome,
|
|
413
|
-
bindings=
|
|
408
|
+
bindings=candidate.binding_name_to_value,
|
|
414
409
|
)
|
|
415
410
|
self.validated_binding_name_to_value = validated_bindings
|
|
416
411
|
outcome = step_outcome
|
nighthawk/runtime/scoping.py
CHANGED
|
@@ -334,7 +334,7 @@ def _validate_step_executor(step_executor: object) -> None:
|
|
|
334
334
|
def run(
|
|
335
335
|
step_executor: StepExecutor,
|
|
336
336
|
*,
|
|
337
|
-
run_id: str |
|
|
337
|
+
run_id: str | UnsetType = UNSET,
|
|
338
338
|
usage_meter: UsageMeter | UnsetType = UNSET,
|
|
339
339
|
) -> Iterator[None]:
|
|
340
340
|
"""Start an execution run with the given step executor.
|
|
@@ -344,8 +344,8 @@ def run(
|
|
|
344
344
|
|
|
345
345
|
Args:
|
|
346
346
|
step_executor: The step executor to use for Natural block execution.
|
|
347
|
-
run_id:
|
|
348
|
-
|
|
347
|
+
run_id: Nonempty identifier for the run. Omission or UNSET generates
|
|
348
|
+
a fresh ULID. None and empty strings are invalid.
|
|
349
349
|
usage_meter: Optional meter that accumulates LLM usage for the run.
|
|
350
350
|
If not provided, a fresh :class:`UsageMeter` is created.
|
|
351
351
|
|
|
@@ -356,8 +356,12 @@ def run(
|
|
|
356
356
|
TypeError: If the executor does not implement either execution protocol or the usage meter is invalid.
|
|
357
357
|
"""
|
|
358
358
|
_validate_step_executor(step_executor)
|
|
359
|
+
if not isinstance(run_id, (str, UnsetType)):
|
|
360
|
+
raise TypeError("run_id must be a nonempty string or UNSET")
|
|
361
|
+
if isinstance(run_id, str) and not run_id:
|
|
362
|
+
raise ValueError("run_id must not be empty")
|
|
359
363
|
execution_reference = ExecutionReference(
|
|
360
|
-
run_id=
|
|
364
|
+
run_id=generate_ulid() if isinstance(run_id, UnsetType) else run_id,
|
|
361
365
|
scope_id=generate_ulid(),
|
|
362
366
|
step_execution_id=None,
|
|
363
367
|
source_location=None,
|
|
@@ -434,6 +438,7 @@ def scope(
|
|
|
434
438
|
installing context. Executor replacement precedes delegated model selection.
|
|
435
439
|
model replaces only the model; step_executor_configuration replaces all configuration
|
|
436
440
|
fields. Supplying both is invalid. Selection preserves unrelated scoped controls.
|
|
441
|
+
A new scope retains the active step identity and its source location.
|
|
437
442
|
"""
|
|
438
443
|
from ..lifecycle import StepLifecycle
|
|
439
444
|
from ..oversight import Oversight
|
|
@@ -455,9 +460,7 @@ def scope(
|
|
|
455
460
|
raise TypeError("lifecycle must be StepLifecycle, None, or UNSET")
|
|
456
461
|
next_step_executor = current_step_executor if isinstance(step_executor, UnsetType) else step_executor
|
|
457
462
|
selection = _model_selection(model, step_executor_configuration)
|
|
458
|
-
|
|
459
|
-
next_step_executor = _resolve_model_selection(next_step_executor, selection)
|
|
460
|
-
next_execution_reference = replace(current_execution_reference, scope_id=generate_ulid(), step_execution_id=None, source_location=None)
|
|
463
|
+
next_execution_reference = replace(current_execution_reference, scope_id=generate_ulid())
|
|
461
464
|
next_usage_meter = get_usage_meter() if isinstance(usage_meter, UnsetType) else usage_meter
|
|
462
465
|
next_oversight = _oversight_var.get() if isinstance(oversight, UnsetType) else oversight
|
|
463
466
|
next_system_prompt_suffix_fragments = _compose_sequence(_system_prompt_suffix_fragments_var.get(), system_prompt_suffix_fragments)
|
|
@@ -481,6 +484,9 @@ def scope(
|
|
|
481
484
|
entries = _compose_sequence((), tools)
|
|
482
485
|
next_tools = resolve_scoped_tools((), entries)
|
|
483
486
|
|
|
487
|
+
if selection is not None:
|
|
488
|
+
next_step_executor = _resolve_model_selection(next_step_executor, selection)
|
|
489
|
+
|
|
484
490
|
step_executor_token = _step_executor_var.set(next_step_executor)
|
|
485
491
|
execution_reference_token = _execution_reference_var.set(next_execution_reference)
|
|
486
492
|
usage_meter_token = _usage_meter_var.set(next_usage_meter)
|
|
@@ -46,7 +46,8 @@ class StepContext:
|
|
|
46
46
|
|
|
47
47
|
``environment`` owns effective lookup and tracked step assignments.
|
|
48
48
|
Use :meth:`record_assignment` or the common assignment operations to keep that environment and ``step_locals`` bookkeeping consistent.
|
|
49
|
-
Revision and mutation marks describe tracked changes
|
|
49
|
+
Revision and mutation marks describe tracked changes.
|
|
50
|
+
Finalization validates initialized declared outputs when the candidate omits its output selection; an explicit mapping supplies the complete selection.
|
|
50
51
|
"""
|
|
51
52
|
|
|
52
53
|
execution_reference: ExecutionReference
|
|
@@ -139,10 +140,6 @@ def python_cell_scope(name_to_cell: dict[str, CellType]) -> Iterator[None]:
|
|
|
139
140
|
_python_lookup_state_var.reset(token)
|
|
140
141
|
|
|
141
142
|
|
|
142
|
-
def get_step_context_stack() -> tuple[StepContext, ...]:
|
|
143
|
-
return _step_context_stack_var.get()
|
|
144
|
-
|
|
145
|
-
|
|
146
143
|
def get_python_cell_scope_stack() -> tuple[dict[str, CellType], ...]:
|
|
147
144
|
return _python_lookup_state_var.get().python_cell_scope_stack
|
|
148
145
|
|
|
@@ -7,8 +7,10 @@ from pydantic_ai import Agent, RunContext, StructuredDict
|
|
|
7
7
|
from pydantic_ai.capabilities import AbstractCapability
|
|
8
8
|
from pydantic_ai.messages import UserContent
|
|
9
9
|
from pydantic_ai.models import ModelRequestContext
|
|
10
|
+
from pydantic_ai.settings import ModelSettings
|
|
10
11
|
from pydantic_ai.toolsets.function import FunctionToolset
|
|
11
12
|
|
|
13
|
+
from ..composition import UNSET, UnsetType
|
|
12
14
|
from ..configuration import ModelSelection, StepExecutorConfiguration
|
|
13
15
|
from ..errors import ExecutionError, NighthawkError
|
|
14
16
|
from ..tools._admission import ToolAdmissionCapability
|
|
@@ -125,10 +127,6 @@ def _new_agent_step_executor(
|
|
|
125
127
|
case _:
|
|
126
128
|
model = model_identifier
|
|
127
129
|
|
|
128
|
-
constructor_arguments: dict[str, Any] = {}
|
|
129
|
-
if configuration.model_settings is not None:
|
|
130
|
-
constructor_arguments["model_settings"] = configuration.model_settings
|
|
131
|
-
|
|
132
130
|
system_prompt_text = build_system_prompt(configuration=configuration)
|
|
133
131
|
|
|
134
132
|
agent = Agent(
|
|
@@ -136,7 +134,7 @@ def _new_agent_step_executor(
|
|
|
136
134
|
output_type=StepFinalResult,
|
|
137
135
|
deps_type=StepContext,
|
|
138
136
|
system_prompt=system_prompt_text,
|
|
139
|
-
|
|
137
|
+
model_settings=cast(ModelSettings, configuration.model_settings),
|
|
140
138
|
)
|
|
141
139
|
|
|
142
140
|
@agent.system_prompt(dynamic=True)
|
|
@@ -173,10 +171,14 @@ class AgentStepExecutor:
|
|
|
173
171
|
|
|
174
172
|
def __init__(
|
|
175
173
|
self,
|
|
176
|
-
configuration: StepExecutorConfiguration |
|
|
174
|
+
configuration: StepExecutorConfiguration | UnsetType = UNSET,
|
|
177
175
|
agent: StepExecutionAgent | None = None,
|
|
178
176
|
) -> None:
|
|
179
|
-
|
|
177
|
+
if not isinstance(configuration, (StepExecutorConfiguration, UnsetType)):
|
|
178
|
+
raise TypeError("configuration must be StepExecutorConfiguration or UNSET")
|
|
179
|
+
if agent is not None and not isinstance(agent, (AsyncExecutionAgent, SyncExecutionAgent)):
|
|
180
|
+
raise TypeError("agent must implement the synchronous or asynchronous agent protocol")
|
|
181
|
+
self.configuration = StepExecutorConfiguration() if isinstance(configuration, UnsetType) else configuration
|
|
180
182
|
if agent is not None and not isinstance(self.configuration.model, str):
|
|
181
183
|
raise NighthawkError("The external agent owns model selection; configuration.model must be a string")
|
|
182
184
|
self.agent_is_managed = agent is None
|
|
@@ -193,15 +195,17 @@ class AgentStepExecutor:
|
|
|
193
195
|
cls,
|
|
194
196
|
*,
|
|
195
197
|
agent: StepExecutionAgent,
|
|
196
|
-
configuration: StepExecutorConfiguration |
|
|
198
|
+
configuration: StepExecutorConfiguration | UnsetType = UNSET,
|
|
197
199
|
) -> AgentStepExecutor:
|
|
198
200
|
"""Create an executor wrapping an existing agent.
|
|
199
201
|
|
|
200
202
|
Args:
|
|
201
203
|
agent: A pre-configured agent to use for step execution.
|
|
202
|
-
configuration:
|
|
203
|
-
|
|
204
|
+
configuration: Omission or UNSET creates StepExecutorConfiguration().
|
|
205
|
+
None and raw dictionaries are invalid; parse configuration data explicitly.
|
|
204
206
|
"""
|
|
207
|
+
if not isinstance(agent, (AsyncExecutionAgent, SyncExecutionAgent)):
|
|
208
|
+
raise TypeError("agent must implement the synchronous or asynchronous agent protocol")
|
|
205
209
|
return cls(configuration=configuration, agent=agent)
|
|
206
210
|
|
|
207
211
|
@classmethod
|
|
@@ -211,6 +215,8 @@ class AgentStepExecutor:
|
|
|
211
215
|
configuration: StepExecutorConfiguration,
|
|
212
216
|
) -> AgentStepExecutor:
|
|
213
217
|
"""Create an executor from a configuration, building a managed agent internally."""
|
|
218
|
+
if not isinstance(configuration, StepExecutorConfiguration):
|
|
219
|
+
raise TypeError("configuration must be StepExecutorConfiguration")
|
|
214
220
|
return cls(configuration=configuration)
|
|
215
221
|
|
|
216
222
|
def resolve_model_selection(self, selection: ModelSelection) -> StepExecutor:
|
|
@@ -374,7 +380,7 @@ class AgentStepExecutor:
|
|
|
374
380
|
usage_meter.record(result.usage, kind="step")
|
|
375
381
|
|
|
376
382
|
step_outcome = self._parse_agent_result(result)
|
|
377
|
-
return await resolve_model_candidate(request, step_outcome
|
|
383
|
+
return await resolve_model_candidate(request, step_outcome)
|
|
378
384
|
|
|
379
385
|
def run_step(
|
|
380
386
|
self,
|
|
@@ -1,14 +1,29 @@
|
|
|
1
1
|
"""Frozen executor contracts containing live application values."""
|
|
2
2
|
|
|
3
|
-
from collections.abc import Mapping
|
|
3
|
+
from collections.abc import Mapping, Sequence
|
|
4
4
|
from types import MappingProxyType
|
|
5
5
|
|
|
6
|
-
from pydantic import BaseModel, ConfigDict,
|
|
6
|
+
from pydantic import BaseModel, ConfigDict, InstanceOf, field_validator, model_validator
|
|
7
7
|
|
|
8
|
+
from ..composition import UNSET, UnsetType
|
|
9
|
+
from .environment import StepEnvironment, UnboundEnvironmentNameError
|
|
8
10
|
from .execution_reference import ExecutionReference
|
|
9
11
|
from .step_context import StepContext
|
|
10
12
|
from .step_contract import StepKind
|
|
11
|
-
from .step_result import StepResult
|
|
13
|
+
from .step_result import Raise, StepResult
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _snapshot_output_bindings(environment: StepEnvironment, output_binding_names: Sequence[str]) -> dict[str, object]:
|
|
17
|
+
binding_name_to_value: dict[str, object] = {}
|
|
18
|
+
for name in output_binding_names:
|
|
19
|
+
if name in environment.step_name_to_value:
|
|
20
|
+
binding_name_to_value[name] = environment.step_name_to_value[name]
|
|
21
|
+
continue
|
|
22
|
+
try:
|
|
23
|
+
binding_name_to_value[name] = environment.python_name_to_value[name]
|
|
24
|
+
except (KeyError, UnboundEnvironmentNameError):
|
|
25
|
+
continue
|
|
26
|
+
return binding_name_to_value
|
|
12
27
|
|
|
13
28
|
|
|
14
29
|
class StepRequest(BaseModel):
|
|
@@ -29,6 +44,17 @@ class StepRequest(BaseModel):
|
|
|
29
44
|
allowed_step_kinds: tuple[StepKind, ...]
|
|
30
45
|
allow_awaitable_return: bool = False
|
|
31
46
|
|
|
47
|
+
def snapshot_output_bindings(self) -> dict[str, object]:
|
|
48
|
+
"""Snapshot initialized declared outputs in declaration order.
|
|
49
|
+
|
|
50
|
+
Tracked assignments precede current Python values, including live globals and cells.
|
|
51
|
+
Unbound names and task-only references are excluded.
|
|
52
|
+
The dictionary is independent; values retain identity.
|
|
53
|
+
This neither validates nor writes values.
|
|
54
|
+
Later working changes do not change its name associations.
|
|
55
|
+
"""
|
|
56
|
+
return _snapshot_output_bindings(self.step_context.environment, self.output_binding_names)
|
|
57
|
+
|
|
32
58
|
@field_validator("binding_name_to_type", mode="after")
|
|
33
59
|
@classmethod
|
|
34
60
|
def freeze_contracts(cls, value: Mapping[str, object]) -> Mapping[str, object]:
|
|
@@ -48,14 +74,26 @@ class StepRequest(BaseModel):
|
|
|
48
74
|
|
|
49
75
|
|
|
50
76
|
class StepCandidate(BaseModel):
|
|
51
|
-
"""Resolved control flow and
|
|
77
|
+
"""Resolved control flow and output selection, pending runtime validation.
|
|
78
|
+
|
|
79
|
+
Omission or UNSET selects initialized declared outputs at finalization.
|
|
80
|
+
A mapping supplies the entire output selection; {} selects no writes.
|
|
81
|
+
Explicit selections receive no supplementation from the working environment.
|
|
82
|
+
Raise permits only omitted or empty outputs.
|
|
83
|
+
"""
|
|
52
84
|
|
|
53
85
|
model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True, extra="forbid", validate_default=True)
|
|
54
86
|
|
|
55
87
|
outcome: StepResult
|
|
56
|
-
binding_name_to_value: Mapping[str, object] =
|
|
88
|
+
binding_name_to_value: Mapping[str, object] | UnsetType = UNSET
|
|
57
89
|
|
|
58
90
|
@field_validator("binding_name_to_value", mode="after")
|
|
59
91
|
@classmethod
|
|
60
|
-
def preserve_live_values(cls, value: Mapping[str, object]) -> Mapping[str, object]:
|
|
61
|
-
return MappingProxyType(dict(value))
|
|
92
|
+
def preserve_live_values(cls, value: Mapping[str, object] | UnsetType) -> Mapping[str, object] | UnsetType:
|
|
93
|
+
return value if isinstance(value, UnsetType) else MappingProxyType(dict(value))
|
|
94
|
+
|
|
95
|
+
@model_validator(mode="after")
|
|
96
|
+
def validate_output_selection(self) -> "StepCandidate":
|
|
97
|
+
if isinstance(self.outcome, Raise) and not isinstance(self.binding_name_to_value, UnsetType) and len(self.binding_name_to_value):
|
|
98
|
+
raise ValueError("StepCandidate Raise cannot include output bindings")
|
|
99
|
+
return self
|
nighthawk/testing.py
CHANGED
|
@@ -6,9 +6,12 @@ deterministic tests of Natural functions without LLM API calls.
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
-
from collections.abc import Callable
|
|
10
|
-
from dataclasses import dataclass
|
|
9
|
+
from collections.abc import Callable, Mapping
|
|
10
|
+
from dataclasses import dataclass
|
|
11
11
|
|
|
12
|
+
from pydantic import Field
|
|
13
|
+
|
|
14
|
+
from .composition import UNSET, UnsetType
|
|
12
15
|
from .runtime.execution_reference import ExecutionReference
|
|
13
16
|
from .runtime.step_context import StepContext
|
|
14
17
|
from .runtime.step_contract import StepKind
|
|
@@ -42,20 +45,19 @@ class StepCall:
|
|
|
42
45
|
step_locals: dict[str, object]
|
|
43
46
|
step_globals: dict[str, object]
|
|
44
47
|
task_name_to_value: dict[str, object]
|
|
45
|
-
parent_name_to_value: dict[str, object]
|
|
46
48
|
|
|
47
49
|
|
|
48
|
-
|
|
49
|
-
class StepResponse:
|
|
50
|
+
class StepResponse(StepCandidate):
|
|
50
51
|
"""Scripted response for a single Natural block execution.
|
|
51
52
|
|
|
52
53
|
Attributes:
|
|
53
|
-
|
|
54
|
+
binding_name_to_value: Complete output selection.
|
|
55
|
+
Omission or UNSET selects initialized declared outputs; {} selects no writes.
|
|
56
|
+
Undeclared names fail finalization.
|
|
54
57
|
outcome: Resolved Python control result. Strings are values, never expressions.
|
|
55
58
|
"""
|
|
56
59
|
|
|
57
|
-
|
|
58
|
-
outcome: StepResult = field(default_factory=Pass)
|
|
60
|
+
outcome: StepResult = Field(default_factory=Pass)
|
|
59
61
|
|
|
60
62
|
|
|
61
63
|
def _build_step_call(
|
|
@@ -67,7 +69,7 @@ def _build_step_call(
|
|
|
67
69
|
referenced_global_names = (
|
|
68
70
|
step_context.read_binding_names | (set(step_context.environment.task_name_to_value) | step_context.type_alias_reference_names)
|
|
69
71
|
) - step_context.step_locals.keys()
|
|
70
|
-
|
|
72
|
+
global_name_to_value = {name: step_context.step_globals[name] for name in referenced_global_names if name in step_context.step_globals}
|
|
71
73
|
return StepCall(
|
|
72
74
|
execution_reference=step_context.execution_reference,
|
|
73
75
|
natural_program=processed_natural_program,
|
|
@@ -75,9 +77,8 @@ def _build_step_call(
|
|
|
75
77
|
binding_name_to_type=dict(step_context.binding_name_to_type),
|
|
76
78
|
allowed_step_kinds=allowed_step_kinds,
|
|
77
79
|
step_locals=dict(step_context.step_locals),
|
|
78
|
-
step_globals=
|
|
80
|
+
step_globals=global_name_to_value,
|
|
79
81
|
task_name_to_value=dict(step_context.environment.task_name_to_value),
|
|
80
|
-
parent_name_to_value=dict(step_context.environment.parent_name_to_value),
|
|
81
82
|
)
|
|
82
83
|
|
|
83
84
|
|
|
@@ -117,7 +118,7 @@ class ScriptedExecutor:
|
|
|
117
118
|
f"{len(self.responses)} responses were supplied. "
|
|
118
119
|
"Supply another response or an explicit default_response."
|
|
119
120
|
)
|
|
120
|
-
return
|
|
121
|
+
return response
|
|
121
122
|
|
|
122
123
|
async def run_step_async(
|
|
123
124
|
self,
|
|
@@ -148,7 +149,7 @@ class CallbackExecutor:
|
|
|
148
149
|
)
|
|
149
150
|
self.calls.append(call)
|
|
150
151
|
response = self.handler(call)
|
|
151
|
-
return
|
|
152
|
+
return response
|
|
152
153
|
|
|
153
154
|
async def run_step_async(
|
|
154
155
|
self,
|
|
@@ -158,9 +159,9 @@ class CallbackExecutor:
|
|
|
158
159
|
return self.run_step(request=request)
|
|
159
160
|
|
|
160
161
|
|
|
161
|
-
def pass_response(
|
|
162
|
-
"""
|
|
163
|
-
return StepResponse(
|
|
162
|
+
def pass_response(*, binding_name_to_value: Mapping[str, object] | UnsetType = UNSET) -> StepResponse:
|
|
163
|
+
"""Continue with automatic output selection, or an explicit complete mapping."""
|
|
164
|
+
return StepResponse(binding_name_to_value=binding_name_to_value)
|
|
164
165
|
|
|
165
166
|
|
|
166
167
|
def raise_response(message: str, *, error_type: type[BaseException] = Exception) -> StepResponse:
|
|
@@ -176,21 +177,22 @@ def raise_response(message: str, *, error_type: type[BaseException] = Exception)
|
|
|
176
177
|
return StepResponse(outcome=Raise(message, error_type))
|
|
177
178
|
|
|
178
179
|
|
|
179
|
-
def return_response(value: object, /,
|
|
180
|
+
def return_response(value: object, /, *, binding_name_to_value: Mapping[str, object] | UnsetType = UNSET) -> StepResponse:
|
|
180
181
|
"""Return a Python value subject to the Natural function's return contract.
|
|
181
182
|
|
|
182
183
|
Args:
|
|
183
184
|
value: Resolved value; strings are returned literally without evaluation.
|
|
184
|
-
|
|
185
|
+
binding_name_to_value: Complete output selection; omission or UNSET selects initialized declared outputs.
|
|
186
|
+
{} selects no writes.
|
|
185
187
|
"""
|
|
186
|
-
return StepResponse(
|
|
188
|
+
return StepResponse(binding_name_to_value=binding_name_to_value, outcome=Return(value))
|
|
187
189
|
|
|
188
190
|
|
|
189
|
-
def break_response() -> StepResponse:
|
|
191
|
+
def break_response(*, binding_name_to_value: Mapping[str, object] | UnsetType = UNSET) -> StepResponse:
|
|
190
192
|
"""Create a response with break outcome (exit enclosing loop)."""
|
|
191
|
-
return StepResponse(outcome=Break())
|
|
193
|
+
return StepResponse(outcome=Break(), binding_name_to_value=binding_name_to_value)
|
|
192
194
|
|
|
193
195
|
|
|
194
|
-
def continue_response() -> StepResponse:
|
|
196
|
+
def continue_response(*, binding_name_to_value: Mapping[str, object] | UnsetType = UNSET) -> StepResponse:
|
|
195
197
|
"""Create a response with continue outcome (skip to next iteration)."""
|
|
196
|
-
return StepResponse(outcome=Continue())
|
|
198
|
+
return StepResponse(outcome=Continue(), binding_name_to_value=binding_name_to_value)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: nighthawk-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.16.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/
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
nighthawk/__init__.py,sha256=A_3q-S4wUnuy_GzRlI8TSADfy2cQNPfOdh2y6eIvdHg,2390
|
|
2
|
-
nighthawk/composition.py,sha256=
|
|
3
|
-
nighthawk/configuration.py,sha256=
|
|
2
|
+
nighthawk/composition.py,sha256=qd5NgA3p4RiFeTgZEnSruEcVmAejolP3xAgT226m3OA,2070
|
|
3
|
+
nighthawk/configuration.py,sha256=3w4fAihCGub9ubSwlwAtVTIHSLPU4wUu-_ZRpdcbf4w,9127
|
|
4
4
|
nighthawk/errors.py,sha256=U9ogl_0MLA1xXe_bU2RCA1gRWN0ev5E4a3FiBLgpilI,1282
|
|
5
|
-
nighthawk/function_call.py,sha256=
|
|
5
|
+
nighthawk/function_call.py,sha256=o_i86cbsRxC0kMsTXCWvijA7SJmC7oNrcEghTNvpWJE,6745
|
|
6
6
|
nighthawk/identifier_path.py,sha256=QRbDte7j0mubwg8ebs1GlxW7QeA5V_ow29MpVPCg6Ck,626
|
|
7
7
|
nighthawk/json_renderer.py,sha256=QT4-kyKcr_rcga5hg2irKZ4a2OH6XJ8c0_b1zBlPjcE,10501
|
|
8
8
|
nighthawk/lifecycle.py,sha256=pHCTZCMbG7iTVmLOWuwspi4DeM5p2Y5BskONFF_beTY,5513
|
|
9
|
-
nighthawk/oversight.py,sha256=
|
|
10
|
-
nighthawk/testing.py,sha256=
|
|
9
|
+
nighthawk/oversight.py,sha256=xCuJ5kARG0P2KS0eaf4pHqvkUYVgSULNqB-QLXEKxGU,5762
|
|
10
|
+
nighthawk/testing.py,sha256=yJE2ru0igXknjN2CCpLur2XGSCGI-ww_58Y8XK4h4iw,8045
|
|
11
11
|
nighthawk/ulid.py,sha256=CBpGLXDGm2j69Yk15hEq4Z5V8StWOY4714ow4BZn5tc,546
|
|
12
12
|
nighthawk/backends/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
13
|
nighthawk/backends/_text_fallback.py,sha256=h4dt3nU73Uwd6rxn2zLx7trnlxgxKVhNYWc3xZjTMeg,3606
|
|
@@ -45,24 +45,24 @@ nighthawk/runtime/context.py,sha256=fJlrV8-Ty0P5dATYQsW4aPCGErH90JJYly_H2mqZyIM,
|
|
|
45
45
|
nighthawk/runtime/description.py,sha256=tLL_tD6RTUFGBBz64QZo9atwHaWDLd0SpQPnZ2JkUc4,21411
|
|
46
46
|
nighthawk/runtime/discovery.py,sha256=Z0gRU0xVQfxfZ-2ffol3CKwIWg5vWDchOU59ucN9Lpc,5145
|
|
47
47
|
nighthawk/runtime/embedded.py,sha256=59wZE7ooLpnxxRvS1YwqQzcIqOo9DBoWJD1PlFRxWbE,4961
|
|
48
|
-
nighthawk/runtime/environment.py,sha256=
|
|
48
|
+
nighthawk/runtime/environment.py,sha256=hvmAS9N2_cbxxzQcJ4QZJfkhufPrrw1gcJY0ct7rsx8,14038
|
|
49
49
|
nighthawk/runtime/execution_reference.py,sha256=9sMqcpS8kmyNWEPruFnuYMaOqgpDszcfk2Rofqr2Eek,410
|
|
50
|
-
nighthawk/runtime/expression.py,sha256=
|
|
50
|
+
nighthawk/runtime/expression.py,sha256=421HuL53GGEKW1Tuag9VRRcJffmPShpJ0rTRmZYkxWM,4042
|
|
51
51
|
nighthawk/runtime/inspection.py,sha256=06zWKw_u61585qc-Wp1QmvKAekydWoCcmaDEU22Hdbw,3454
|
|
52
52
|
nighthawk/runtime/interruption.py,sha256=9B3hjMbXXtjf1sP2qVAf10dWk0KDS_UttZwqaix1tFU,679
|
|
53
|
-
nighthawk/runtime/model_candidate.py,sha256=
|
|
53
|
+
nighthawk/runtime/model_candidate.py,sha256=X0aJvlk9zSDTwU6KCnYLBnOuiKab-E4IqVEeypvljhs,3122
|
|
54
54
|
nighthawk/runtime/notifications.py,sha256=khLidCGr9lc2aed_GwGiWaUOx6jNQepGqY0_a6flJWo,1449
|
|
55
55
|
nighthawk/runtime/operation_contract.py,sha256=zTZTQXrv6xpUgJxUIGB3xVFDQZ0NglgmdU30aFbOf9Q,2471
|
|
56
56
|
nighthawk/runtime/operations.py,sha256=OQ15NKnSs2NSUgRxFALzS6bRCWPgbzNrjXCOOW6GH7A,6706
|
|
57
|
-
nighthawk/runtime/preparation.py,sha256=
|
|
58
|
-
nighthawk/runtime/prompt.py,sha256=
|
|
59
|
-
nighthawk/runtime/runner.py,sha256=
|
|
60
|
-
nighthawk/runtime/scoping.py,sha256=
|
|
61
|
-
nighthawk/runtime/step_context.py,sha256=
|
|
57
|
+
nighthawk/runtime/preparation.py,sha256=zLrnP53k7PrKv4gTYf_RJDB2dOEbI_KxB7JSqPzG0EM,11396
|
|
58
|
+
nighthawk/runtime/prompt.py,sha256=ihKOoMEWiTKX6SsyXiAvXydPfgnIAQBrBhDsBib2_ak,29750
|
|
59
|
+
nighthawk/runtime/runner.py,sha256=I5A2CnqzxcWAmIQlGO46h5p460RB_ibHxnLktDj0g7c,27097
|
|
60
|
+
nighthawk/runtime/scoping.py,sha256=94l8lW9UWDSAh4e1-lG1GOt3l7vlm_KMfAcCTbGGCM0,21623
|
|
61
|
+
nighthawk/runtime/step_context.py,sha256=DM7mHCiiFhulUAqS6qK7TAZLHfOe-ZqRtZHkEwj22OE,5836
|
|
62
62
|
nighthawk/runtime/step_contract.py,sha256=xIQcpD1PDY_gqrTqSUZ_5yZoMQt1EJOU33fpn-Da_28,6737
|
|
63
63
|
nighthawk/runtime/step_definition.py,sha256=f64IVSAfpbxUpFVdIKBuWW43m2sQRQDvVmFgBrQeKbE,1913
|
|
64
|
-
nighthawk/runtime/step_executor.py,sha256=
|
|
65
|
-
nighthawk/runtime/step_request.py,sha256=
|
|
64
|
+
nighthawk/runtime/step_executor.py,sha256=Q7Teu6mUrForLi1q6Q570ZRkP44KZ-9KoodUUXnJEXs,16089
|
|
65
|
+
nighthawk/runtime/step_request.py,sha256=qs_kJZ_SqvYSD4yTs-5jSoHVrTbVT27YxWUFHeLHLjM,4508
|
|
66
66
|
nighthawk/runtime/step_result.py,sha256=mCdteQgQMMwoYTvhCJLuwcctljw-rjjyoFy2awfiRRg,1678
|
|
67
67
|
nighthawk/runtime/tool_calls.py,sha256=IZQWk2IH8rPF6hjVBzBDn0cpGDkG9k368dDFpmquYOg,3318
|
|
68
68
|
nighthawk/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -74,7 +74,7 @@ nighthawk/tools/contracts.py,sha256=X-YixPwBun0FmjiyPPNCXPW2pJf7CpmWR7jyTVJQvsM,
|
|
|
74
74
|
nighthawk/tools/declarations.py,sha256=goPDT7iVaXez11R_9z8e4czeeA7wevmZPs8sO85hqdA,3986
|
|
75
75
|
nighthawk/tools/execution.py,sha256=IeZs32tUSLJkj7_M94vrWh0Xt1CkJgvoUike9iIKe1I,6720
|
|
76
76
|
nighthawk/tools/provided.py,sha256=H17PoX8ztUmtgrBkB-bEdfDkK5OSULIl_tyMgzEc3g4,1958
|
|
77
|
-
nighthawk_python-0.
|
|
78
|
-
nighthawk_python-0.
|
|
79
|
-
nighthawk_python-0.
|
|
80
|
-
nighthawk_python-0.
|
|
77
|
+
nighthawk_python-0.16.0.dist-info/METADATA,sha256=g2-niQ9VaMFvMMN3Rn0advG6wBR-Oume9kwYaC_UvjI,6147
|
|
78
|
+
nighthawk_python-0.16.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
79
|
+
nighthawk_python-0.16.0.dist-info/licenses/LICENSE,sha256=R8tjrX79o5Pbi_l_aFPPWDbTZkmz4is0CxmzMMs3f0A,1076
|
|
80
|
+
nighthawk_python-0.16.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|