Skip to content

Commit 3264f0f

Browse files
mios-devclaude
andcommitted
MiOS-Agent pipe: stderr-suppress dispatch, tool_calls envelope reply, zero topic exclusions
Five fixes from operator-driven verification this session: 1. shlex import (was the immediate NameError crash on every open_app / launch_app dispatch -- _dispatch_mios_verb used shlex.quote heavily but the module was never imported). 2. Router prompt clarity (was: router picked mios_apps for "open notepad" because both verbs contained "app"). Now each verb is tagged [WRITE] vs [READ] and a Verb-pick priority section lists the literal patterns (open X / close X / focus X / move X / list apps / list windows / go to URL). Explicit rule: NEVER pick a READ verb when the user clearly wants a system effect. 3. mios-find narrative leak suppressed at dispatch (operator-observed: "✅ open_app · (picked best of 4; 3 alternatives:)"). mios-find writes its picker narrative to stderr in human-mode; the broker's CAPTURE concatenates stdout+stderr, so the agent-facing tool_result picked up English noise. Fix: 2>/dev/null on the dispatch bash command for open_app so only the resolution stdout is piped to bash. 4. Tool-result reply is now OpenAI-tool-result-native. Previous form yielded a hardcoded English line ("✅ `tool` · <first line of stderr>") which leaked downstream-tool English into the chat AND failed gracefully on failure with "failed". New form yields a <details type="tool_calls" done="true"> block OWUI renders natively, containing the structured tool_call + tool_result envelope shaped to the OpenAI streaming protocol (function.name, function.arguments, tool_result.success, tool_result.output, tool_result.stderr). The only literal characters at this layer are universal status symbols (✅ / ⚠️) and the cross-locale tool name. 5. NO HARDCODED TOPIC EXCLUSIONS in the router or followup templates (operator-corrected mid-session: an earlier draft of this commit baked "decline weather/time/news/ etc." into _ROUTER_SYSTEM and the OWUI followup template -- operator clarified that MiOS-Agent is BOTH an Agentic-OS AND a generalized AI agent, so no per-topic deny-lists belong in source). Followup template now reads: "grounded in the CURRENT chat content and the operator's CURRENT system state" -- pure context-grounded suggestion, no static filter. Pipe deploy chain reminder (saved as agent memory this session): edit /usr/share/mios/owui/pipes/<file>.py -> mios-owui-install-pipe (pushes into webui.db function row) -> systemctl restart mios-open-webui. Skipping the installer step = container restart with no observable code change because OWUI loads pipes from the DB, not the filesystem. Quadlet deploy chain reminder: edit C:\MiOS\etc\containers\ systemd\<name>.container -> cp to live /etc/containers/ systemd/ -> automation/15-render-quadlets.sh to resolve ${MIOS_*:-default} placeholders to literals -> systemctl daemon-reload + restart. Raw cp of source-with-placeholders fails the unit at load time. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 0f0dddd commit 3264f0f

2 files changed

Lines changed: 83 additions & 38 deletions

File tree

etc/containers/systemd/mios-open-webui.container

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ Environment=ENABLE_AUTOCOMPLETE_GENERATION=True
207207
# for emphasis instead of double-hyphen.
208208
Environment="TITLE_GENERATION_PROMPT_TEMPLATE=Output a 3 to 6 word title for this chat, in the same language the chat used. Title only, no quotes, no preamble, no thinking. Chat:\n{{MESSAGES:END:2}}"
209209
Environment="TAGS_GENERATION_PROMPT_TEMPLATE=Output 1 to 3 short topic tags for this chat (single words or hyphenated, in the chat language). JSON only: {\"tags\":[\"\",\"\"]}. Chat:\n{{MESSAGES:END:4}}"
210-
Environment="FOLLOW_UP_GENERATION_PROMPT_TEMPLATE=Suggest 3 short follow up actions the user might want next, in the same language and tone the chat used. Each is a single phrase or short imperative the user could click. JSON only: {\"follow_ups\":[\"\",\"\",\"\"]}. Chat:\n{{MESSAGES:END:6}}"
210+
Environment="FOLLOW_UP_GENERATION_PROMPT_TEMPLATE=Suggest 3 short follow up actions the user might want next, grounded in the CURRENT chat content and the operator's CURRENT system state. Each is a single phrase or short imperative the user could click, in the same language and tone the chat used. JSON only: {\"follow_ups\":[\"\",\"\",\"\"]}. Chat:\n{{MESSAGES:END:6}}"
211211
Environment="AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE=Continue the user partial input naturally, in their language. Output ONLY the completion text, no quotes, no preamble. Input so far: {{PROMPT}}"
212212

213213
# Identity. UID 817 (mios-open-webui)

usr/share/mios/owui/pipes/mios_agent_pipe.py

Lines changed: 82 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import json
3939
import os
4040
import re
41+
import shlex
4142
import asyncio
4243
import time
4344
from typing import AsyncGenerator, Awaitable, Callable, Optional
@@ -1015,31 +1016,46 @@ def _render_tool_history_for_compose(self, msgs: list[dict]) -> str:
10151016
' several tools. Emit\n'
10161017
' {"action":"agent","reason":"<short>"}\n'
10171018
"\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'
10341047
"\n"
10351048
"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"
10381054
" 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"
10431059
"- Mirror the user's language in `reply` fields.\n"
10441060
"- Output JSON ONLY -- no preamble, no markdown, no commentary."
10451061
)
@@ -1129,7 +1145,14 @@ async def _dispatch_mios_verb(
11291145
ea = " ".join(shlex.quote(str(a)) for a in extra_args)
11301146
cmd = f"{env_prefix}mios-windows launch {shlex.quote(name)} {ea}"
11311147
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"
11331156
elif tool == "launch_app":
11341157
cmd = f"mios-launch {shlex.quote(str(args.get('name', '')))}"
11351158
elif tool == "focus_window":
@@ -1925,20 +1948,42 @@ async def pipe(
19251948
result = json.loads(result_json)
19261949
except json.JSONDecodeError:
19271950
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+
)
19421987
return
19431988
elif action == "chat":
19441989
reply = str(verdict.get("reply", "")).strip()

0 commit comments

Comments
 (0)