nighthawk-python 0.16.0__py3-none-any.whl → 0.17.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/backends/mcp_boundary.py +33 -6
- nighthawk/backends/mcp_server.py +4 -6
- nighthawk/backends/tool_bridge.py +16 -3
- nighthawk/lifecycle.py +2 -2
- nighthawk/oversight.py +30 -7
- nighthawk/runtime/__init__.py +14 -1
- nighthawk/runtime/model_response.py +43 -0
- nighthawk/runtime/operation_contract.py +33 -2
- nighthawk/runtime/operations.py +49 -19
- nighthawk/runtime/runner.py +19 -9
- nighthawk/runtime/step_context.py +2 -0
- nighthawk/runtime/step_executor.py +52 -23
- nighthawk/tools/_ordering.py +171 -7
- nighthawk/tools/contracts.py +6 -0
- nighthawk/tools/execution.py +31 -3
- {nighthawk_python-0.16.0.dist-info → nighthawk_python-0.17.0.dist-info}/METADATA +1 -1
- {nighthawk_python-0.16.0.dist-info → nighthawk_python-0.17.0.dist-info}/RECORD +19 -18
- {nighthawk_python-0.16.0.dist-info → nighthawk_python-0.17.0.dist-info}/WHEEL +0 -0
- {nighthawk_python-0.16.0.dist-info → nighthawk_python-0.17.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
-
from typing import cast
|
|
4
|
+
from typing import TYPE_CHECKING, cast
|
|
5
5
|
|
|
6
6
|
import tiktoken
|
|
7
7
|
from opentelemetry import context as otel_context
|
|
@@ -29,6 +29,9 @@ from ._text_fallback import (
|
|
|
29
29
|
)
|
|
30
30
|
from .tool_bridge import ToolHandler
|
|
31
31
|
|
|
32
|
+
if TYPE_CHECKING:
|
|
33
|
+
from mcp.types import CallToolResult
|
|
34
|
+
|
|
32
35
|
_logger = logging.getLogger("nighthawk")
|
|
33
36
|
|
|
34
37
|
|
|
@@ -187,27 +190,51 @@ def tool_handler_result_to_low_level_mcp_content(
|
|
|
187
190
|
return [mcp_types.TextContent(type="text", text=render_preview())]
|
|
188
191
|
|
|
189
192
|
|
|
193
|
+
def tool_handler_result_to_low_level_mcp_result(
|
|
194
|
+
*,
|
|
195
|
+
tool_name: str,
|
|
196
|
+
tool_handler_result: ToolHandlerResult,
|
|
197
|
+
rendering_policy: ToolResultRenderingPolicy | None = None,
|
|
198
|
+
) -> CallToolResult:
|
|
199
|
+
from mcp import types as mcp_types
|
|
200
|
+
|
|
201
|
+
content = tool_handler_result_to_low_level_mcp_content(
|
|
202
|
+
tool_name=tool_name, tool_handler_result=tool_handler_result, rendering_policy=rendering_policy
|
|
203
|
+
)
|
|
204
|
+
error = tool_handler_result["tool_outcome"]["error"] if tool_handler_result["kind"] == "tool_result" else None
|
|
205
|
+
return mcp_types.CallToolResult(
|
|
206
|
+
content=cast(list[mcp_types.ContentBlock], content),
|
|
207
|
+
is_error=tool_handler_result["kind"] != "tool_result" or error is not None,
|
|
208
|
+
structured_content={"error": dict(error)} if error is not None else None,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
|
|
190
212
|
def _tool_handler_result_to_claude_code_sdk_content(
|
|
191
213
|
*,
|
|
192
214
|
tool_name: str,
|
|
193
215
|
tool_handler_result: ToolHandlerResult,
|
|
194
216
|
rendering_policy: ToolResultRenderingPolicy | None = None,
|
|
195
217
|
) -> dict[str, object]:
|
|
196
|
-
|
|
218
|
+
result = tool_handler_result_to_low_level_mcp_result(
|
|
197
219
|
tool_name=tool_name,
|
|
198
220
|
tool_handler_result=tool_handler_result,
|
|
199
221
|
rendering_policy=rendering_policy,
|
|
200
222
|
)
|
|
201
223
|
|
|
202
224
|
dumped_content_block_list: list[dict[str, object]] = []
|
|
203
|
-
for content_block in
|
|
225
|
+
for content_block in result.content:
|
|
204
226
|
model_dump = getattr(content_block, "model_dump", None)
|
|
205
227
|
if callable(model_dump):
|
|
206
228
|
dumped_content_block_list.append(cast(dict[str, object], model_dump(mode="json", by_alias=True, exclude_none=True)))
|
|
207
229
|
continue
|
|
208
230
|
raise TypeError(f"Unsupported MCP content block: {type(content_block).__name__}")
|
|
209
231
|
|
|
210
|
-
|
|
232
|
+
response: dict[str, object] = {"content": dumped_content_block_list}
|
|
233
|
+
if result.is_error:
|
|
234
|
+
response["isError"] = True
|
|
235
|
+
if result.structured_content is not None:
|
|
236
|
+
response["structuredContent"] = result.structured_content
|
|
237
|
+
return response
|
|
211
238
|
|
|
212
239
|
|
|
213
240
|
async def _get_tool_handler_result(
|
|
@@ -252,14 +279,14 @@ async def call_tool_for_low_level_mcp_server(
|
|
|
252
279
|
tool_handler: ToolHandler,
|
|
253
280
|
parent_otel_context: OtelContext,
|
|
254
281
|
rendering_policy: ToolResultRenderingPolicy | None = None,
|
|
255
|
-
) ->
|
|
282
|
+
) -> CallToolResult:
|
|
256
283
|
tool_handler_result = await _get_tool_handler_result(
|
|
257
284
|
tool_name=tool_name,
|
|
258
285
|
arguments=arguments,
|
|
259
286
|
tool_handler=tool_handler,
|
|
260
287
|
parent_otel_context=parent_otel_context,
|
|
261
288
|
)
|
|
262
|
-
return
|
|
289
|
+
return tool_handler_result_to_low_level_mcp_result(
|
|
263
290
|
tool_name=tool_name,
|
|
264
291
|
tool_handler_result=tool_handler_result,
|
|
265
292
|
rendering_policy=rendering_policy,
|
nighthawk/backends/mcp_server.py
CHANGED
|
@@ -8,7 +8,7 @@ import time
|
|
|
8
8
|
from collections.abc import AsyncIterator, Awaitable, Callable
|
|
9
9
|
from contextlib import asynccontextmanager
|
|
10
10
|
from contextvars import copy_context
|
|
11
|
-
from typing import Any
|
|
11
|
+
from typing import Any
|
|
12
12
|
|
|
13
13
|
from opentelemetry import context as otel_context
|
|
14
14
|
from pydantic_ai.exceptions import UnexpectedModelBehavior
|
|
@@ -19,7 +19,7 @@ from ..runtime.step_context import (
|
|
|
19
19
|
resolve_tool_result_rendering_policy,
|
|
20
20
|
)
|
|
21
21
|
from ..tools.contracts import ToolHandlerResult, ToolOutcome, build_tool_result_observation
|
|
22
|
-
from .mcp_boundary import call_tool_for_low_level_mcp_server,
|
|
22
|
+
from .mcp_boundary import call_tool_for_low_level_mcp_server, tool_handler_result_to_low_level_mcp_result
|
|
23
23
|
from .tool_bridge import get_current_step_run_context_required
|
|
24
24
|
|
|
25
25
|
|
|
@@ -95,21 +95,19 @@ class McpServer:
|
|
|
95
95
|
"error": {"kind": "resolution", "message": f"Unknown tool: {name}", "guidance": "Choose a visible tool name and retry."},
|
|
96
96
|
}
|
|
97
97
|
tool_handler_result: ToolHandlerResult = build_tool_result_observation(tool_outcome=tool_outcome)
|
|
98
|
-
|
|
98
|
+
return tool_handler_result_to_low_level_mcp_result(
|
|
99
99
|
tool_name=name,
|
|
100
100
|
tool_handler_result=tool_handler_result,
|
|
101
101
|
rendering_policy=self._tool_result_rendering_policy,
|
|
102
102
|
)
|
|
103
|
-
return mcp_types.CallToolResult(content=cast(list[mcp_types.ContentBlock], content_block_list))
|
|
104
103
|
|
|
105
|
-
|
|
104
|
+
return await call_tool_for_low_level_mcp_server(
|
|
106
105
|
tool_name=name,
|
|
107
106
|
arguments=arguments,
|
|
108
107
|
tool_handler=handler,
|
|
109
108
|
parent_otel_context=self._parent_otel_context,
|
|
110
109
|
rendering_policy=self._tool_result_rendering_policy,
|
|
111
110
|
)
|
|
112
|
-
return mcp_types.CallToolResult(content=cast(list[mcp_types.ContentBlock], content_block_list))
|
|
113
111
|
|
|
114
112
|
mcp_server = McpLowLevelServer(
|
|
115
113
|
"nighthawk",
|
|
@@ -154,8 +154,10 @@ async def execute_tool_call(
|
|
|
154
154
|
try:
|
|
155
155
|
value = await manager.handle_call(ToolCallPart(tool_name, arguments, tool_call_id), wrap_validation_errors=False)
|
|
156
156
|
except ValidationError as exception:
|
|
157
|
-
if run_context.deps.operation_failure is
|
|
158
|
-
raise
|
|
157
|
+
if run_context.deps.operation_failure is not None:
|
|
158
|
+
raise run_context.deps.operation_failure from run_context.deps.operation_failure.__cause__
|
|
159
|
+
if observed_toolset.outcome is not None and (observed_toolset.outcome["error"] or {}).get("status") == "rejected":
|
|
160
|
+
return build_tool_result_observation(tool_outcome=observed_toolset.outcome)
|
|
159
161
|
retry_text = RetryPromptPart(
|
|
160
162
|
tool_name=tool_name,
|
|
161
163
|
content=exception.errors(include_url=False, include_context=False),
|
|
@@ -163,9 +165,20 @@ async def execute_tool_call(
|
|
|
163
165
|
).model_response()
|
|
164
166
|
return _build_retry_prompt_observation(retry_text=retry_text)
|
|
165
167
|
except ModelRetry as exception:
|
|
168
|
+
if run_context.deps.operation_failure is not None:
|
|
169
|
+
raise run_context.deps.operation_failure from run_context.deps.operation_failure.__cause__
|
|
170
|
+
if observed_toolset.outcome is not None and (observed_toolset.outcome["error"] or {}).get("status") == "rejected":
|
|
171
|
+
return build_tool_result_observation(tool_outcome=observed_toolset.outcome)
|
|
166
172
|
return _build_retry_prompt_observation(retry_text=exception.message)
|
|
167
173
|
|
|
168
|
-
if
|
|
174
|
+
if run_context.deps.operation_failure is not None:
|
|
175
|
+
raise run_context.deps.operation_failure
|
|
176
|
+
recorded_error = observed_toolset.outcome["error"] if observed_toolset.outcome is not None else None
|
|
177
|
+
if recorded_error is not None and recorded_error.get("status") == "rejected":
|
|
178
|
+
# A capability may replace a return value, but cannot turn a refused body into a success.
|
|
179
|
+
assert observed_toolset.outcome is not None
|
|
180
|
+
tool_outcome = observed_toolset.outcome
|
|
181
|
+
elif isinstance(value, ToolDenied):
|
|
169
182
|
tool_outcome: ToolOutcome = {"payload": None, "error": {"kind": "oversight", "message": value.message, "guidance": None}}
|
|
170
183
|
elif observed_toolset.outcome is not None and value is observed_toolset.return_value:
|
|
171
184
|
tool_outcome = observed_toolset.outcome
|
nighthawk/lifecycle.py
CHANGED
|
@@ -31,7 +31,7 @@ type FailureStage = Literal[
|
|
|
31
31
|
"return_validation",
|
|
32
32
|
"commit_inspection",
|
|
33
33
|
"rewrite_validation",
|
|
34
|
-
"
|
|
34
|
+
"oversight_abort",
|
|
35
35
|
"raise_construction",
|
|
36
36
|
"binding_assignment",
|
|
37
37
|
]
|
|
@@ -82,7 +82,7 @@ class StepFailed(_StepFinished):
|
|
|
82
82
|
attempted_outcome: StepResult | None = None
|
|
83
83
|
validated_binding_name_to_value: Mapping[str, object] | None = None
|
|
84
84
|
inspection_subject: Literal["step_commit", "operation"] | None = None
|
|
85
|
-
|
|
85
|
+
abort_reason: str | None = None
|
|
86
86
|
kind: Literal["failed"] = field(default="failed", init=False)
|
|
87
87
|
|
|
88
88
|
|
nighthawk/oversight.py
CHANGED
|
@@ -18,8 +18,8 @@ def _reference_mapping(name_to_value: Mapping[str, object]) -> Mapping[str, obje
|
|
|
18
18
|
return MappingProxyType(dict(name_to_value))
|
|
19
19
|
|
|
20
20
|
|
|
21
|
-
class
|
|
22
|
-
"""
|
|
21
|
+
class StepAbortedError(NighthawkError):
|
|
22
|
+
"""An explicit host decision ending the inspected step, without rollback."""
|
|
23
23
|
|
|
24
24
|
def __init__(self, reason: str, *, subject: str = "operation") -> None:
|
|
25
25
|
self.reason = reason
|
|
@@ -75,9 +75,29 @@ class Accept:
|
|
|
75
75
|
|
|
76
76
|
|
|
77
77
|
@dataclass(frozen=True)
|
|
78
|
-
class
|
|
78
|
+
class RejectOperation:
|
|
79
|
+
"""Do not execute this operation; expose reason and guidance to its caller or model."""
|
|
80
|
+
|
|
81
|
+
reason: str
|
|
82
|
+
guidance: str | None = None
|
|
83
|
+
|
|
84
|
+
def __post_init__(self) -> None:
|
|
85
|
+
if not isinstance(self.reason, str) or not self.reason.strip():
|
|
86
|
+
raise ValueError("RejectOperation requires a nonempty string reason")
|
|
87
|
+
if self.guidance is not None and not isinstance(self.guidance, str):
|
|
88
|
+
raise TypeError("RejectOperation guidance must be a string or None")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass(frozen=True)
|
|
92
|
+
class AbortStep:
|
|
93
|
+
"""End the current step; catching its exception cannot authorize completion."""
|
|
94
|
+
|
|
79
95
|
reason: str
|
|
80
96
|
|
|
97
|
+
def __post_init__(self) -> None:
|
|
98
|
+
if not isinstance(self.reason, str) or not self.reason.strip():
|
|
99
|
+
raise ValueError("AbortStep requires a nonempty string reason")
|
|
100
|
+
|
|
81
101
|
|
|
82
102
|
@dataclass(frozen=True)
|
|
83
103
|
class Rewrite:
|
|
@@ -106,12 +126,13 @@ class Rewrite:
|
|
|
106
126
|
raise ValueError("Rewrite Raise cannot include output bindings")
|
|
107
127
|
|
|
108
128
|
|
|
109
|
-
type
|
|
129
|
+
type OperationDecision = Accept | RejectOperation | AbortStep
|
|
130
|
+
type StepCommitDecision = Accept | Rewrite | AbortStep
|
|
110
131
|
|
|
111
132
|
|
|
112
133
|
@dataclass(frozen=True)
|
|
113
134
|
class Oversight:
|
|
114
|
-
inspect_operation: Callable[[OperationRequest],
|
|
135
|
+
inspect_operation: Callable[[OperationRequest], OperationDecision | Awaitable[OperationDecision]] | None = None
|
|
115
136
|
inspect_step_commit: Callable[[StepCommit], StepCommitDecision | Awaitable[StepCommitDecision]] | None = None
|
|
116
137
|
|
|
117
138
|
|
|
@@ -153,9 +174,11 @@ __all__ = [
|
|
|
153
174
|
"Raise",
|
|
154
175
|
"StepResult",
|
|
155
176
|
"Accept",
|
|
177
|
+
"AbortStep",
|
|
178
|
+
"OperationDecision",
|
|
156
179
|
"Oversight",
|
|
157
|
-
"
|
|
158
|
-
"
|
|
180
|
+
"RejectOperation",
|
|
181
|
+
"StepAbortedError",
|
|
159
182
|
"Rewrite",
|
|
160
183
|
"StepCommit",
|
|
161
184
|
"StepCommitDecision",
|
nighthawk/runtime/__init__.py
CHANGED
|
@@ -18,7 +18,14 @@ if TYPE_CHECKING:
|
|
|
18
18
|
from .environment import StepEnvironment
|
|
19
19
|
from .expression import eval_expression, eval_expression_async
|
|
20
20
|
from .inspection import inspect_value
|
|
21
|
-
from .operation_contract import
|
|
21
|
+
from .operation_contract import (
|
|
22
|
+
OperationDeliveryError,
|
|
23
|
+
OperationError,
|
|
24
|
+
OperationFinished,
|
|
25
|
+
OperationRejectedError,
|
|
26
|
+
OperationRejection,
|
|
27
|
+
OperationRequest,
|
|
28
|
+
)
|
|
22
29
|
from .step_definition import StepDefinition
|
|
23
30
|
from .step_request import StepCandidate, StepRequest
|
|
24
31
|
from .step_result import Break, Continue, Pass, Raise, Return, StepResult
|
|
@@ -48,6 +55,9 @@ _name_to_module = {
|
|
|
48
55
|
"StepEnvironment": "environment",
|
|
49
56
|
"inspect_value": "inspection",
|
|
50
57
|
"OperationError": "operation_contract",
|
|
58
|
+
"OperationDeliveryError": "operation_contract",
|
|
59
|
+
"OperationRejectedError": "operation_contract",
|
|
60
|
+
"OperationRejection": "operation_contract",
|
|
51
61
|
"OperationFinished": "operation_contract",
|
|
52
62
|
"OperationRequest": "operation_contract",
|
|
53
63
|
"Pass": "step_result",
|
|
@@ -82,6 +92,9 @@ __all__ = [
|
|
|
82
92
|
"StepEnvironment",
|
|
83
93
|
"inspect_value",
|
|
84
94
|
"OperationError",
|
|
95
|
+
"OperationDeliveryError",
|
|
96
|
+
"OperationRejectedError",
|
|
97
|
+
"OperationRejection",
|
|
85
98
|
"OperationFinished",
|
|
86
99
|
"OperationRequest",
|
|
87
100
|
"Pass",
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""A response-local rejection barrier, independent of model retry budgets."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterator
|
|
4
|
+
from contextlib import contextmanager
|
|
5
|
+
from contextvars import ContextVar
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
from .operation_contract import OperationRejection
|
|
9
|
+
from .step_context import StepContext
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class ModelResponseExecution:
|
|
14
|
+
step_context: StepContext
|
|
15
|
+
rejection: OperationRejection | None = None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
_model_response_execution: ContextVar[ModelResponseExecution | None] = ContextVar("nighthawk_model_response_execution", default=None)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@contextmanager
|
|
22
|
+
def model_response_scope(step_context: StepContext, *, reuse: bool = False) -> Iterator[ModelResponseExecution]:
|
|
23
|
+
current = _model_response_execution.get()
|
|
24
|
+
if reuse and current is not None and current.step_context is step_context:
|
|
25
|
+
yield current
|
|
26
|
+
return
|
|
27
|
+
execution = ModelResponseExecution(step_context)
|
|
28
|
+
token = _model_response_execution.set(execution)
|
|
29
|
+
try:
|
|
30
|
+
yield execution
|
|
31
|
+
finally:
|
|
32
|
+
_model_response_execution.reset(token)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_response_rejection(step_context: StepContext) -> OperationRejection | None:
|
|
36
|
+
execution = _model_response_execution.get()
|
|
37
|
+
return execution.rejection if execution is not None and execution.step_context is step_context else None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def record_response_rejection(step_context: StepContext, rejection: OperationRejection) -> None:
|
|
41
|
+
execution = _model_response_execution.get()
|
|
42
|
+
if execution is not None and execution.step_context is step_context:
|
|
43
|
+
execution.rejection = rejection
|
|
@@ -11,8 +11,9 @@ from ..errors import NighthawkError
|
|
|
11
11
|
from ..tools.contracts import ToolBoundaryError
|
|
12
12
|
from .execution_reference import ExecutionReference
|
|
13
13
|
|
|
14
|
-
type OperationKind = Literal["evaluation", "assignment", "return", "native_tool", "inspection"]
|
|
14
|
+
type OperationKind = Literal["evaluation", "assignment", "return", "native_tool", "inspection", "output"]
|
|
15
15
|
type OperationPhase = Literal["admission", "inspection", "evaluation", "target_resolution", "validation", "assignment", "completed"]
|
|
16
|
+
type OperationStatus = Literal["succeeded", "rejected", "aborted", "skipped", "failed", "interrupted"]
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
class OperationRequest(BaseModel):
|
|
@@ -33,14 +34,44 @@ class OperationRequest(BaseModel):
|
|
|
33
34
|
return MappingProxyType(dict(value))
|
|
34
35
|
|
|
35
36
|
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class OperationRejection:
|
|
39
|
+
"""One refused operation and the host's deliberately model-visible feedback."""
|
|
40
|
+
|
|
41
|
+
request: OperationRequest
|
|
42
|
+
reason: str
|
|
43
|
+
guidance: str | None = None
|
|
44
|
+
|
|
45
|
+
def model_feedback(self) -> dict[str, object]:
|
|
46
|
+
"""Expose correlation and feedback, without serializing live arguments or diagnostics."""
|
|
47
|
+
return {
|
|
48
|
+
"status": "rejected",
|
|
49
|
+
"operation_id": self.request.operation_id,
|
|
50
|
+
"step_execution_id": self.request.execution_reference.step_execution_id,
|
|
51
|
+
"reason": self.reason,
|
|
52
|
+
"guidance": self.guidance,
|
|
53
|
+
"evaluation_started": False,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class OperationRejectedError(NighthawkError):
|
|
58
|
+
"""The operation did not start; the same step can perform a different operation."""
|
|
59
|
+
|
|
60
|
+
def __init__(self, rejection: OperationRejection) -> None:
|
|
61
|
+
self.rejection = rejection
|
|
62
|
+
super().__init__(rejection.reason)
|
|
63
|
+
|
|
64
|
+
|
|
36
65
|
@dataclass(frozen=True)
|
|
37
66
|
class OperationFinished:
|
|
38
67
|
request: OperationRequest
|
|
39
|
-
status:
|
|
68
|
+
status: OperationStatus
|
|
40
69
|
phase: OperationPhase
|
|
41
70
|
evaluation_started: bool
|
|
42
71
|
value: object = None
|
|
43
72
|
original_exception: BaseException | None = None
|
|
73
|
+
rejection: OperationRejection | None = None
|
|
74
|
+
blocked_by_operation_id: str | None = None
|
|
44
75
|
|
|
45
76
|
|
|
46
77
|
class OperationError(ToolBoundaryError):
|
nighthawk/runtime/operations.py
CHANGED
|
@@ -10,11 +10,21 @@ from dataclasses import dataclass
|
|
|
10
10
|
from pydantic_ai.exceptions import ApprovalRequired, CallDeferred, ModelRetry
|
|
11
11
|
|
|
12
12
|
from ..errors import NighthawkError
|
|
13
|
-
from ..oversight import Accept,
|
|
13
|
+
from ..oversight import AbortStep, Accept, RejectOperation, StepAbortedError, record_oversight_decision
|
|
14
14
|
from ..ulid import generate_ulid
|
|
15
15
|
from .interruption import chain_cleanup_errors
|
|
16
16
|
from .notifications import await_notification
|
|
17
|
-
from .operation_contract import
|
|
17
|
+
from .operation_contract import (
|
|
18
|
+
OperationDeliveryError,
|
|
19
|
+
OperationError,
|
|
20
|
+
OperationFinished,
|
|
21
|
+
OperationKind,
|
|
22
|
+
OperationPhase,
|
|
23
|
+
OperationRejectedError,
|
|
24
|
+
OperationRejection,
|
|
25
|
+
OperationRequest,
|
|
26
|
+
OperationStatus,
|
|
27
|
+
)
|
|
18
28
|
from .step_context import StepContext
|
|
19
29
|
|
|
20
30
|
_active_operation_context: ContextVar[StepContext | None] = ContextVar("nighthawk_active_operation_context", default=None)
|
|
@@ -34,6 +44,7 @@ async def execute_operation_async(
|
|
|
34
44
|
admit: Callable[[], None],
|
|
35
45
|
execute: Callable[[OperationExecution], Awaitable[object]],
|
|
36
46
|
recoverable_admission_error: BaseException | None = None,
|
|
47
|
+
blocked_by: OperationRejection | None = None,
|
|
37
48
|
) -> object:
|
|
38
49
|
from .scoping import _lifecycle_var, _oversight_var
|
|
39
50
|
|
|
@@ -46,52 +57,71 @@ async def execute_operation_async(
|
|
|
46
57
|
token = _active_operation_context.set(step_context)
|
|
47
58
|
outcome: object = None
|
|
48
59
|
original_exception: BaseException | None = None
|
|
49
|
-
status = "succeeded"
|
|
60
|
+
status: OperationStatus = "succeeded"
|
|
61
|
+
rejection_error: OperationRejectedError | None = None
|
|
62
|
+
abort_error: StepAbortedError | None = None
|
|
50
63
|
try:
|
|
51
64
|
try:
|
|
52
|
-
|
|
65
|
+
if blocked_by is not None:
|
|
66
|
+
status = "skipped"
|
|
67
|
+
else:
|
|
68
|
+
admit()
|
|
53
69
|
execution.phase = "inspection"
|
|
54
70
|
oversight = _oversight_var.get()
|
|
55
|
-
if oversight is not None and oversight.inspect_operation is not None:
|
|
71
|
+
if blocked_by is None and oversight is not None and oversight.inspect_operation is not None:
|
|
56
72
|
decision = oversight.inspect_operation(request)
|
|
57
73
|
if inspect.isawaitable(decision):
|
|
58
74
|
decision = await decision
|
|
59
|
-
if not isinstance(decision, (Accept,
|
|
60
|
-
raise NighthawkError("Operation inspection must return Accept or
|
|
75
|
+
if not isinstance(decision, (Accept, RejectOperation, AbortStep)):
|
|
76
|
+
raise NighthawkError("Operation inspection must return Accept, RejectOperation, or AbortStep")
|
|
61
77
|
record_oversight_decision(
|
|
62
78
|
subject="operation",
|
|
63
|
-
verdict="reject" if isinstance(decision,
|
|
79
|
+
verdict="reject" if isinstance(decision, RejectOperation) else "abort" if isinstance(decision, AbortStep) else "accept",
|
|
64
80
|
execution_reference=request.execution_reference,
|
|
65
81
|
tool_name=request.tool_name,
|
|
66
82
|
reason=decision.reason,
|
|
67
83
|
)
|
|
68
|
-
if isinstance(decision,
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
84
|
+
if isinstance(decision, RejectOperation):
|
|
85
|
+
rejection_error = OperationRejectedError(OperationRejection(request, decision.reason, decision.guidance))
|
|
86
|
+
raise rejection_error
|
|
87
|
+
if isinstance(decision, AbortStep):
|
|
88
|
+
abort_error = StepAbortedError(decision.reason, subject="operation")
|
|
89
|
+
step_context.operation_abort = abort_error
|
|
90
|
+
raise abort_error
|
|
91
|
+
if blocked_by is None:
|
|
92
|
+
execution.phase = "evaluation"
|
|
93
|
+
execution.evaluation_started = True
|
|
94
|
+
outcome = await execute(execution)
|
|
95
|
+
execution.phase = "completed"
|
|
96
|
+
else:
|
|
97
|
+
execution.phase = "admission"
|
|
74
98
|
except BaseException as exception:
|
|
75
99
|
original_exception = exception
|
|
76
|
-
if
|
|
100
|
+
if exception is rejection_error:
|
|
77
101
|
status = "rejected"
|
|
102
|
+
elif exception is abort_error:
|
|
103
|
+
status = "aborted"
|
|
78
104
|
elif isinstance(exception, Exception):
|
|
79
105
|
status = "failed"
|
|
80
106
|
else:
|
|
81
107
|
status = "interrupted"
|
|
82
108
|
recoverable_admission = execution.phase == "admission" and exception is recoverable_admission_error
|
|
83
|
-
if
|
|
84
|
-
|
|
109
|
+
if (
|
|
110
|
+
exception is not rejection_error
|
|
111
|
+
and not recoverable_admission
|
|
112
|
+
and (not isinstance(exception, (OperationError, ModelRetry, ApprovalRequired, CallDeferred)) or execution.phase == "inspection")
|
|
85
113
|
):
|
|
86
114
|
step_context.operation_failure = exception
|
|
87
115
|
step_context.operation_failure_stage = "operation_inspection" if execution.phase == "inspection" else "operation_execution"
|
|
88
116
|
finished = OperationFinished(
|
|
89
117
|
request=request,
|
|
90
|
-
status=status,
|
|
91
|
-
phase=execution.phase,
|
|
118
|
+
status=status,
|
|
119
|
+
phase=execution.phase,
|
|
92
120
|
evaluation_started=execution.evaluation_started,
|
|
93
121
|
value=outcome,
|
|
94
122
|
original_exception=original_exception,
|
|
123
|
+
rejection=rejection_error.rejection if rejection_error is not None else None,
|
|
124
|
+
blocked_by_operation_id=blocked_by.request.operation_id if blocked_by is not None else None,
|
|
95
125
|
)
|
|
96
126
|
lifecycle = _lifecycle_var.get()
|
|
97
127
|
if lifecycle is not None and lifecycle.on_operation_finished is not None:
|
nighthawk/runtime/runner.py
CHANGED
|
@@ -15,10 +15,10 @@ from ..composition import UnsetType
|
|
|
15
15
|
from ..errors import ExecutionError, NighthawkError
|
|
16
16
|
from ..lifecycle import FailureStage, StepCompleted, StepFailed, StepFinished, StepInterrupted, StepLifecycle, StepRaised, StepStarted
|
|
17
17
|
from ..oversight import (
|
|
18
|
+
AbortStep,
|
|
18
19
|
Accept,
|
|
19
|
-
OversightRejectedError,
|
|
20
|
-
Reject,
|
|
21
20
|
Rewrite,
|
|
21
|
+
StepAbortedError,
|
|
22
22
|
StepCommit,
|
|
23
23
|
record_oversight_decision,
|
|
24
24
|
)
|
|
@@ -103,6 +103,7 @@ class Runner:
|
|
|
103
103
|
self.attempted_outcome: StepResult | None = None
|
|
104
104
|
self.outcome: StepResult | None = None
|
|
105
105
|
self.raise_exception: BaseException | None = None
|
|
106
|
+
self.commit_abort: StepAbortedError | None = None
|
|
106
107
|
self.context_stack = ExitStack()
|
|
107
108
|
|
|
108
109
|
@contextmanager
|
|
@@ -130,19 +131,25 @@ class Runner:
|
|
|
130
131
|
self.failure_stage = self.preparation.step_context.operation_failure_stage
|
|
131
132
|
finished: StepFinished
|
|
132
133
|
if isinstance(original_exception, Exception):
|
|
133
|
-
|
|
134
|
+
operation_abort = self.preparation.step_context.operation_abort if self.preparation is not None else None
|
|
135
|
+
rejection = (
|
|
136
|
+
original_exception
|
|
137
|
+
if isinstance(original_exception, StepAbortedError)
|
|
138
|
+
and (original_exception is operation_abort or original_exception is self.commit_abort)
|
|
139
|
+
else None
|
|
140
|
+
)
|
|
134
141
|
finished = StepFailed(
|
|
135
142
|
execution_reference=execution_reference,
|
|
136
143
|
processed_natural_program=self.preparation_state.processed_natural_program,
|
|
137
144
|
input_binding_name_to_value=self.preparation_state.input_binding_name_to_value,
|
|
138
145
|
allowed_step_kinds=self.preparation_state.allowed_step_kinds,
|
|
139
146
|
assigned_binding_name_to_value=self.assigned_binding_name_to_value,
|
|
140
|
-
failure_stage="
|
|
147
|
+
failure_stage="oversight_abort" if rejection else self.failure_stage,
|
|
141
148
|
original_exception=original_exception,
|
|
142
149
|
attempted_outcome=self.attempted_outcome,
|
|
143
150
|
validated_binding_name_to_value=self.validated_binding_name_to_value,
|
|
144
151
|
inspection_subject=cast(typing.Literal["step_commit", "operation"], rejection.subject) if rejection else None,
|
|
145
|
-
|
|
152
|
+
abort_reason=rejection.reason if rejection else None,
|
|
146
153
|
)
|
|
147
154
|
_record_internal_step_failure(step_span=step_span, exception=original_exception)
|
|
148
155
|
elif original_exception is not None:
|
|
@@ -201,6 +208,8 @@ class Runner:
|
|
|
201
208
|
raise delivery_exception from original_exception
|
|
202
209
|
raise
|
|
203
210
|
if isinstance(finished, StepFailed):
|
|
211
|
+
if finished.original_exception is self.commit_abort:
|
|
212
|
+
raise finished.original_exception
|
|
204
213
|
if self.preparation is not None and self.preparation.step_context.operation_failure is finished.original_exception:
|
|
205
214
|
raise finished.original_exception
|
|
206
215
|
raise ExecutionError(finished, description=self.failure_description) from finished.original_exception
|
|
@@ -341,14 +350,15 @@ class Runner:
|
|
|
341
350
|
if inspect.isawaitable(decision):
|
|
342
351
|
decision = await decision
|
|
343
352
|
|
|
344
|
-
if isinstance(decision,
|
|
353
|
+
if isinstance(decision, AbortStep):
|
|
345
354
|
record_oversight_decision(
|
|
346
355
|
subject="step_commit",
|
|
347
|
-
verdict="
|
|
356
|
+
verdict="abort",
|
|
348
357
|
execution_reference=execution_reference,
|
|
349
358
|
reason=decision.reason,
|
|
350
359
|
)
|
|
351
|
-
|
|
360
|
+
self.commit_abort = StepAbortedError(decision.reason, subject="step_commit")
|
|
361
|
+
raise self.commit_abort
|
|
352
362
|
|
|
353
363
|
if isinstance(decision, Accept):
|
|
354
364
|
record_oversight_decision(
|
|
@@ -360,7 +370,7 @@ class Runner:
|
|
|
360
370
|
return outcome, bindings
|
|
361
371
|
|
|
362
372
|
if not isinstance(decision, Rewrite):
|
|
363
|
-
raise NighthawkError("Oversight inspect_step_commit must return Accept,
|
|
373
|
+
raise NighthawkError("Oversight inspect_step_commit must return Accept, Rewrite, or AbortStep")
|
|
364
374
|
|
|
365
375
|
record_oversight_decision(
|
|
366
376
|
subject="step_commit",
|
|
@@ -15,6 +15,7 @@ from .execution_reference import ExecutionReference
|
|
|
15
15
|
|
|
16
16
|
if TYPE_CHECKING:
|
|
17
17
|
from ..lifecycle import FailureStage
|
|
18
|
+
from ..oversight import StepAbortedError
|
|
18
19
|
|
|
19
20
|
_MISSING: object = object()
|
|
20
21
|
"""Sentinel indicating a name could not be resolved. Distinct from None."""
|
|
@@ -72,6 +73,7 @@ class StepContext:
|
|
|
72
73
|
operation_lock: asyncio.Lock = field(default_factory=asyncio.Lock, init=False, repr=False)
|
|
73
74
|
operation_failure: BaseException | None = field(default=None, init=False, repr=False)
|
|
74
75
|
operation_failure_stage: FailureStage | None = field(default=None, init=False)
|
|
76
|
+
operation_abort: StepAbortedError | None = field(default=None, init=False, repr=False)
|
|
75
77
|
|
|
76
78
|
def __post_init__(self, task_name_to_value: Mapping[str, object]) -> None:
|
|
77
79
|
self.binding_name_to_type = MappingProxyType(dict(self.binding_name_to_type))
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import json
|
|
3
4
|
from typing import Any, Protocol, cast, runtime_checkable
|
|
4
5
|
|
|
5
6
|
from pydantic import TypeAdapter
|
|
6
7
|
from pydantic_ai import Agent, RunContext, StructuredDict
|
|
7
8
|
from pydantic_ai.capabilities import AbstractCapability
|
|
9
|
+
from pydantic_ai.exceptions import ModelRetry
|
|
8
10
|
from pydantic_ai.messages import UserContent
|
|
9
11
|
from pydantic_ai.models import ModelRequestContext
|
|
12
|
+
from pydantic_ai.output import OutputContext
|
|
10
13
|
from pydantic_ai.settings import ModelSettings
|
|
11
14
|
from pydantic_ai.toolsets.function import FunctionToolset
|
|
12
15
|
|
|
@@ -20,6 +23,8 @@ from ..tools.execution import ToolResultWrapperToolset
|
|
|
20
23
|
from .async_bridge import run_coroutine_synchronously
|
|
21
24
|
from .context import DefaultContextBuilder, build_context_content, build_context_request
|
|
22
25
|
from .model_candidate import resolve_model_candidate
|
|
26
|
+
from .model_response import get_response_rejection, record_response_rejection
|
|
27
|
+
from .operation_contract import OperationRejectedError
|
|
23
28
|
from .prompt import build_system_prompt, extract_references_and_program, resolve_step_system_prompt_template_text
|
|
24
29
|
from .scoping import (
|
|
25
30
|
_context_builder_var,
|
|
@@ -54,10 +59,45 @@ class _OperationFailureGuard(AbstractCapability[StepContext]):
|
|
|
54
59
|
# Pydantic AI can classify a body ValidationError as retryable or let a capability supply a substitute result.
|
|
55
60
|
# Neither clears our operation failure, and neither may cause another model request for this step.
|
|
56
61
|
if ctx.deps.operation_failure is not None:
|
|
57
|
-
raise ctx.deps.operation_failure
|
|
62
|
+
raise ctx.deps.operation_failure from ctx.deps.operation_failure.__cause__
|
|
58
63
|
return request_context
|
|
59
64
|
|
|
60
65
|
|
|
66
|
+
class _StepOutputBoundary(_OperationFailureGuard):
|
|
67
|
+
"""Resolve candidate operations while the agent can still receive feedback."""
|
|
68
|
+
|
|
69
|
+
def __init__(self, request: StepRequest) -> None:
|
|
70
|
+
super().__init__()
|
|
71
|
+
self.request = request
|
|
72
|
+
|
|
73
|
+
async def after_output_process(self, ctx: RunContext[StepContext], *, output_context: OutputContext, output: Any) -> Any:
|
|
74
|
+
if ctx.partial_output:
|
|
75
|
+
return output
|
|
76
|
+
if ctx.deps.operation_failure is not None:
|
|
77
|
+
raise ctx.deps.operation_failure from ctx.deps.operation_failure.__cause__
|
|
78
|
+
rejection = get_response_rejection(ctx.deps)
|
|
79
|
+
if rejection is not None:
|
|
80
|
+
raise ModelRetry(json.dumps(rejection.model_feedback(), ensure_ascii=False))
|
|
81
|
+
try:
|
|
82
|
+
return await resolve_model_candidate(self.request, _parse_step_outcome(output))
|
|
83
|
+
except OperationRejectedError as exception:
|
|
84
|
+
if ctx.deps.operation_failure is not None:
|
|
85
|
+
raise ctx.deps.operation_failure from ctx.deps.operation_failure.__cause__
|
|
86
|
+
record_response_rejection(ctx.deps, exception.rejection)
|
|
87
|
+
raise ModelRetry(json.dumps(exception.rejection.model_feedback(), ensure_ascii=False)) from exception
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _parse_step_outcome(raw_output: Any) -> StepOutcome:
|
|
91
|
+
try:
|
|
92
|
+
if isinstance(raw_output, StepFinalResult):
|
|
93
|
+
return raw_output.result
|
|
94
|
+
if isinstance(raw_output, dict) and "result" in raw_output:
|
|
95
|
+
return TypeAdapter(StepOutcome).validate_python(raw_output["result"])
|
|
96
|
+
return TypeAdapter(StepOutcome).validate_python(raw_output)
|
|
97
|
+
except Exception as exception:
|
|
98
|
+
raise ExecutionError(f"Step produced invalid step outcome: {exception}") from exception
|
|
99
|
+
|
|
100
|
+
|
|
61
101
|
@runtime_checkable
|
|
62
102
|
class AsyncExecutionAgent(Protocol):
|
|
63
103
|
async def run(self, *args: Any, **kwargs: Any) -> Any: ...
|
|
@@ -236,7 +276,7 @@ class AgentStepExecutor:
|
|
|
236
276
|
self,
|
|
237
277
|
*,
|
|
238
278
|
user_prompt: tuple[UserContent, ...],
|
|
239
|
-
|
|
279
|
+
request: StepRequest,
|
|
240
280
|
toolset: ToolResultWrapperToolset,
|
|
241
281
|
structured_output_type: object,
|
|
242
282
|
) -> Any:
|
|
@@ -246,12 +286,13 @@ class AgentStepExecutor:
|
|
|
246
286
|
normalized_user_prompt: str | tuple[UserContent, ...] = (
|
|
247
287
|
user_prompt[0] if len(user_prompt) == 1 and isinstance(user_prompt[0], str) else user_prompt
|
|
248
288
|
)
|
|
249
|
-
|
|
289
|
+
# First means last for after_output_process: resolve the output after application hooks.
|
|
290
|
+
capabilities = [_StepOutputBoundary(request), *_current_capabilities(), ToolAdmissionCapability()]
|
|
250
291
|
|
|
251
292
|
if isinstance(self.agent, AsyncExecutionAgent):
|
|
252
293
|
return await self.agent.run(
|
|
253
294
|
normalized_user_prompt,
|
|
254
|
-
deps=step_context,
|
|
295
|
+
deps=request.step_context,
|
|
255
296
|
toolsets=[toolset],
|
|
256
297
|
output_type=structured_output_type,
|
|
257
298
|
capabilities=capabilities,
|
|
@@ -260,7 +301,7 @@ class AgentStepExecutor:
|
|
|
260
301
|
if isinstance(self.agent, SyncExecutionAgent):
|
|
261
302
|
return self.agent.run_sync(
|
|
262
303
|
normalized_user_prompt,
|
|
263
|
-
deps=step_context,
|
|
304
|
+
deps=request.step_context,
|
|
264
305
|
toolsets=[toolset],
|
|
265
306
|
output_type=structured_output_type,
|
|
266
307
|
capabilities=capabilities,
|
|
@@ -313,20 +354,6 @@ class AgentStepExecutor:
|
|
|
313
354
|
|
|
314
355
|
return structured_output_type, step_system_prompt_fragment
|
|
315
356
|
|
|
316
|
-
def _parse_agent_result(
|
|
317
|
-
self,
|
|
318
|
-
result: Any,
|
|
319
|
-
) -> StepOutcome:
|
|
320
|
-
try:
|
|
321
|
-
raw_output = result.output
|
|
322
|
-
if isinstance(raw_output, StepFinalResult):
|
|
323
|
-
return raw_output.result
|
|
324
|
-
if isinstance(raw_output, dict) and "result" in raw_output:
|
|
325
|
-
return TypeAdapter(StepOutcome).validate_python(raw_output["result"])
|
|
326
|
-
return TypeAdapter(StepOutcome).validate_python(raw_output)
|
|
327
|
-
except Exception as e:
|
|
328
|
-
raise ExecutionError(f"Step produced invalid step outcome: {e}") from e
|
|
329
|
-
|
|
330
357
|
async def run_step_async(
|
|
331
358
|
self,
|
|
332
359
|
*,
|
|
@@ -361,7 +388,7 @@ class AgentStepExecutor:
|
|
|
361
388
|
try:
|
|
362
389
|
result = await self._run_agent(
|
|
363
390
|
user_prompt=user_prompt,
|
|
364
|
-
|
|
391
|
+
request=request,
|
|
365
392
|
toolset=toolset,
|
|
366
393
|
structured_output_type=structured_output_type,
|
|
367
394
|
)
|
|
@@ -373,14 +400,16 @@ class AgentStepExecutor:
|
|
|
373
400
|
raise failure from failure.__cause__
|
|
374
401
|
|
|
375
402
|
if step_context.operation_failure is not None:
|
|
376
|
-
raise step_context.operation_failure
|
|
403
|
+
raise step_context.operation_failure from step_context.operation_failure.__cause__
|
|
377
404
|
|
|
378
405
|
usage_meter = get_usage_meter()
|
|
379
406
|
if usage_meter is not None and hasattr(result, "usage"):
|
|
380
407
|
usage_meter.record(result.usage, kind="step")
|
|
381
408
|
|
|
382
|
-
|
|
383
|
-
|
|
409
|
+
if isinstance(result.output, StepCandidate):
|
|
410
|
+
return result.output
|
|
411
|
+
# Protocol agents that do not execute Pydantic AI capabilities own their model loop.
|
|
412
|
+
return await resolve_model_candidate(request, _parse_step_outcome(result.output))
|
|
384
413
|
|
|
385
414
|
def run_step(
|
|
386
415
|
self,
|
nighthawk/tools/_ordering.py
CHANGED
|
@@ -1,30 +1,40 @@
|
|
|
1
|
-
"""Keep
|
|
1
|
+
"""Keep validation and refusal barriers inside ordered model-tool dispatch.
|
|
2
2
|
|
|
3
3
|
Pydantic AI 2.37 validates function batches before its execution barriers.
|
|
4
|
-
These scoped compatibility hooks
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
These scoped compatibility hooks move validation to sequential dispatch,
|
|
5
|
+
stop later calls after a refusal, and discard candidates from that response.
|
|
6
|
+
Pydantic AI still owns execution, retry limits, and output selection among
|
|
7
|
+
permitted candidates. Ordinary Pydantic AI runs are unchanged.
|
|
7
8
|
"""
|
|
8
9
|
|
|
9
10
|
from __future__ import annotations
|
|
10
11
|
|
|
12
|
+
import json
|
|
11
13
|
from collections.abc import AsyncIterator, Iterator
|
|
12
14
|
from contextlib import contextmanager
|
|
13
15
|
from contextvars import ContextVar
|
|
14
16
|
from typing import Any
|
|
15
17
|
|
|
16
|
-
from pydantic_ai import _tool_execution
|
|
18
|
+
from pydantic_ai import _agent_graph, _tool_execution
|
|
17
19
|
from pydantic_ai.exceptions import UnexpectedModelBehavior
|
|
18
|
-
from pydantic_ai.messages import FunctionToolCallEvent, HandleResponseEvent, ToolCallPart
|
|
20
|
+
from pydantic_ai.messages import FunctionToolCallEvent, FunctionToolResultEvent, HandleResponseEvent, RetryPromptPart, ToolCallPart, ToolReturnPart
|
|
19
21
|
from pydantic_ai.tool_manager import ToolManager, ValidatedToolCall
|
|
20
22
|
from pydantic_ai.tools import ToolApproved
|
|
21
23
|
|
|
24
|
+
from ..runtime.model_response import get_response_rejection, model_response_scope
|
|
25
|
+
from ..runtime.operations import OperationExecution, execute_operation_async, new_operation_request
|
|
22
26
|
from ..runtime.step_context import StepContext
|
|
23
27
|
|
|
24
28
|
_ordered_dispatch: ContextVar[bool] = ContextVar("nighthawk_ordered_model_dispatch", default=False)
|
|
25
29
|
_original_function_calls = _tool_execution._ToolCallProcessor._run_function_calls
|
|
26
30
|
_original_validation = _tool_execution._ExhaustiveProcessor._validate_function_calls
|
|
27
31
|
_original_call_tool = _tool_execution._ExhaustiveProcessor._call_tool
|
|
32
|
+
_original_run = _tool_execution._ToolCallProcessor.run
|
|
33
|
+
_original_output_call = _tool_execution._ToolCallProcessor._run_output_tool_call
|
|
34
|
+
_original_run_output = _tool_execution._ToolCallProcessor._run_output
|
|
35
|
+
_original_finalize_deferred = _tool_execution._ToolCallProcessor._finalize_deferred
|
|
36
|
+
_original_early_strategy = _tool_execution._EarlyProcessor._run_strategy
|
|
37
|
+
_original_handle_tool_calls = _agent_graph.CallToolsNode._handle_tool_calls
|
|
28
38
|
|
|
29
39
|
|
|
30
40
|
def _requires_ordered_validation(processor: Any) -> bool:
|
|
@@ -32,13 +42,157 @@ def _requires_ordered_validation(processor: Any) -> bool:
|
|
|
32
42
|
return _ordered_dispatch.get() and context is not None and isinstance(context.deps, StepContext)
|
|
33
43
|
|
|
34
44
|
|
|
45
|
+
def _rejection(processor: Any) -> Any:
|
|
46
|
+
return get_response_rejection(processor.tool_manager.ctx.deps) if _requires_ordered_validation(processor) else None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
async def _skip_call(processor: Any, call: ToolCallPart, *, output: bool = False) -> ToolReturnPart:
|
|
50
|
+
context = processor.tool_manager.ctx.deps
|
|
51
|
+
rejection = get_response_rejection(context)
|
|
52
|
+
assert rejection is not None
|
|
53
|
+
tool_definition = processor.tool_manager.get_tool_def(call.tool_name)
|
|
54
|
+
kind = (tool_definition.metadata or {}).get("nighthawk.operation_kind") if tool_definition is not None else None
|
|
55
|
+
arguments = call.args if isinstance(call.args, dict) else {"unvalidated_arguments": call.args}
|
|
56
|
+
expression = arguments.get("expression")
|
|
57
|
+
target_path = arguments.get("target_path")
|
|
58
|
+
request = new_operation_request(
|
|
59
|
+
context,
|
|
60
|
+
kind="output" if output else kind if kind in ("evaluation", "assignment", "inspection") else "native_tool",
|
|
61
|
+
tool_name=call.tool_name,
|
|
62
|
+
argument_name_to_value=arguments,
|
|
63
|
+
expression=expression if isinstance(expression, str) else None,
|
|
64
|
+
target_path=target_path if isinstance(target_path, str) else None,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
async def unreachable(execution: OperationExecution) -> object:
|
|
68
|
+
raise AssertionError("A skipped operation cannot execute")
|
|
69
|
+
|
|
70
|
+
await execute_operation_async(context, request, admit=lambda: None, execute=unreachable, blocked_by=rejection)
|
|
71
|
+
return ToolReturnPart(
|
|
72
|
+
tool_name=call.tool_name,
|
|
73
|
+
tool_call_id=call.tool_call_id,
|
|
74
|
+
outcome="failed",
|
|
75
|
+
content={
|
|
76
|
+
"status": "skipped",
|
|
77
|
+
"operation_id": request.operation_id,
|
|
78
|
+
"step_execution_id": request.execution_reference.step_execution_id,
|
|
79
|
+
"blocked_by_operation_id": rejection.request.operation_id,
|
|
80
|
+
"evaluation_started": False,
|
|
81
|
+
"message": "Not executed after an operation rejection. Choose the next operations in a new response.",
|
|
82
|
+
},
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
async def _run(self: Any) -> AsyncIterator[HandleResponseEvent]:
|
|
87
|
+
if not _requires_ordered_validation(self):
|
|
88
|
+
async for event in _original_run(self):
|
|
89
|
+
yield event
|
|
90
|
+
return
|
|
91
|
+
with model_response_scope(self.tool_manager.ctx.deps, reuse=True) as execution:
|
|
92
|
+
if execution.rejection is not None:
|
|
93
|
+
# Native output under `early` was inspected before tool dispatch.
|
|
94
|
+
# Pydantic AI suppresses its retry; preserve both feedback and accounting.
|
|
95
|
+
self.ctx.state.consume_output_retry(self.ctx.deps.max_output_retries)
|
|
96
|
+
self.output_parts.append(RetryPromptPart(content=json.dumps(execution.rejection.model_feedback())))
|
|
97
|
+
async for event in _original_run(self):
|
|
98
|
+
yield event
|
|
99
|
+
if execution.rejection is not None:
|
|
100
|
+
# An earlier accepted output cannot mask a later refusal in the same response.
|
|
101
|
+
self.final_result = None
|
|
102
|
+
if self.winning_output_part is not None:
|
|
103
|
+
self.winning_output_part.content = {
|
|
104
|
+
"status": "discarded",
|
|
105
|
+
"blocked_by_operation_id": execution.rejection.request.operation_id,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
async def _handle_tool_calls(
|
|
110
|
+
self: Any, ctx: Any, tool_calls: list[ToolCallPart], *, response_output: Any = None
|
|
111
|
+
) -> AsyncIterator[HandleResponseEvent]:
|
|
112
|
+
if _ordered_dispatch.get() and isinstance(ctx.deps.user_deps, StepContext):
|
|
113
|
+
with model_response_scope(ctx.deps.user_deps):
|
|
114
|
+
async for event in _original_handle_tool_calls(self, ctx, tool_calls, response_output=response_output):
|
|
115
|
+
yield event
|
|
116
|
+
else:
|
|
117
|
+
async for event in _original_handle_tool_calls(self, ctx, tool_calls, response_output=response_output):
|
|
118
|
+
yield event
|
|
119
|
+
|
|
120
|
+
|
|
35
121
|
async def _run_function_calls(self: Any, calls: list[ToolCallPart]) -> AsyncIterator[HandleResponseEvent]:
|
|
36
122
|
batches = ([call] for call in calls) if _requires_ordered_validation(self) else (calls,)
|
|
37
123
|
for batch in batches:
|
|
124
|
+
if _rejection(self) is not None:
|
|
125
|
+
for call in batch:
|
|
126
|
+
part = await _skip_call(self, call)
|
|
127
|
+
self.output_parts.append(part)
|
|
128
|
+
yield FunctionToolCallEvent(call, args_valid=None)
|
|
129
|
+
yield FunctionToolResultEvent(part)
|
|
130
|
+
continue
|
|
38
131
|
async for event in _original_function_calls(self, batch):
|
|
39
132
|
yield event
|
|
40
133
|
|
|
41
134
|
|
|
135
|
+
async def _call_tool(self: Any, tool_call: Any, *, tool_call_result: Any) -> Any:
|
|
136
|
+
before = _rejection(self)
|
|
137
|
+
result = await _original_call_tool(self, tool_call, tool_call_result=tool_call_result)
|
|
138
|
+
if _requires_ordered_validation(self):
|
|
139
|
+
context = self.tool_manager.ctx.deps
|
|
140
|
+
if context.operation_failure is not None:
|
|
141
|
+
raise context.operation_failure
|
|
142
|
+
rejection = get_response_rejection(context)
|
|
143
|
+
if before is None and rejection is not None:
|
|
144
|
+
call = tool_call.call if isinstance(tool_call, ValidatedToolCall) else tool_call
|
|
145
|
+
return [
|
|
146
|
+
ToolReturnPart(tool_name=call.tool_name, tool_call_id=call.tool_call_id, content=rejection.model_feedback(), outcome="denied")
|
|
147
|
+
], None
|
|
148
|
+
return result
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
async def _run_output_tool_call(self: Any, call: ToolCallPart) -> Any:
|
|
152
|
+
if _rejection(self) is not None:
|
|
153
|
+
part = await _skip_call(self, call, output=True)
|
|
154
|
+
# No output validator or retry budget is consumed by an unattempted candidate.
|
|
155
|
+
return _tool_execution._OutputCallResult(
|
|
156
|
+
call=call,
|
|
157
|
+
retry_part=RetryPromptPart(tool_name=call.tool_name, tool_call_id=call.tool_call_id, content=json.dumps(part.content)),
|
|
158
|
+
)
|
|
159
|
+
result = await _original_output_call(self, call)
|
|
160
|
+
if _rejection(self) is not None and result.raise_exc is not None:
|
|
161
|
+
# Exhaustive output settlement must not hide exhausted retry limits behind an earlier candidate.
|
|
162
|
+
raise result.raise_exc
|
|
163
|
+
return result
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
async def _run_output(self: Any, call: ToolCallPart) -> AsyncIterator[HandleResponseEvent]:
|
|
167
|
+
if _rejection(self) is not None:
|
|
168
|
+
part = await _skip_call(self, call, output=True)
|
|
169
|
+
for event in self._record_output_part(call, part, args_valid=None):
|
|
170
|
+
yield event
|
|
171
|
+
return
|
|
172
|
+
async for event in _original_run_output(self, call):
|
|
173
|
+
yield event
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
async def _finalize_deferred(self: Any) -> AsyncIterator[HandleResponseEvent]:
|
|
177
|
+
if _rejection(self) is not None:
|
|
178
|
+
settled = {part.tool_call_id for part in self.output_parts if isinstance(part, (ToolReturnPart, RetryPromptPart))}
|
|
179
|
+
for call in self.tool_calls:
|
|
180
|
+
if call.tool_call_id not in settled:
|
|
181
|
+
part = await _skip_call(self, call)
|
|
182
|
+
self.output_parts.append(part)
|
|
183
|
+
yield FunctionToolCallEvent(call, args_valid=None)
|
|
184
|
+
yield FunctionToolResultEvent(part)
|
|
185
|
+
return
|
|
186
|
+
async for event in _original_finalize_deferred(self):
|
|
187
|
+
yield event
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
async def _run_early_strategy(self: Any) -> AsyncIterator[HandleResponseEvent]:
|
|
191
|
+
strategy = _tool_execution._GracefulProcessor._run_strategy if _requires_ordered_validation(self) else _original_early_strategy
|
|
192
|
+
async for event in strategy(self):
|
|
193
|
+
yield event
|
|
194
|
+
|
|
195
|
+
|
|
42
196
|
async def _validate_exhaustive_calls(
|
|
43
197
|
self: Any, calls: list[ToolCallPart], *, validated_calls: dict[str, ValidatedToolCall[Any]]
|
|
44
198
|
) -> AsyncIterator[HandleResponseEvent]:
|
|
@@ -54,6 +208,9 @@ async def _validate_exhaustive_calls(
|
|
|
54
208
|
|
|
55
209
|
|
|
56
210
|
async def _call_exhaustive_tool(self: Any, tool_call: Any, *, tool_call_result: Any) -> Any:
|
|
211
|
+
if _rejection(self) is not None:
|
|
212
|
+
call = tool_call.call if isinstance(tool_call, ValidatedToolCall) else tool_call
|
|
213
|
+
return [await _skip_call(self, call)], None
|
|
57
214
|
if (
|
|
58
215
|
_requires_ordered_validation(self)
|
|
59
216
|
and isinstance(tool_call, ToolCallPart)
|
|
@@ -68,14 +225,21 @@ async def _call_exhaustive_tool(self: Any, tool_call: Any, *, tool_call_result:
|
|
|
68
225
|
except UnexpectedModelBehavior:
|
|
69
226
|
self.ctx.state.check_incomplete_tool_call()
|
|
70
227
|
raise
|
|
71
|
-
return await
|
|
228
|
+
return await _call_tool(self, tool_call, tool_call_result=tool_call_result)
|
|
72
229
|
|
|
73
230
|
|
|
74
231
|
# Install dispatchers once under Python's import lock. Context variables scope
|
|
75
232
|
# their behavior across asynchronous tasks and supported thread bridges.
|
|
76
233
|
_tool_execution._ToolCallProcessor._run_function_calls = _run_function_calls
|
|
234
|
+
_tool_execution._ToolCallProcessor.run = _run
|
|
235
|
+
_tool_execution._ToolCallProcessor._call_tool = _call_tool
|
|
236
|
+
_tool_execution._ToolCallProcessor._run_output_tool_call = _run_output_tool_call
|
|
237
|
+
_tool_execution._ToolCallProcessor._run_output = _run_output
|
|
238
|
+
_tool_execution._ToolCallProcessor._finalize_deferred = _finalize_deferred
|
|
239
|
+
_tool_execution._EarlyProcessor._run_strategy = _run_early_strategy
|
|
77
240
|
_tool_execution._ExhaustiveProcessor._validate_function_calls = _validate_exhaustive_calls
|
|
78
241
|
_tool_execution._ExhaustiveProcessor._call_tool = _call_exhaustive_tool
|
|
242
|
+
_agent_graph.CallToolsNode._handle_tool_calls = _handle_tool_calls
|
|
79
243
|
|
|
80
244
|
|
|
81
245
|
@contextmanager
|
nighthawk/tools/contracts.py
CHANGED
|
@@ -23,6 +23,9 @@ class ToolError(TypedDict):
|
|
|
23
23
|
guidance: str | None
|
|
24
24
|
phase: NotRequired[str]
|
|
25
25
|
evaluation_started: NotRequired[bool]
|
|
26
|
+
operation_id: NotRequired[str]
|
|
27
|
+
step_execution_id: NotRequired[str | None]
|
|
28
|
+
status: NotRequired[Literal["rejected", "skipped"]]
|
|
26
29
|
|
|
27
30
|
|
|
28
31
|
class ToolOutcome(TypedDict):
|
|
@@ -77,6 +80,9 @@ def render_tool_handler_result_preview_text(
|
|
|
77
80
|
) -> str:
|
|
78
81
|
if tool_handler_result["kind"] == "tool_result":
|
|
79
82
|
tool_outcome = tool_handler_result["tool_outcome"]
|
|
83
|
+
if tool_outcome["error"] is not None and tool_outcome["error"].get("status") in ("rejected", "skipped"):
|
|
84
|
+
# Control feedback is not a lossy value preview. Preserve its identity and host-supplied reason.
|
|
85
|
+
return json.dumps({"value": None, "error": tool_outcome["error"]}, ensure_ascii=False)
|
|
80
86
|
return render_tool_result_json_text(
|
|
81
87
|
value=tool_outcome["payload"],
|
|
82
88
|
error=tool_outcome["error"],
|
nighthawk/tools/execution.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import json
|
|
5
6
|
from collections.abc import Awaitable, Callable
|
|
6
7
|
from typing import Any
|
|
7
8
|
|
|
@@ -9,6 +10,7 @@ from pydantic import TypeAdapter
|
|
|
9
10
|
from pydantic_ai import RunContext
|
|
10
11
|
from pydantic_ai.exceptions import ApprovalRequired, CallDeferred, ModelRetry
|
|
11
12
|
from pydantic_ai.messages import CachePoint, TextContent, ToolReturnContent, is_multi_modal_content
|
|
13
|
+
from pydantic_ai.tools import ToolDenied
|
|
12
14
|
from pydantic_ai.toolsets.abstract import ToolsetTool
|
|
13
15
|
from pydantic_ai.toolsets.wrapper import WrapperToolset
|
|
14
16
|
from pydantic_core import PydanticSerializationError
|
|
@@ -16,7 +18,8 @@ from pydantic_core import PydanticSerializationError
|
|
|
16
18
|
from ..json_renderer import project_discovery_value, to_jsonable_value
|
|
17
19
|
from ..runtime._user_content import is_top_level_sequence_payload
|
|
18
20
|
from ..runtime.discovery import DiscoveryResult, contains_discovery_result, map_discovery_containers
|
|
19
|
-
from ..runtime.
|
|
21
|
+
from ..runtime.model_response import record_response_rejection
|
|
22
|
+
from ..runtime.operation_contract import OperationError, OperationRejectedError, OperationRejection
|
|
20
23
|
from ..runtime.operations import OperationExecution, execute_operation_async, new_operation_request
|
|
21
24
|
from ..runtime.step_context import StepContext
|
|
22
25
|
from .contracts import ErrorKind, ToolError, ToolOutcome
|
|
@@ -77,20 +80,45 @@ def _normalize_tool_failure(*, kind: ErrorKind, message: str, guidance: str | No
|
|
|
77
80
|
def _build_standard_tool_return_value(*, tool_outcome: ToolOutcome) -> object:
|
|
78
81
|
if tool_outcome["error"] is None:
|
|
79
82
|
return tool_outcome["payload"]
|
|
83
|
+
if tool_outcome["error"].get("status") == "rejected":
|
|
84
|
+
return ToolDenied(json.dumps(tool_outcome["error"], ensure_ascii=False))
|
|
80
85
|
return {
|
|
81
86
|
"value": tool_outcome["payload"],
|
|
82
87
|
"error": tool_outcome["error"],
|
|
83
88
|
}
|
|
84
89
|
|
|
85
90
|
|
|
86
|
-
|
|
91
|
+
def rejection_tool_outcome(rejection: OperationRejection) -> ToolOutcome:
|
|
92
|
+
return {
|
|
93
|
+
"payload": None,
|
|
94
|
+
"error": {
|
|
95
|
+
"kind": "oversight",
|
|
96
|
+
"status": "rejected",
|
|
97
|
+
"operation_id": rejection.request.operation_id,
|
|
98
|
+
"step_execution_id": rejection.request.execution_reference.step_execution_id,
|
|
99
|
+
"message": rejection.reason,
|
|
100
|
+
"guidance": rejection.guidance,
|
|
101
|
+
"phase": "inspection",
|
|
102
|
+
"evaluation_started": False,
|
|
103
|
+
},
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
async def _run_tool_and_normalize(tool_call: Callable[[], Awaitable[object]], step_context: StepContext) -> ToolOutcome:
|
|
87
108
|
"""Exceptions from Pydantic AI toolset plumbing are handled in ``tool_bridge.execute_tool_call``."""
|
|
88
109
|
|
|
89
110
|
try:
|
|
90
111
|
value = await tool_call()
|
|
112
|
+
except OperationRejectedError as exception:
|
|
113
|
+
if step_context.operation_failure is not None:
|
|
114
|
+
raise step_context.operation_failure from step_context.operation_failure.__cause__
|
|
115
|
+
record_response_rejection(step_context, exception.rejection)
|
|
116
|
+
return rejection_tool_outcome(exception.rejection)
|
|
91
117
|
except (ModelRetry, CallDeferred, ApprovalRequired):
|
|
92
118
|
raise
|
|
93
119
|
except OperationError as exception:
|
|
120
|
+
if step_context.operation_failure is not None:
|
|
121
|
+
raise step_context.operation_failure from step_context.operation_failure.__cause__
|
|
94
122
|
outcome = _normalize_tool_failure(
|
|
95
123
|
kind=exception.kind,
|
|
96
124
|
message=str(exception),
|
|
@@ -125,7 +153,7 @@ class ToolResultWrapperToolset(WrapperToolset[StepContext]):
|
|
|
125
153
|
|
|
126
154
|
return await execute_operation_async(run_context.deps, request, admit=lambda: None, execute=execute)
|
|
127
155
|
|
|
128
|
-
tool_result = await _run_tool_and_normalize(tool_call)
|
|
156
|
+
tool_result = await _run_tool_and_normalize(tool_call, run_context.deps)
|
|
129
157
|
return tool_result
|
|
130
158
|
|
|
131
159
|
# DEPENDENCY: Pydantic AI's WrapperToolset calls self.call_tool() rather
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: nighthawk-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.17.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/
|
|
@@ -5,8 +5,8 @@ nighthawk/errors.py,sha256=U9ogl_0MLA1xXe_bU2RCA1gRWN0ev5E4a3FiBLgpilI,1282
|
|
|
5
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
|
-
nighthawk/lifecycle.py,sha256=
|
|
9
|
-
nighthawk/oversight.py,sha256=
|
|
8
|
+
nighthawk/lifecycle.py,sha256=X8y_xNjROcoFTr3jErPdH666sebnyNl8BRTpNcSItxw,5505
|
|
9
|
+
nighthawk/oversight.py,sha256=e_qOpHV_dEX0gJAge3YC20t0kv_42A_ko_Ym50zx2vQ,6691
|
|
10
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
|
|
@@ -16,11 +16,11 @@ nighthawk/backends/claude_code_cli.py,sha256=QntuxO2hJBstLJTmoA7Wk2SWdI-_1RnTEoC
|
|
|
16
16
|
nighthawk/backends/claude_code_sdk.py,sha256=v_KkBV-gSjh0VJ0Y1551GlxrsGOIBErbkPjZ4c8ippQ,13470
|
|
17
17
|
nighthawk/backends/claude_code_settings.py,sha256=wjHf8P38yrOvbhqUVT3uZ1qFjLlMIQ9de8DKHcRJn1g,1061
|
|
18
18
|
nighthawk/backends/codex.py,sha256=6uYw7FGQYkVtwe34TseOw12Ip8tGN78RZ4N2F9rgdVE,15101
|
|
19
|
-
nighthawk/backends/mcp_boundary.py,sha256=
|
|
20
|
-
nighthawk/backends/mcp_server.py,sha256=
|
|
19
|
+
nighthawk/backends/mcp_boundary.py,sha256=9A3MtJKocm4NNwKCAIs2I9VFyBVtnxjwEm33dKHddTI,10518
|
|
20
|
+
nighthawk/backends/mcp_server.py,sha256=stAqpAOvY7Mkte1cDmet0_rbIEjMERacg5hRi-zB6CM,8059
|
|
21
21
|
nighthawk/backends/process_management.py,sha256=ExPDyyJ7qFDzNe_1qEfiTM_s1ByAL9XXAm1yEwLFld0,776
|
|
22
22
|
nighthawk/backends/text_projection.py,sha256=4qhugWdDXuwGNpTTuAEZge-8PMPKtAMypo66iziDB0I,12034
|
|
23
|
-
nighthawk/backends/tool_bridge.py,sha256=
|
|
23
|
+
nighthawk/backends/tool_bridge.py,sha256=x2fXovrbgEYiSg_UmtT09hjkkvRIq7MfBJrL0gCCv4I,11741
|
|
24
24
|
nighthawk/backends/tool_transport.py,sha256=iknOFt6tgi47Pfru2vQmQJuA8UPmyoCiFL1kO9J2sS0,3537
|
|
25
25
|
nighthawk/natural/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
26
26
|
nighthawk/natural/_names.py,sha256=zWXgVLbsFqzxDnQdlGtJamt0zUL8GlZckvECi5FEm70,916
|
|
@@ -35,7 +35,7 @@ nighthawk/resilience/_fallback.py,sha256=oqmmmGKe2BgQUWGEdBuMYTEFalnjUllYDHwSBa-
|
|
|
35
35
|
nighthawk/resilience/_retry.py,sha256=9Rq8EpdtW_3B_-QmEzn7nJ2G4QdUJE7KXuvuMNRY14k,8462
|
|
36
36
|
nighthawk/resilience/_timeout.py,sha256=j1FPvnQB3XtjH-7UVu7Cp4ujDtF5qQOWFcO_YOrth5I,4024
|
|
37
37
|
nighthawk/resilience/_vote.py,sha256=rV2x8AkmoZJLAjUOfOV5SUa45NeZYo0kYic8lPuugXE,4675
|
|
38
|
-
nighthawk/runtime/__init__.py,sha256=
|
|
38
|
+
nighthawk/runtime/__init__.py,sha256=SipxZoD-LoQIGhiacTKWK_NfMtRwWaSlBiIWaFquCnM,3456
|
|
39
39
|
nighthawk/runtime/_user_content.py,sha256=Vnt5Nbbk0I02m06ng8OtooE0Ph0Qqc3twzqbjsDUA6Q,3122
|
|
40
40
|
nighthawk/runtime/annotations.py,sha256=TPjBz8-DZ9BjXSrRLlUqKVq4mGTvKBffPwHoHWdN8EA,3029
|
|
41
41
|
nighthawk/runtime/assignment.py,sha256=hCMrLpfN7Z3CuE56co1uemJn0nhMwO0tDwYnlQTPqcs,6486
|
|
@@ -51,30 +51,31 @@ nighthawk/runtime/expression.py,sha256=421HuL53GGEKW1Tuag9VRRcJffmPShpJ0rTRmZYkx
|
|
|
51
51
|
nighthawk/runtime/inspection.py,sha256=06zWKw_u61585qc-Wp1QmvKAekydWoCcmaDEU22Hdbw,3454
|
|
52
52
|
nighthawk/runtime/interruption.py,sha256=9B3hjMbXXtjf1sP2qVAf10dWk0KDS_UttZwqaix1tFU,679
|
|
53
53
|
nighthawk/runtime/model_candidate.py,sha256=X0aJvlk9zSDTwU6KCnYLBnOuiKab-E4IqVEeypvljhs,3122
|
|
54
|
+
nighthawk/runtime/model_response.py,sha256=CcdJE440LJgvbmS3YpE6dv9BHvkWnl_9zpUdwgqsUKc,1565
|
|
54
55
|
nighthawk/runtime/notifications.py,sha256=khLidCGr9lc2aed_GwGiWaUOx6jNQepGqY0_a6flJWo,1449
|
|
55
|
-
nighthawk/runtime/operation_contract.py,sha256=
|
|
56
|
-
nighthawk/runtime/operations.py,sha256=
|
|
56
|
+
nighthawk/runtime/operation_contract.py,sha256=h_wDoz780Jrr9lJbTyZHWRaq_RjP_qk3h3-3arceT_U,3596
|
|
57
|
+
nighthawk/runtime/operations.py,sha256=5IkgJJAX3nwv4t_AKAVyT-oFUjCPjUTYzaqdDVTc7J0,8010
|
|
57
58
|
nighthawk/runtime/preparation.py,sha256=zLrnP53k7PrKv4gTYf_RJDB2dOEbI_KxB7JSqPzG0EM,11396
|
|
58
59
|
nighthawk/runtime/prompt.py,sha256=ihKOoMEWiTKX6SsyXiAvXydPfgnIAQBrBhDsBib2_ak,29750
|
|
59
|
-
nighthawk/runtime/runner.py,sha256=
|
|
60
|
+
nighthawk/runtime/runner.py,sha256=prIcoi6rZrSfA09YkO_ajDoXSqN1FmIJ2fD46rhvlLQ,27587
|
|
60
61
|
nighthawk/runtime/scoping.py,sha256=94l8lW9UWDSAh4e1-lG1GOt3l7vlm_KMfAcCTbGGCM0,21623
|
|
61
|
-
nighthawk/runtime/step_context.py,sha256=
|
|
62
|
+
nighthawk/runtime/step_context.py,sha256=k5oy_0GgWS4XeLiY7C-UQ9IVnQ7JSZEoR3Iyz-7pUp0,5972
|
|
62
63
|
nighthawk/runtime/step_contract.py,sha256=xIQcpD1PDY_gqrTqSUZ_5yZoMQt1EJOU33fpn-Da_28,6737
|
|
63
64
|
nighthawk/runtime/step_definition.py,sha256=f64IVSAfpbxUpFVdIKBuWW43m2sQRQDvVmFgBrQeKbE,1913
|
|
64
|
-
nighthawk/runtime/step_executor.py,sha256=
|
|
65
|
+
nighthawk/runtime/step_executor.py,sha256=QP23Cv91Xc_EC7iWIcGinb1219amaX-zWUg6VuSJuns,17818
|
|
65
66
|
nighthawk/runtime/step_request.py,sha256=qs_kJZ_SqvYSD4yTs-5jSoHVrTbVT27YxWUFHeLHLjM,4508
|
|
66
67
|
nighthawk/runtime/step_result.py,sha256=mCdteQgQMMwoYTvhCJLuwcctljw-rjjyoFy2awfiRRg,1678
|
|
67
68
|
nighthawk/runtime/tool_calls.py,sha256=IZQWk2IH8rPF6hjVBzBDn0cpGDkG9k368dDFpmquYOg,3318
|
|
68
69
|
nighthawk/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
69
70
|
nighthawk/tools/_admission.py,sha256=T4W3n8Y36Q4CnAE01W85ftbd16Z1W7xjfqwn2qMXZFs,4075
|
|
70
|
-
nighthawk/tools/_ordering.py,sha256=
|
|
71
|
+
nighthawk/tools/_ordering.py,sha256=rTPKMIN4664EUeyAj-ouf3kjUY3sIATdXUOYBGCWKGU,12270
|
|
71
72
|
nighthawk/tools/_tool_return_content.py,sha256=HlDDufHvMvqHfw3-L5fqQYM3s4qxaWz8GFjgNkHcg8s,2111
|
|
72
73
|
nighthawk/tools/assignment.py,sha256=Uai4UCw0IICssmNRJvUkdPTX9QWXnyLIASVbw8c3PFk,1254
|
|
73
|
-
nighthawk/tools/contracts.py,sha256=
|
|
74
|
+
nighthawk/tools/contracts.py,sha256=xMrNjIducxIzcMn9o3zPoZN-Q8FPTSceCqZO-zk5qwA,5784
|
|
74
75
|
nighthawk/tools/declarations.py,sha256=goPDT7iVaXez11R_9z8e4czeeA7wevmZPs8sO85hqdA,3986
|
|
75
|
-
nighthawk/tools/execution.py,sha256=
|
|
76
|
+
nighthawk/tools/execution.py,sha256=8knipGL3SpjY9X-wPrGzC6_-USpWjXJtv0-JrzcnX-M,8068
|
|
76
77
|
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.
|
|
78
|
+
nighthawk_python-0.17.0.dist-info/METADATA,sha256=I7pUiaTPLhW00Q032Wa1R1REheYK9DBejKQw6PVIj98,6147
|
|
79
|
+
nighthawk_python-0.17.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
80
|
+
nighthawk_python-0.17.0.dist-info/licenses/LICENSE,sha256=R8tjrX79o5Pbi_l_aFPPWDbTZkmz4is0CxmzMMs3f0A,1076
|
|
81
|
+
nighthawk_python-0.17.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|