|
38 | 38 | import json |
39 | 39 | import os |
40 | 40 | import re |
| 41 | +import shlex |
41 | 42 | import asyncio |
42 | 43 | import time |
43 | 44 | from typing import AsyncGenerator, Awaitable, Callable, Optional |
@@ -1015,31 +1016,46 @@ def _render_tool_history_for_compose(self, msgs: list[dict]) -> str: |
1015 | 1016 | ' several tools. Emit\n' |
1016 | 1017 | ' {"action":"agent","reason":"<short>"}\n' |
1017 | 1018 | "\n" |
1018 | | - "MiOS verbs available for dispatch (use EXACT name + args shape):\n" |
1019 | | - ' open_app(name:str, position:str="default", args:list[str]?, monitor:int=0)\n' |
1020 | | - ' position enum: default / as-is / center / left / right /\n' |
1021 | | - ' top / bottom / top-left / top-right / bottom-left /\n' |
1022 | | - ' bottom-right / maximize\n' |
1023 | | - ' focus_window(title:str)\n' |
1024 | | - ' move_window(title:str, position:str, monitor:int=0)\n' |
1025 | | - ' close_window(title:str, mode:str="graceful") # mode: graceful|force\n' |
1026 | | - ' list_windows()\n' |
1027 | | - ' screen_layout()\n' |
1028 | | - ' open_url(url:str, browser:str?)\n' |
1029 | | - ' launch_app(name:str) # simple, no position\n' |
1030 | | - ' mios_find(name:str) # READ-ONLY resolve\n' |
1031 | | - ' mios_apps(filter:str?) # inventory\n' |
1032 | | - ' everything_search(query:str, limit:int=10, ext:str?)\n' |
1033 | | - ' system_status()\n' |
| 1019 | + "MiOS verbs available for dispatch. Each verb is tagged WRITE\n" |
| 1020 | + "(causes a visible system effect) or READ (returns info only):\n" |
| 1021 | + ' [WRITE] open_app(name, position="default", args?, monitor=0)\n' |
| 1022 | + ' -- LAUNCH an app/program. Use for "open X" / "launch X" /\n' |
| 1023 | + ' "start X" / "run X". position enum:\n' |
| 1024 | + ' default / as-is / center / left / right / top /\n' |
| 1025 | + ' bottom / top-left / top-right / bottom-left /\n' |
| 1026 | + ' bottom-right / maximize\n' |
| 1027 | + ' [WRITE] launch_app(name) -- simpler launch, no position arg\n' |
| 1028 | + ' [WRITE] focus_window(title) -- bring an OPEN window to front\n' |
| 1029 | + ' [WRITE] move_window(title, position, monitor=0)\n' |
| 1030 | + ' [WRITE] close_window(title, mode="graceful") -- mode: graceful|force\n' |
| 1031 | + ' [WRITE] open_url(url, browser?) -- open a URL in a browser\n' |
| 1032 | + ' [READ ] list_windows() -- list currently OPEN windows\n' |
| 1033 | + ' [READ ] screen_layout() -- monitor geometry\n' |
| 1034 | + ' [READ ] mios_find(name) -- resolve name -> path, no launch\n' |
| 1035 | + ' [READ ] mios_apps(filter?) -- INVENTORY of installed apps, no launch\n' |
| 1036 | + ' [READ ] everything_search(query, limit=10, ext?)\n' |
| 1037 | + ' [READ ] system_status()\n' |
| 1038 | + "\n" |
| 1039 | + "Verb-pick priority (most common cases first):\n" |
| 1040 | + ' "open X" / "launch X" / "start X" / "run X" -> open_app(name=X)\n' |
| 1041 | + ' "close X" -> close_window(title=X)\n' |
| 1042 | + ' "focus X" / "bring X to front" / "switch to X" -> focus_window(title=X)\n' |
| 1043 | + ' "move X to <pos>" -> move_window(title=X, position=<pos>)\n' |
| 1044 | + ' "what apps are installed" / "list apps" -> mios_apps()\n' |
| 1045 | + ' "what windows are open" -> list_windows()\n' |
| 1046 | + ' "go to <url>" / "visit <url>" -> open_url(url=<url>)\n' |
1034 | 1047 | "\n" |
1035 | 1048 | "Rules:\n" |
1036 | | - "- Pick `dispatch` ONLY when ONE call solves it. Position\n" |
1037 | | - " defaults to \"default\" (golden+16:10 centered); set\n" |
| 1049 | + "- `dispatch` only when ONE verb solves it.\n" |
| 1050 | + "- A WRITE verb is the right pick whenever the user asks for a\n" |
| 1051 | + " system effect (open/close/focus/move). NEVER pick a READ verb\n" |
| 1052 | + " when the user clearly wants an effect.\n" |
| 1053 | + "- Position defaults to \"default\" (golden+16:10 centered); set\n" |
1038 | 1054 | " explicitly only when the user named a side.\n" |
1039 | | - "- Pick `chat` for greetings, thanks, follow-up clarification\n" |
1040 | | - " the agent can answer in one sentence.\n" |
1041 | | - "- Pick `agent` for anything that needs N>1 tools, web\n" |
1042 | | - " research, install, file editing, multi-step plans.\n" |
| 1055 | + "- `chat` for greetings, thanks, one-sentence clarification.\n" |
| 1056 | + "- `agent` for N>1 tools, web research, install, file editing,\n" |
| 1057 | + " general knowledge questions, conversational follow-through.\n" |
| 1058 | + " MiOS-Agent is both an Agentic-OS AND a generalized AI agent.\n" |
1043 | 1059 | "- Mirror the user's language in `reply` fields.\n" |
1044 | 1060 | "- Output JSON ONLY -- no preamble, no markdown, no commentary." |
1045 | 1061 | ) |
@@ -1129,7 +1145,14 @@ async def _dispatch_mios_verb( |
1129 | 1145 | ea = " ".join(shlex.quote(str(a)) for a in extra_args) |
1130 | 1146 | cmd = f"{env_prefix}mios-windows launch {shlex.quote(name)} {ea}" |
1131 | 1147 | else: |
1132 | | - cmd = f"{env_prefix}mios-find {shlex.quote(name)} | bash" |
| 1148 | + # 2>/dev/null suppresses mios-find's interactive narrative |
| 1149 | + # ("(picked best of N; M alternatives:)" + " 1. [...] ..."), |
| 1150 | + # which is English noise written to stderr for human |
| 1151 | + # operators. Agent-side dispatch only needs the stdout |
| 1152 | + # resolution line (which is piped to bash). Without this |
| 1153 | + # filter the broker's combined stdout+stderr capture leaks |
| 1154 | + # the narrative into the user-facing tool_result. |
| 1155 | + cmd = f"{env_prefix}mios-find {shlex.quote(name)} 2>/dev/null | bash" |
1133 | 1156 | elif tool == "launch_app": |
1134 | 1157 | cmd = f"mios-launch {shlex.quote(str(args.get('name', '')))}" |
1135 | 1158 | elif tool == "focus_window": |
@@ -1925,20 +1948,42 @@ async def pipe( |
1925 | 1948 | result = json.loads(result_json) |
1926 | 1949 | except json.JSONDecodeError: |
1927 | 1950 | result = {"output": result_json} |
1928 | | - out = result.get("output") or result.get("stderr") or "" |
1929 | | - # Thin operator-facing reply: just confirm the |
1930 | | - # action + cite the broker output one-liner. No |
1931 | | - # polish (the verb already ran; nothing to polish). |
1932 | | - if result.get("success") is False: |
1933 | | - await self._emit(__event_emitter__, "✅", done=True) |
1934 | | - yield f"_⚠️ `{tool}` → {(out or 'failed')[:240]}_" |
1935 | | - return |
1936 | | - await self._emit(__event_emitter__, "✅", done=True) |
1937 | | - # Surface the first non-empty line of output for context. |
1938 | | - first_line = next((ln for ln in out.splitlines() |
1939 | | - if ln.strip()), "") |
1940 | | - yield f"✅ `{tool}` · {first_line[:240]}" if first_line \ |
1941 | | - else f"✅ `{tool}`" |
| 1951 | + ok = bool(result.get("success")) |
| 1952 | + await self._emit(__event_emitter__, |
| 1953 | + "✅" if ok else "⚠️", done=True) |
| 1954 | + # OpenAI-tool-result-native propagation: emit the |
| 1955 | + # structured tool_call + tool_result envelope inside |
| 1956 | + # a <details type="tool_calls"> block OWUI renders |
| 1957 | + # natively. The only literal characters at this layer |
| 1958 | + # are cross-locale identifiers (the tool name) + |
| 1959 | + # universal symbols (✅ / ⚠️). The structured JSON |
| 1960 | + # matches the OpenAI tool_calls / role:tool shape, |
| 1961 | + # so any downstream agent reading chat history sees |
| 1962 | + # the canonical tool_result content (not a prose |
| 1963 | + # render) and the operator gets a collapsible block |
| 1964 | + # with the raw data inside (zero English narrative). |
| 1965 | + envelope = { |
| 1966 | + "tool_call": { |
| 1967 | + "id": f"call_{int(time.time()*1000)}", |
| 1968 | + "type": "function", |
| 1969 | + "function": { |
| 1970 | + "name": tool, |
| 1971 | + "arguments": args if isinstance(args, dict) else {}, |
| 1972 | + }, |
| 1973 | + }, |
| 1974 | + "tool_result": { |
| 1975 | + "success": ok, |
| 1976 | + "output": (result.get("output") or "")[:2000], |
| 1977 | + "stderr": (result.get("stderr") or "")[:2000], |
| 1978 | + }, |
| 1979 | + } |
| 1980 | + symbol = "✅" if ok else "⚠️" |
| 1981 | + yield ( |
| 1982 | + f"<details type=\"tool_calls\" done=\"true\">\n" |
| 1983 | + f"<summary>{symbol} `{tool}`</summary>\n\n" |
| 1984 | + f"```json\n{json.dumps(envelope, indent=2, default=str)}\n```\n" |
| 1985 | + f"</details>" |
| 1986 | + ) |
1942 | 1987 | return |
1943 | 1988 | elif action == "chat": |
1944 | 1989 | reply = str(verdict.get("reply", "")).strip() |
|
0 commit comments