Files
2026-07-01 17:56:31 +08:00

97 lines
3.9 KiB
Python

"""Runtime-managed tool hook bus for Native Runtime V2."""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any, Awaitable, Callable, Optional
@dataclass
class RuntimeToolHookContext:
phase: str
tool_name: str
call: dict[str, Any]
task: Any = None
tool: Any = None
arguments: dict[str, Any] = field(default_factory=dict)
predicted_permission: Any = None
result: dict[str, Any] | None = None
state: dict[str, Any] = field(default_factory=dict)
RuntimeToolHook = Callable[[RuntimeToolHookContext], Awaitable[Optional[dict[str, Any]]]]
RuntimeHookEmitter = Callable[[str, dict[str, Any]], Awaitable[None]]
class RuntimeToolHookBus:
"""Composable pre/post/failure hook bus for runtime-managed tool execution."""
def __init__(self, *, emit_event: RuntimeHookEmitter | None = None) -> None:
self.emit_event = emit_event
self._pre_hooks: list[tuple[str, RuntimeToolHook]] = []
self._post_hooks: list[tuple[str, RuntimeToolHook]] = []
self._failure_hooks: list[tuple[str, RuntimeToolHook]] = []
def register_pre_hook(self, name: str, hook: RuntimeToolHook) -> None:
self._pre_hooks.append((name, hook))
def register_post_hook(self, name: str, hook: RuntimeToolHook) -> None:
self._post_hooks.append((name, hook))
def register_failure_hook(self, name: str, hook: RuntimeToolHook) -> None:
self._failure_hooks.append((name, hook))
async def run_pre_hooks(self, context: RuntimeToolHookContext) -> RuntimeToolHookContext:
return await self._run_hooks(self._pre_hooks, context)
async def run_post_hooks(self, context: RuntimeToolHookContext) -> RuntimeToolHookContext:
return await self._run_hooks(self._post_hooks, context)
async def run_failure_hooks(self, context: RuntimeToolHookContext) -> RuntimeToolHookContext:
return await self._run_hooks(self._failure_hooks, context)
async def _run_hooks(
self,
hooks: list[tuple[str, RuntimeToolHook]],
context: RuntimeToolHookContext,
) -> RuntimeToolHookContext:
for hook_name, hook in hooks:
patch = await hook(context) or {}
self._apply_patch(context, patch)
if self.emit_event:
await self.emit_event(
"tool_hook",
{
"phase": context.phase,
"tool_name": context.tool_name,
"tool_call_id": context.call.get("id", ""),
"hook_name": hook_name,
"stopped": bool(context.state.get("stop_execution")),
"result_overridden": context.result is not None,
},
)
if context.state.get("stop_execution"):
break
return context
@staticmethod
def _apply_patch(context: RuntimeToolHookContext, patch: dict[str, Any]) -> None:
if not patch:
return
if isinstance(patch.get("arguments"), dict):
context.arguments = dict(patch["arguments"])
if isinstance(patch.get("result"), dict):
context.result = dict(patch["result"])
if isinstance(patch.get("metadata"), dict):
context.state.setdefault("metadata", {}).update(dict(patch["metadata"]))
if isinstance(patch.get("approval"), dict):
context.state.setdefault("approval", {}).update(dict(patch["approval"]))
if "stop_execution" in patch:
context.state["stop_execution"] = bool(patch["stop_execution"])
if "stop_batch_on_failure" in patch:
context.state["stop_batch_on_failure"] = bool(patch["stop_batch_on_failure"])
if "prevent_continuation" in patch:
context.state["prevent_continuation"] = bool(patch["prevent_continuation"])
if patch.get("stop_reason"):
context.state["stop_reason"] = str(patch["stop_reason"])