mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-06-15 07:14:08 +00:00
* Improve tool call validation strictness Reject near-miss tool names without executing suggested tools. Require object-shaped tool parameters while preserving only lossless JSON wire-shape normalization. * Tighten tool call argument validation * Simplify tool argument validation tests * Improve tool name suggestions * Simplify tool suggestion helpers * Limit tool suggestions to canonical matches * Allow repair only for tool history replay * Clarify non-object tool argument errors * Inline replay tool argument normalization * Track only successful tool executions * Reject JSON null tool arguments
107 lines
3.6 KiB
Python
107 lines
3.6 KiB
Python
"""Structured progress-event helpers shared by agent runtimes."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import inspect
|
|
from collections.abc import Awaitable, Callable
|
|
from typing import Any
|
|
|
|
from nanobot.agent.hook import AgentHookContext
|
|
|
|
|
|
def on_progress_accepts_tool_events(cb: Callable[..., Any]) -> bool:
|
|
return _on_progress_accepts(cb, "tool_events")
|
|
|
|
|
|
def on_progress_accepts_file_edit_events(cb: Callable[..., Any]) -> bool:
|
|
return _on_progress_accepts(cb, "file_edit_events")
|
|
|
|
|
|
def _on_progress_accepts(cb: Callable[..., Any], name: str) -> bool:
|
|
try:
|
|
sig = inspect.signature(cb)
|
|
except (TypeError, ValueError):
|
|
return False
|
|
if any(p.kind == inspect.Parameter.VAR_KEYWORD for p in sig.parameters.values()):
|
|
return True
|
|
return name in sig.parameters
|
|
|
|
|
|
async def invoke_on_progress(
|
|
on_progress: Callable[..., Awaitable[None]],
|
|
content: str,
|
|
*,
|
|
tool_hint: bool = False,
|
|
tool_events: list[dict[str, Any]] | None = None,
|
|
) -> None:
|
|
if tool_events and on_progress_accepts_tool_events(on_progress):
|
|
await on_progress(content, tool_hint=tool_hint, tool_events=tool_events)
|
|
return
|
|
await on_progress(content, tool_hint=tool_hint)
|
|
|
|
|
|
async def invoke_file_edit_progress(
|
|
on_progress: Callable[..., Awaitable[None]],
|
|
file_edit_events: list[dict[str, Any]],
|
|
) -> None:
|
|
if not file_edit_events or not on_progress_accepts_file_edit_events(on_progress):
|
|
return
|
|
await on_progress("", file_edit_events=file_edit_events)
|
|
|
|
|
|
def _tool_event_arguments(tool_call: Any) -> dict[str, Any]:
|
|
arguments = getattr(tool_call, "arguments", {}) or {}
|
|
return arguments if isinstance(arguments, dict) else {}
|
|
|
|
|
|
def build_tool_event_start_payload(tool_call: Any) -> dict[str, Any]:
|
|
return {
|
|
"version": 1,
|
|
"phase": "start",
|
|
"call_id": str(getattr(tool_call, "id", "") or ""),
|
|
"name": getattr(tool_call, "name", ""),
|
|
"arguments": _tool_event_arguments(tool_call),
|
|
"result": None,
|
|
"error": None,
|
|
"files": [],
|
|
"embeds": [],
|
|
}
|
|
|
|
|
|
def tool_event_result_extras(result: Any) -> tuple[list[Any], list[Any]]:
|
|
if not isinstance(result, dict):
|
|
return [], []
|
|
files = result.get("files") if isinstance(result.get("files"), list) else []
|
|
embeds = result.get("embeds") if isinstance(result.get("embeds"), list) else []
|
|
return files, embeds
|
|
|
|
|
|
def build_tool_event_finish_payloads(context: AgentHookContext) -> list[dict[str, Any]]:
|
|
payloads: list[dict[str, Any]] = []
|
|
count = min(len(context.tool_calls), len(context.tool_results), len(context.tool_events))
|
|
for idx in range(count):
|
|
tool_call = context.tool_calls[idx]
|
|
result = context.tool_results[idx]
|
|
event = context.tool_events[idx] if isinstance(context.tool_events[idx], dict) else {}
|
|
status = event.get("status")
|
|
phase = "end" if status == "ok" else "error"
|
|
files, embeds = tool_event_result_extras(result)
|
|
payload = {
|
|
"version": 1,
|
|
"phase": phase,
|
|
"call_id": str(getattr(tool_call, "id", "") or ""),
|
|
"name": getattr(tool_call, "name", ""),
|
|
"arguments": _tool_event_arguments(tool_call),
|
|
"result": result if phase == "end" else None,
|
|
"error": None,
|
|
"files": files,
|
|
"embeds": embeds,
|
|
}
|
|
if phase == "error":
|
|
if isinstance(result, str) and result.strip():
|
|
payload["error"] = result.strip()
|
|
else:
|
|
payload["error"] = str(event.get("detail") or "Tool execution failed")
|
|
payloads.append(payload)
|
|
return payloads
|