| 1 | """Structured progress-event helpers shared by agent runtimes.""" |
| 2 | |
| 3 | from __future__ import annotations |
| 4 | |
| 5 | import inspect |
| 6 | from collections.abc import Awaitable, Callable |
| 7 | from typing import Any |
| 8 | |
| 9 | from nanobot.agent.hook import AgentHookContext |
| 10 | |
| 11 | |
| 12 | def on_progress_accepts_tool_events(cb: Callable[..., Any]) -> bool: |
| 13 | try: |
| 14 | sig = inspect.signature(cb) |
| 15 | except (TypeError, ValueError): |
| 16 | return False |
| 17 | if any(p.kind == inspect.Parameter.VAR_KEYWORD for p in sig.parameters.values()): |
| 18 | return True |
| 19 | return "tool_events" in sig.parameters |
| 20 | |
| 21 | |
| 22 | async def invoke_on_progress( |
| 23 | on_progress: Callable[..., Awaitable[None]], |
| 24 | content: str, |
| 25 | *, |
| 26 | tool_hint: bool = False, |
| 27 | tool_events: list[dict[str, Any]] | None = None, |
| 28 | ) -> None: |
| 29 | if tool_events and on_progress_accepts_tool_events(on_progress): |
| 30 | await on_progress(content, tool_hint=tool_hint, tool_events=tool_events) |
| 31 | return |
| 32 | await on_progress(content, tool_hint=tool_hint) |
| 33 | |
| 34 | |
| 35 | def build_tool_event_start_payload(tool_call: Any) -> dict[str, Any]: |
| 36 | return { |
| 37 | "version": 1, |
| 38 | "phase": "start", |
| 39 | "call_id": str(getattr(tool_call, "id", "") or ""), |
| 40 | "name": getattr(tool_call, "name", ""), |
| 41 | "arguments": getattr(tool_call, "arguments", {}) or {}, |
| 42 | "result": None, |
| 43 | "error": None, |
| 44 | "files": [], |
| 45 | "embeds": [], |
| 46 | } |
| 47 | |
| 48 | |
| 49 | def tool_event_result_extras(result: Any) -> tuple[list[Any], list[Any]]: |
| 50 | if not isinstance(result, dict): |
| 51 | return [], [] |
| 52 | files = result.get("files") if isinstance(result.get("files"), list) else [] |
| 53 | embeds = result.get("embeds") if isinstance(result.get("embeds"), list) else [] |
| 54 | return files, embeds |
| 55 | |
| 56 | |
| 57 | def build_tool_event_finish_payloads(context: AgentHookContext) -> list[dict[str, Any]]: |
| 58 | payloads: list[dict[str, Any]] = [] |
| 59 | count = min(len(context.tool_calls), len(context.tool_results), len(context.tool_events)) |
| 60 | for idx in range(count): |
| 61 | tool_call = context.tool_calls[idx] |
| 62 | result = context.tool_results[idx] |
| 63 | event = context.tool_events[idx] if isinstance(context.tool_events[idx], dict) else {} |
| 64 | status = event.get("status") |
| 65 | phase = "end" if status == "ok" else "error" |
| 66 | files, embeds = tool_event_result_extras(result) |
| 67 | payload = { |
| 68 | "version": 1, |
| 69 | "phase": phase, |
| 70 | "call_id": str(getattr(tool_call, "id", "") or ""), |
| 71 | "name": getattr(tool_call, "name", ""), |
| 72 | "arguments": getattr(tool_call, "arguments", {}) or {}, |
| 73 | "result": result if phase == "end" else None, |
| 74 | "error": None, |
| 75 | "files": files, |
| 76 | "embeds": embeds, |
| 77 | } |
| 78 | if phase == "error": |
| 79 | if isinstance(result, str) and result.strip(): |
| 80 | payload["error"] = result.strip() |
| 81 | else: |
| 82 | payload["error"] = str(event.get("detail") or "Tool execution failed") |
| 83 | payloads.append(payload) |
| 84 | return payloads |
| 85 |