nighthawk-python 0.12.0__py3-none-any.whl → 0.13.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 CHANGED
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from . import oversight, resilience
4
+ from .composition import UNSET, Extend, Merge, UnsetType
4
5
  from .configuration import (
5
6
  StepContextLimits,
6
7
  StepExecutorConfiguration,
@@ -8,31 +9,41 @@ from .configuration import (
8
9
  )
9
10
  from .errors import (
10
11
  ExecutionError,
12
+ NameConflictError,
11
13
  NaturalParseError,
12
14
  NighthawkError,
15
+ ToolDeclarationError,
13
16
  ToolEvaluationError,
14
- ToolRegistrationError,
17
+ ToolNameConflictError,
15
18
  ToolValidationError,
16
19
  )
17
20
  from .json_renderer import JsonableValue, to_jsonable_value
18
- from .natural.decorator import natural_function
21
+ from .natural.decorator import get_transformed_function, natural_function
19
22
  from .runtime.scoping import (
20
23
  ExecutionRef,
21
24
  UsageMeter,
22
- get_current_usage_meter,
25
+ get_capabilities,
23
26
  get_execution_ref,
24
27
  get_implicit_references,
28
+ get_oversight,
25
29
  get_step_executor,
26
30
  get_system_prompt_suffix_fragments,
31
+ get_tools,
32
+ get_usage_meter,
27
33
  get_user_prompt_suffix_fragments,
28
34
  run,
29
35
  scope,
30
36
  )
31
- from .runtime.step_context import StepContext, get_current_step_context
37
+ from .runtime.step_context import StepContext, get_step_context
32
38
  from .runtime.step_executor import AgentStepExecutor, StepExecutor
33
- from .tools.registry import tool
34
39
 
35
40
  __all__ = [
41
+ "UNSET",
42
+ "Extend",
43
+ "Merge",
44
+ "UnsetType",
45
+ "NameConflictError",
46
+ "ToolNameConflictError",
36
47
  "AgentStepExecutor",
37
48
  "ExecutionError",
38
49
  "ExecutionRef",
@@ -45,21 +56,24 @@ __all__ = [
45
56
  "StepExecutorConfiguration",
46
57
  "StepPromptTemplates",
47
58
  "ToolEvaluationError",
48
- "ToolRegistrationError",
59
+ "ToolDeclarationError",
49
60
  "ToolValidationError",
50
61
  "UsageMeter",
51
- "get_current_step_context",
52
- "get_current_usage_meter",
62
+ "get_capabilities",
63
+ "get_step_context",
64
+ "get_usage_meter",
53
65
  "get_execution_ref",
54
66
  "get_implicit_references",
67
+ "get_oversight",
55
68
  "get_step_executor",
69
+ "get_tools",
56
70
  "get_system_prompt_suffix_fragments",
57
71
  "get_user_prompt_suffix_fragments",
58
72
  "oversight",
59
73
  "natural_function",
74
+ "get_transformed_function",
60
75
  "resilience",
61
76
  "run",
62
77
  "scope",
63
78
  "to_jsonable_value",
64
- "tool",
65
79
  ]
@@ -15,7 +15,7 @@ from pydantic_ai.profiles import ModelProfile
15
15
  from pydantic_ai.settings import ModelSettings
16
16
  from pydantic_ai.usage import RequestUsage
17
17
 
18
- from ..tools.registry import get_visible_tools
18
+ from ..tools.declarations import get_visible_tools
19
19
  from .base import BackendModelBase, append_text_projected_tool_result_preview_prompt
20
20
  from .claude_code_settings import ClaudeCodeModelSettings
21
21
  from .mcp_server import mcp_server_if_needed
@@ -15,7 +15,7 @@ from pydantic_ai.settings import ModelSettings
15
15
  from pydantic_ai.usage import RequestUsage
16
16
 
17
17
  from ..json_renderer import to_jsonable_value
18
- from ..tools.registry import get_visible_tools
18
+ from ..tools.declarations import get_visible_tools
19
19
  from .base import BackendModelBase, append_text_projected_tool_result_preview_prompt
20
20
  from .claude_code_settings import ClaudeCodeModelSettings
21
21
  from .mcp_boundary import call_tool_for_claude_code_sdk
@@ -16,7 +16,7 @@ from pydantic_ai.profiles.openai import OpenAIJsonSchemaTransformer
16
16
  from pydantic_ai.settings import ModelSettings
17
17
  from pydantic_ai.usage import RequestUsage
18
18
 
19
- from ..tools.registry import get_visible_tools
19
+ from ..tools.declarations import get_visible_tools
20
20
  from .base import BackendModelBase, BackendModelSettings, append_text_projected_tool_result_preview_prompt
21
21
  from .mcp_server import mcp_server_if_needed
22
22
  from .text_projection import TextProjectedRequest, resolve_text_projection_staging_root_directory
@@ -13,7 +13,7 @@ from pydantic_ai.messages import BinaryContent, FileUrl, TextContent, UploadedFi
13
13
  from ..runtime.step_context import (
14
14
  DEFAULT_TOOL_RESULT_RENDERING_POLICY,
15
15
  ToolResultRenderingPolicy,
16
- get_current_step_context,
16
+ get_step_context,
17
17
  resolve_tool_result_rendering_policy,
18
18
  )
19
19
  from ..tools._tool_return_content import resolve_tool_return_segments
@@ -38,7 +38,7 @@ _logger = logging.getLogger("nighthawk")
38
38
 
39
39
  def _resolve_tool_result_rendering_policy() -> ToolResultRenderingPolicy:
40
40
  try:
41
- step_context = get_current_step_context()
41
+ step_context = get_step_context()
42
42
  except Exception:
43
43
  return DEFAULT_TOOL_RESULT_RENDERING_POLICY
44
44
  return resolve_tool_result_rendering_policy(step_context.tool_result_rendering_policy)
@@ -0,0 +1,59 @@
1
+ """Explicit inheritance and composition operations for execution scopes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping, Sequence
6
+ from dataclasses import dataclass
7
+ from types import MappingProxyType
8
+ from typing import ClassVar
9
+
10
+
11
+ class UnsetType:
12
+ """The singleton type of :data:`UNSET`, representing an omitted argument."""
13
+
14
+ __slots__ = ()
15
+ _instance: ClassVar[UnsetType | None] = None
16
+
17
+ def __new__(cls) -> UnsetType:
18
+ if cls._instance is None:
19
+ cls._instance = super().__new__(cls)
20
+ return cls._instance
21
+
22
+ def __repr__(self) -> str:
23
+ return "UNSET"
24
+
25
+ def __copy__(self) -> UnsetType:
26
+ return self
27
+
28
+ def __deepcopy__(self, identity_to_copy: dict[int, object]) -> UnsetType:
29
+ return self
30
+
31
+
32
+ UNSET = UnsetType()
33
+ """Inherit the enclosing scope value; also marks omitted rewrite fields."""
34
+
35
+
36
+ @dataclass(frozen=True, init=False)
37
+ class Extend[T]:
38
+ """Append a sequence to an inherited ordered collection, preserving entry identity."""
39
+
40
+ values: tuple[T, ...]
41
+
42
+ def __init__(self, values: Sequence[T]) -> None:
43
+ if not isinstance(values, Sequence) or isinstance(values, (str, bytes, bytearray)):
44
+ raise TypeError("Extend requires a sequence of entries")
45
+ object.__setattr__(self, "values", tuple(values))
46
+
47
+
48
+ @dataclass(frozen=True, init=False)
49
+ class Merge[T]:
50
+ """Merge named references with inherited references, checking conflicts by identity."""
51
+
52
+ name_to_value: Mapping[str, T]
53
+
54
+ def __init__(self, name_to_value: Mapping[str, T]) -> None:
55
+ if not isinstance(name_to_value, Mapping):
56
+ raise TypeError("Merge requires a name-to-value mapping")
57
+ if any(not isinstance(name, str) for name in name_to_value):
58
+ raise TypeError("Merge names must be strings")
59
+ object.__setattr__(self, "name_to_value", MappingProxyType(dict(name_to_value)))
@@ -3,7 +3,9 @@ from __future__ import annotations
3
3
  from typing import Any
4
4
 
5
5
  import tiktoken
6
- from pydantic import BaseModel, ConfigDict, Field, field_validator
6
+ from pydantic import BaseModel, ConfigDict, Field, InstanceOf, field_serializer, field_validator
7
+ from pydantic_ai.models import Model
8
+ from pydantic_core import PydanticSerializationError
7
9
 
8
10
  from .json_renderer import JsonRendererStyle
9
11
 
@@ -113,7 +115,10 @@ class StepExecutorConfiguration(BaseModel):
113
115
  """Configuration for a step executor.
114
116
 
115
117
  Attributes:
116
- model: Model identifier in "provider:model" format (e.g. "openai:gpt-4o").
118
+ model: Provider-qualified identifier or borrowed Pydantic AI Model instance.
119
+ Instances retain identity; the host owns client lifetime. Serialization
120
+ must explicitly exclude a live model field, including in nested records.
121
+ Configuration representations omit the model to avoid traversing credentials.
117
122
  model_settings: Provider-specific model settings. Accepts a dict or a
118
123
  backend-specific BaseModel instance (auto-converted to dict).
119
124
  prompts: Prompt templates for step execution.
@@ -128,7 +133,7 @@ class StepExecutorConfiguration(BaseModel):
128
133
 
129
134
  model_config = ConfigDict(extra="forbid", frozen=True)
130
135
 
131
- model: str = "openai-responses:gpt-5.4-nano"
136
+ model: str | InstanceOf[Model] = Field(default="openai-responses:gpt-5.6-luna", repr=False)
132
137
  model_settings: dict[str, Any] | BaseModel | None = None
133
138
 
134
139
  @field_validator("model_settings", mode="before")
@@ -147,8 +152,14 @@ class StepExecutorConfiguration(BaseModel):
147
152
 
148
153
  @field_validator("model")
149
154
  @classmethod
150
- def _validate_model(cls, value: str) -> str:
151
- return _validate_model_identifier(value)
155
+ def _validate_model(cls, value: str | Model) -> str | Model:
156
+ return _validate_model_identifier(value) if isinstance(value, str) else value
157
+
158
+ @field_serializer("model")
159
+ def _serialize_model(self, value: str | Model) -> str:
160
+ if not isinstance(value, str):
161
+ raise PydanticSerializationError("A runtime Model cannot be serialized; exclude the model field explicitly")
162
+ return value
152
163
 
153
164
  def resolve_token_encoding(self) -> tiktoken.Encoding:
154
165
  """Return the tiktoken encoding for this configuration.
@@ -160,7 +171,7 @@ class StepExecutorConfiguration(BaseModel):
160
171
  if self.tokenizer_encoding is not None:
161
172
  return tiktoken.get_encoding(self.tokenizer_encoding)
162
173
 
163
- _, model_name = self.model.split(":", 1)
174
+ model_name = self.model.split(":", 1)[1] if isinstance(self.model, str) else self.model.model_name
164
175
 
165
176
  try:
166
177
  return tiktoken.encoding_for_model(model_name)
nighthawk/errors.py CHANGED
@@ -21,5 +21,13 @@ class ToolValidationError(NighthawkError):
21
21
  """Raised when tool input validation fails."""
22
22
 
23
23
 
24
- class ToolRegistrationError(NighthawkError):
25
- """Raised when tool registration fails."""
24
+ class ToolDeclarationError(NighthawkError):
25
+ """Raised when a tool declaration is invalid."""
26
+
27
+
28
+ class NameConflictError(NighthawkError):
29
+ """Raised when distinct declarations claim the same name."""
30
+
31
+
32
+ class ToolNameConflictError(ToolDeclarationError, NameConflictError):
33
+ """Raised when tool declarations claim the same or a reserved name."""
@@ -12,8 +12,7 @@ from typing import Any, cast
12
12
  from ..runtime.runner import Runner, StepEnvelope
13
13
  from ..runtime.scoping import get_step_executor
14
14
  from ..runtime.step_context import python_cell_scope, python_name_scope
15
- from ..tools.registry import call_scope
16
- from .blocks import find_natural_blocks
15
+ from .blocks import extract_program, find_natural_blocks
17
16
  from .transform import transform_module_ast
18
17
 
19
18
  type NaturalFunctionCallable = Callable[..., Any]
@@ -239,33 +238,37 @@ def natural_function(func: NaturalFunctionCallable | None = None) -> NaturalFunc
239
238
 
240
239
  filename = inspect.getsourcefile(func) or "<nighthawk>"
241
240
 
241
+ # Runtime helpers referenced by the transformed body travel through the factory closure, not through
242
+ # module globals. The transformed function therefore shares ``func.__globals__`` with the original
243
+ # function: names defined in the module after decoration stay visible, and the module namespace is
244
+ # never polluted with helper names.
245
+ helper_name_to_value: dict[str, object] = {
246
+ "__nighthawk_runner__": _RunnerProxy(),
247
+ "__nh_extract_program__": extract_program,
248
+ "__nh_python_cell_scope__": python_cell_scope,
249
+ }
250
+ factory_name_to_value: dict[str, object] = {**name_to_value, **helper_name_to_value}
251
+
242
252
  factory_module = _build_transformed_factory_module(
243
253
  transformed_module=transformed_module,
244
254
  function_name=func.__name__,
245
- name_to_value=name_to_value,
255
+ name_to_value=factory_name_to_value,
246
256
  )
247
257
  code = compile(factory_module, filename, "exec")
248
258
 
249
- globals_namespace: dict[str, object] = dict(func.__globals__)
250
- globals_namespace["__nighthawk_runner__"] = _RunnerProxy()
251
- from .blocks import extract_program as _nh_extract_program
252
-
253
- globals_namespace["__nh_extract_program__"] = _nh_extract_program
254
- globals_namespace["__nh_python_cell_scope__"] = python_cell_scope
255
-
256
259
  module_namespace: dict[str, object] = {}
257
- exec(code, globals_namespace, module_namespace)
260
+ exec(code, func.__globals__, module_namespace)
258
261
 
259
262
  factory = module_namespace.get("__nh_factory__")
260
263
  if not callable(factory):
261
264
  raise RuntimeError("Transformed factory not found after compilation")
262
265
 
263
- transformed = factory(name_to_value)
266
+ transformed = factory(factory_name_to_value)
264
267
  if not callable(transformed):
265
268
  raise RuntimeError("Transformed function not found after factory execution")
266
269
 
267
270
  transformed_freevar_name_set = set(transformed.__code__.co_freevars)
268
- captured_name_set = set(name_to_value.keys())
271
+ captured_name_set = set(factory_name_to_value.keys())
269
272
 
270
273
  unexpected_freevar_name_set = transformed_freevar_name_set - captured_name_set
271
274
  allowed_unexpected_freevar_name_set = {func.__name__}
@@ -274,7 +277,7 @@ def natural_function(func: NaturalFunctionCallable | None = None) -> NaturalFunc
274
277
  f"Transformed function freevars do not match captured names. freevars={transformed.__code__.co_freevars!r} captured={tuple(sorted(name_to_value.keys()))!r}"
275
278
  )
276
279
 
277
- if transformed.__closure__ is None and name_to_value:
280
+ if transformed.__closure__ is None and transformed_freevar_name_set:
278
281
  raise RuntimeError("Transformed function closure is missing for captured names")
279
282
 
280
283
  if inspect.iscoroutinefunction(func):
@@ -282,20 +285,43 @@ def natural_function(func: NaturalFunctionCallable | None = None) -> NaturalFunc
282
285
 
283
286
  @wraps(func)
284
287
  async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
285
- with call_scope():
286
- if name_to_value:
287
- with python_name_scope(name_to_value):
288
- return await transformed_async(*args, **kwargs)
289
- return await transformed_async(*args, **kwargs)
288
+ if name_to_value:
289
+ with python_name_scope(name_to_value):
290
+ return await transformed_async(*args, **kwargs)
291
+ return await transformed_async(*args, **kwargs)
290
292
 
293
+ async_wrapper.__nighthawk_transformed_function__ = transformed # type: ignore[attr-defined]
291
294
  return cast(NaturalFunctionCallable, async_wrapper) # type: ignore[return-value]
292
295
 
293
296
  @wraps(func)
294
297
  def wrapper(*args: Any, **kwargs: Any) -> Any:
295
- with call_scope():
296
- if name_to_value:
297
- with python_name_scope(name_to_value):
298
- return transformed(*args, **kwargs)
299
- return transformed(*args, **kwargs)
298
+ if name_to_value:
299
+ with python_name_scope(name_to_value):
300
+ return transformed(*args, **kwargs)
301
+ return transformed(*args, **kwargs)
300
302
 
303
+ wrapper.__nighthawk_transformed_function__ = transformed # type: ignore[attr-defined]
301
304
  return cast(NaturalFunctionCallable, wrapper) # type: ignore[return-value]
305
+
306
+
307
+ def get_transformed_function(function: Callable[..., object]) -> Callable[..., object]:
308
+ """Find a Natural function's compiled body through conventional wrapper chains.
309
+
310
+ Standard inspect.unwrap follows the original source. This accessor exposes the
311
+ transformed body for inspection; normal execution should use the decorated callable
312
+ to establish its runtime context. Invalid inputs and wrapper cycles raise TypeError.
313
+ """
314
+ current: object = function
315
+ seen_identity_set: set[int] = set()
316
+ while id(current) not in seen_identity_set:
317
+ seen_identity_set.add(id(current))
318
+ if isinstance(current, (staticmethod, classmethod)) or inspect.ismethod(current):
319
+ current = current.__func__
320
+ continue
321
+ transformed = getattr(current, "__nighthawk_transformed_function__", None)
322
+ if callable(transformed):
323
+ return transformed
324
+ current = getattr(current, "__wrapped__", None)
325
+ if current is None:
326
+ break
327
+ raise TypeError("Expected a Natural function or a conventional wrapper around one")
@@ -326,11 +326,9 @@ def build_runtime_call_and_assignments(
326
326
  statements.extend(ast.parse(f'if "{name}" in __nh_bindings__:\n {name} = __nh_bindings__["{name}"]\n').body)
327
327
 
328
328
  # Add outcome extraction and dispatch.
329
- statements.extend(ast.parse('__nh_step_outcome__ = __nh_envelope__["step_outcome"]').body)
329
+ statements.extend(ast.parse('__nh_step_outcome__ = __nh_envelope__["outcome"]').body)
330
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
- )
331
+ outcome_source = 'if __nh_step_outcome__ is not None:\n if __nh_step_outcome__.kind == "return":\n return __nh_step_outcome__.value\n'
334
332
  if is_in_loop:
335
333
  outcome_source += (
336
334
  ' if __nh_step_outcome__.kind == "break":\n break\n if __nh_step_outcome__.kind == "continue":\n continue\n'
nighthawk/oversight.py CHANGED
@@ -5,37 +5,16 @@ from dataclasses import dataclass
5
5
  from types import MappingProxyType
6
6
 
7
7
  from opentelemetry.trace import get_current_span
8
- from pydantic import BaseModel
9
8
 
9
+ from .composition import UNSET, UnsetType
10
10
  from .errors import NighthawkError
11
11
  from .runtime.scoping import ExecutionRef
12
- from .runtime.step_contract import StepKind, StepOutcome
12
+ from .runtime.step_contract import StepKind
13
+ from .runtime.step_result import Break, Continue, Pass, Raise, Return, StepResult
13
14
 
14
15
 
15
- def _snapshot_value(value: object) -> object:
16
- if isinstance(value, BaseModel):
17
- return value.model_copy(deep=True)
18
- if isinstance(value, Mapping):
19
- return MappingProxyType({key: _snapshot_value(item) for key, item in value.items()})
20
- if isinstance(value, tuple):
21
- return tuple(_snapshot_value(item) for item in value)
22
- if isinstance(value, list):
23
- return tuple(_snapshot_value(item) for item in value)
24
- if isinstance(value, set):
25
- return frozenset(_snapshot_value(item) for item in value)
26
- if isinstance(value, frozenset):
27
- return frozenset(_snapshot_value(item) for item in value)
28
- return value
29
-
30
-
31
- def _snapshot_mapping(name_to_value: Mapping[str, object]) -> Mapping[str, object]:
32
- return MappingProxyType({name: _snapshot_value(value) for name, value in name_to_value.items()})
33
-
34
-
35
- def _snapshot_optional_step_outcome(step_outcome: StepOutcome | None) -> StepOutcome | None:
36
- if step_outcome is None:
37
- return None
38
- return _snapshot_value(step_outcome) # type: ignore[return-value]
16
+ def _reference_mapping(name_to_value: Mapping[str, object]) -> Mapping[str, object]:
17
+ return MappingProxyType(dict(name_to_value))
39
18
 
40
19
 
41
20
  class OversightRejectedError(NighthawkError):
@@ -53,17 +32,25 @@ class ToolCall:
53
32
  object.__setattr__(
54
33
  self,
55
34
  "argument_name_to_value",
56
- _snapshot_mapping(self.argument_name_to_value),
35
+ _reference_mapping(self.argument_name_to_value),
57
36
  )
58
37
 
59
38
 
60
39
  @dataclass(frozen=True)
61
- class StepCommitProposal:
40
+ class StepCommit:
41
+ """Validated step result presented to ``Oversight.inspect_step_commit`` before it is committed.
42
+
43
+ ``binding_name_to_value`` holds the write bindings after Pydantic validation and coercion.
44
+ ``outcome`` is a resolved variant. Values retain type and identity in shallow
45
+ read-only mapping views. Trusted hooks must not mutate these references; use
46
+ Rewrite for changes and copy or serialize explicitly for durable history.
47
+ """
48
+
62
49
  execution_ref: ExecutionRef
63
50
  processed_natural_program: str
64
51
  input_binding_name_to_value: Mapping[str, object]
65
- proposed_step_outcome: StepOutcome
66
- proposed_binding_name_to_value: Mapping[str, object]
52
+ outcome: StepResult
53
+ binding_name_to_value: Mapping[str, object]
67
54
  allowed_step_kinds: tuple[StepKind, ...]
68
55
  output_binding_name_set: frozenset[str]
69
56
  binding_name_to_type: Mapping[str, object]
@@ -72,17 +59,12 @@ class StepCommitProposal:
72
59
  object.__setattr__(
73
60
  self,
74
61
  "input_binding_name_to_value",
75
- _snapshot_mapping(self.input_binding_name_to_value),
76
- )
77
- object.__setattr__(
78
- self,
79
- "proposed_step_outcome",
80
- _snapshot_value(self.proposed_step_outcome),
62
+ _reference_mapping(self.input_binding_name_to_value),
81
63
  )
82
64
  object.__setattr__(
83
65
  self,
84
- "proposed_binding_name_to_value",
85
- _snapshot_mapping(self.proposed_binding_name_to_value),
66
+ "binding_name_to_value",
67
+ _reference_mapping(self.binding_name_to_value),
86
68
  )
87
69
  object.__setattr__(
88
70
  self,
@@ -92,7 +74,7 @@ class StepCommitProposal:
92
74
  object.__setattr__(
93
75
  self,
94
76
  "binding_name_to_type",
95
- _snapshot_mapping(self.binding_name_to_type),
77
+ _reference_mapping(self.binding_name_to_type),
96
78
  )
97
79
 
98
80
 
@@ -108,24 +90,29 @@ class Reject:
108
90
 
109
91
  @dataclass(frozen=True)
110
92
  class Rewrite:
111
- rewritten_step_outcome: StepOutcome | None = None
112
- rewritten_binding_name_to_value: Mapping[str, object] | None = None
93
+ """Replace supplied commit fields; UNSET inherits and explicit None is a return.
94
+
95
+ A mapping replaces all writes. A return_value patch requires an existing Return.
96
+ Use outcome=Return(value=...) to change another allowed outcome into a return.
97
+ Replacements are validated before commit without replaying return expressions.
98
+ """
99
+
100
+ outcome: StepResult | UnsetType = UNSET
101
+ binding_name_to_value: Mapping[str, object] | UnsetType = UNSET
102
+ return_value: object = UNSET
113
103
  reason: str | None = None
114
104
 
115
105
  def __post_init__(self) -> None:
116
- object.__setattr__(
117
- self,
118
- "rewritten_step_outcome",
119
- _snapshot_optional_step_outcome(self.rewritten_step_outcome),
120
- )
121
- if self.rewritten_binding_name_to_value is not None:
122
- object.__setattr__(
123
- self,
124
- "rewritten_binding_name_to_value",
125
- _snapshot_mapping(self.rewritten_binding_name_to_value),
126
- )
127
- if self.rewritten_step_outcome is None and self.rewritten_binding_name_to_value is None:
128
- raise ValueError("Rewrite must change rewritten_step_outcome or rewritten_binding_name_to_value")
106
+ if not isinstance(self.outcome, (UnsetType, Pass, Return, Break, Continue, Raise)):
107
+ raise TypeError("Rewrite outcome must be a resolved StepResult or UNSET")
108
+ if self.outcome is not UNSET and self.return_value is not UNSET:
109
+ raise ValueError("Rewrite cannot supply both outcome and return_value")
110
+ if not isinstance(self.binding_name_to_value, UnsetType):
111
+ if not isinstance(self.binding_name_to_value, Mapping):
112
+ raise TypeError("Rewrite binding_name_to_value must be a mapping or UNSET")
113
+ object.__setattr__(self, "binding_name_to_value", _reference_mapping(self.binding_name_to_value))
114
+ if self.outcome is UNSET and self.binding_name_to_value is UNSET and self.return_value is UNSET:
115
+ raise ValueError("Rewrite must change outcome, binding_name_to_value, or return_value")
129
116
 
130
117
 
131
118
  type ToolCallDecision = Accept | Reject
@@ -135,7 +122,7 @@ type StepCommitDecision = Accept | Reject | Rewrite
135
122
  @dataclass(frozen=True)
136
123
  class Oversight:
137
124
  inspect_tool_call: Callable[[ToolCall], ToolCallDecision] | None = None
138
- inspect_step_commit: Callable[[StepCommitProposal], StepCommitDecision] | None = None
125
+ inspect_step_commit: Callable[[StepCommit], StepCommitDecision] | None = None
139
126
 
140
127
 
141
128
  def record_oversight_decision(
@@ -168,13 +155,19 @@ def record_oversight_decision(
168
155
 
169
156
 
170
157
  __all__ = [
158
+ "Pass",
159
+ "Return",
160
+ "Break",
161
+ "Continue",
162
+ "Raise",
163
+ "StepResult",
171
164
  "Accept",
172
165
  "Oversight",
173
166
  "OversightRejectedError",
174
167
  "Reject",
175
168
  "Rewrite",
169
+ "StepCommit",
176
170
  "StepCommitDecision",
177
- "StepCommitProposal",
178
171
  "ToolCall",
179
172
  "ToolCallDecision",
180
173
  ]
@@ -11,7 +11,7 @@ from opentelemetry.trace import get_current_span
11
11
  from pydantic_ai.usage import RunUsage
12
12
 
13
13
  from ..errors import NighthawkError
14
- from ..runtime.scoping import get_current_usage_meter
14
+ from ..runtime.scoping import _optional_usage_meter
15
15
 
16
16
  _logger = logging.getLogger("nighthawk.resilience")
17
17
 
@@ -43,7 +43,7 @@ class BudgetExceededError(NighthawkError):
43
43
 
44
44
 
45
45
  def _get_usage_snapshot() -> RunUsage | None:
46
- usage_meter = get_current_usage_meter()
46
+ usage_meter = _optional_usage_meter()
47
47
  if usage_meter is None:
48
48
  return None
49
49
  return usage_meter.snapshot()