| """ACP tool-call helpers for mapping hermes tools to ACP ToolKind and building content.""" |
|
|
| from __future__ import annotations |
|
|
| import uuid |
| from typing import Any, Dict, List, Optional |
|
|
| import acp |
| from acp.schema import ( |
| ToolCallLocation, |
| ToolCallStart, |
| ToolCallProgress, |
| ToolKind, |
| ) |
|
|
| |
| |
| |
|
|
| TOOL_KIND_MAP: Dict[str, ToolKind] = { |
| |
| "read_file": "read", |
| "write_file": "edit", |
| "patch": "edit", |
| "search_files": "search", |
| |
| "terminal": "execute", |
| "process": "execute", |
| "execute_code": "execute", |
| |
| "web_search": "fetch", |
| "web_extract": "fetch", |
| |
| "browser_navigate": "fetch", |
| "browser_click": "execute", |
| "browser_type": "execute", |
| "browser_snapshot": "read", |
| "browser_vision": "read", |
| "browser_scroll": "execute", |
| "browser_press": "execute", |
| "browser_back": "execute", |
| "browser_get_images": "read", |
| |
| "delegate_task": "execute", |
| "vision_analyze": "read", |
| "image_generate": "execute", |
| "text_to_speech": "execute", |
| |
| "_thinking": "think", |
| } |
|
|
|
|
| def get_tool_kind(tool_name: str) -> ToolKind: |
| """Return the ACP ToolKind for a hermes tool, defaulting to 'other'.""" |
| return TOOL_KIND_MAP.get(tool_name, "other") |
|
|
|
|
| def make_tool_call_id() -> str: |
| """Generate a unique tool call ID.""" |
| return f"tc-{uuid.uuid4().hex[:12]}" |
|
|
|
|
| def build_tool_title(tool_name: str, args: Dict[str, Any]) -> str: |
| """Build a human-readable title for a tool call.""" |
| if tool_name == "terminal": |
| cmd = args.get("command", "") |
| if len(cmd) > 80: |
| cmd = cmd[:77] + "..." |
| return f"terminal: {cmd}" |
| if tool_name == "read_file": |
| return f"read: {args.get('path', '?')}" |
| if tool_name == "write_file": |
| return f"write: {args.get('path', '?')}" |
| if tool_name == "patch": |
| mode = args.get("mode", "replace") |
| path = args.get("path", "?") |
| return f"patch ({mode}): {path}" |
| if tool_name == "search_files": |
| return f"search: {args.get('pattern', '?')}" |
| if tool_name == "web_search": |
| return f"web search: {args.get('query', '?')}" |
| if tool_name == "web_extract": |
| urls = args.get("urls", []) |
| if urls: |
| return f"extract: {urls[0]}" + (f" (+{len(urls)-1})" if len(urls) > 1 else "") |
| return "web extract" |
| if tool_name == "delegate_task": |
| goal = args.get("goal", "") |
| if goal and len(goal) > 60: |
| goal = goal[:57] + "..." |
| return f"delegate: {goal}" if goal else "delegate task" |
| if tool_name == "execute_code": |
| return "execute code" |
| if tool_name == "vision_analyze": |
| return f"analyze image: {args.get('question', '?')[:50]}" |
| return tool_name |
|
|
|
|
| |
| |
| |
|
|
|
|
| def build_tool_start( |
| tool_call_id: str, |
| tool_name: str, |
| arguments: Dict[str, Any], |
| ) -> ToolCallStart: |
| """Create a ToolCallStart event for the given hermes tool invocation.""" |
| kind = get_tool_kind(tool_name) |
| title = build_tool_title(tool_name, arguments) |
| locations = extract_locations(arguments) |
|
|
| if tool_name == "patch": |
| mode = arguments.get("mode", "replace") |
| if mode == "replace": |
| path = arguments.get("path", "") |
| old = arguments.get("old_string", "") |
| new = arguments.get("new_string", "") |
| content = [acp.tool_diff_content(path=path, new_text=new, old_text=old)] |
| else: |
| |
| patch_text = arguments.get("patch", "") |
| content = [acp.tool_content(acp.text_block(patch_text))] |
| return acp.start_tool_call( |
| tool_call_id, title, kind=kind, content=content, locations=locations, |
| raw_input=arguments, |
| ) |
|
|
| if tool_name == "write_file": |
| path = arguments.get("path", "") |
| file_content = arguments.get("content", "") |
| content = [acp.tool_diff_content(path=path, new_text=file_content)] |
| return acp.start_tool_call( |
| tool_call_id, title, kind=kind, content=content, locations=locations, |
| raw_input=arguments, |
| ) |
|
|
| if tool_name == "terminal": |
| command = arguments.get("command", "") |
| content = [acp.tool_content(acp.text_block(f"$ {command}"))] |
| return acp.start_tool_call( |
| tool_call_id, title, kind=kind, content=content, locations=locations, |
| raw_input=arguments, |
| ) |
|
|
| if tool_name == "read_file": |
| path = arguments.get("path", "") |
| content = [acp.tool_content(acp.text_block(f"Reading {path}"))] |
| return acp.start_tool_call( |
| tool_call_id, title, kind=kind, content=content, locations=locations, |
| raw_input=arguments, |
| ) |
|
|
| if tool_name == "search_files": |
| pattern = arguments.get("pattern", "") |
| target = arguments.get("target", "content") |
| content = [acp.tool_content(acp.text_block(f"Searching for '{pattern}' ({target})"))] |
| return acp.start_tool_call( |
| tool_call_id, title, kind=kind, content=content, locations=locations, |
| raw_input=arguments, |
| ) |
|
|
| |
| import json |
| try: |
| args_text = json.dumps(arguments, indent=2, default=str) |
| except (TypeError, ValueError): |
| args_text = str(arguments) |
| content = [acp.tool_content(acp.text_block(args_text))] |
| return acp.start_tool_call( |
| tool_call_id, title, kind=kind, content=content, locations=locations, |
| raw_input=arguments, |
| ) |
|
|
|
|
| def build_tool_complete( |
| tool_call_id: str, |
| tool_name: str, |
| result: Optional[str] = None, |
| ) -> ToolCallProgress: |
| """Create a ToolCallUpdate (progress) event for a completed tool call.""" |
| kind = get_tool_kind(tool_name) |
|
|
| |
| display_result = result or "" |
| if len(display_result) > 5000: |
| display_result = display_result[:4900] + f"\n... ({len(result)} chars total, truncated)" |
|
|
| content = [acp.tool_content(acp.text_block(display_result))] |
| return acp.update_tool_call( |
| tool_call_id, |
| kind=kind, |
| status="completed", |
| content=content, |
| raw_output=result, |
| ) |
|
|
|
|
| |
| |
| |
|
|
|
|
| def extract_locations( |
| arguments: Dict[str, Any], |
| ) -> List[ToolCallLocation]: |
| """Extract file-system locations from tool arguments.""" |
| locations: List[ToolCallLocation] = [] |
| path = arguments.get("path") |
| if path: |
| line = arguments.get("offset") or arguments.get("line") |
| locations.append(ToolCallLocation(path=path, line=line)) |
| return locations |
|
|