Skip to content

Commit b13c4d3

Browse files
ZD Studiosclaude
andcommitted
feat: hermes embedded in hub (proxy), Team orchestration (try-merge), auto-update, robust start
- hermes-proxy (:8792): same frame-stripping proxy as openclaw, so hermes' full dashboard + settings embed in the hub too. Both openclaw AND hermes are now configurable inside the hub. - Team target (try-merge): one assistant that plans, delegates subtasks to opencode/crewai/ claude-code via CALL directives, and synthesizes one answer. New ✦ Team chat chip. - updates.auto_update: on 'aios start', auto git-pull + reinstall changed deps when the repo updated (was check-only). Default on. - aios start hardened: BrokenPipeError no longer surfaces as a non-zero exit when output is piped (the '255' users saw). start/stop already accept multiple services. - hermes-proxy in config/START_ORDER/interconnect/health; /api/peers exposes hermes-embed. Verified on Windows: hermes-proxy strips XFO and serves hermes UI; team target routes+synthesizes (graceful no-key); aios start exits 0; both embeds exposed; dashboard has 6 chat targets. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent d0e40e5 commit b13c4d3

5 files changed

Lines changed: 98 additions & 12 deletions

File tree

README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,12 +157,15 @@ Plus two integrations that glue it together:
157157
| `aios url` | Print the Control Room + service URLs. |
158158

159159
**In the Control Room** (`http://127.0.0.1:8787/`) you can:
160-
- **Chat** with any agent (Brain, CrewAI, opencode, claude-code) or broadcast to **All**; agents reach each other via the hub.
161-
- **Open openclaw's full control plane inside the hub** — channels, **connectors**, model providers, **MCP servers**, skills, plugins, **automations/cron**, and sessions — embedded via a frame-stripping proxy (openclaw normally blocks embedding).
160+
- **Chat** with any agent (Brain, CrewAI, opencode, claude-code) or broadcast to **All**.
161+
- **✦ Team** — one assistant that orchestrates the whole team: the Brain plans, delegates subtasks to the specialist agents, and synthesizes one answer (the practical "merge").
162+
- **Configure openclaw AND hermes fully inside the hub** — both control-UIs are embedded via frame-stripping proxies, so you get channels, **connectors**, model providers, **MCP servers**, skills, plugins, **automations/cron**, and sessions right in the hub (they normally block embedding).
162163
- **Automations** — schedule prompts to run against any agent every N minutes (daily digests, checks).
163164
- **Settings** — edit provider/key/model, channel tokens, and `aios.config.yaml`; it re-renders into every agent, no terminal needed.
164165
- **Themes** — Light, Dark, Midnight, Slate, Rose.
165166

167+
**Auto-updates:** `aios start` now auto-runs `git pull` + reinstalls changed deps when the repo has updates (`updates.auto_update: true`). Turn it off in `aios.config.yaml`.
168+
166169
## ⚙️ Configuration
167170

168171
- **`.env`** — secrets only (`AIOS_LLM_PROVIDER`, `AIOS_LLM_API_KEY`, `AIOS_DEFAULT_MODEL`, optional channel tokens). Git-ignored.

aios.config.example.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ services:
3232
openclaw_proxy: # frame-stripping proxy so openclaw's control-UI embeds in the hub
3333
enabled: true
3434
port: 8791
35+
hermes_proxy: # frame-stripping proxy so hermes' dashboard embeds in the hub
36+
enabled: true
37+
port: 8792
3538
hub: # ★ AIOS Hub — unified dashboard + interconnect (the Control Room)
3639
enabled: true
3740
port: 8787
@@ -46,6 +49,7 @@ openui:
4649

4750
updates:
4851
check_on_start: true # on `aios start`, check the repo for updates and notify
52+
auto_update: true # on `aios start`, auto git-pull + reinstall if the repo changed
4953

5054
# Health-check URLs aios waits on during `start` and checks in `status`/`smoke`.
5155
health:
@@ -55,5 +59,6 @@ health:
5559
crewai: http://127.0.0.1:4788/health
5660
claudecode: http://127.0.0.1:8000/health
5761
openclaw_proxy: http://127.0.0.1:8791/
62+
hermes_proxy: http://127.0.0.1:8792/
5863
hub: http://127.0.0.1:8787/health
5964
openclaw_os: http://127.0.0.1:18789/plugins/openclawos/

aios.py

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -427,7 +427,7 @@ def service_specs(cfg: dict) -> dict:
427427
"health": cfg_get(cfg, "health.claudecode", f"http://127.0.0.1:{port}/health"),
428428
"tool": "uv",
429429
}
430-
# Frame-stripping reverse proxy so openclaw's control-UI embeds in the hub.
430+
# Frame-stripping reverse proxies so openclaw + hermes control-UIs embed in the hub.
431431
if cl and cfg_get(cfg, "services.openclaw_proxy.enabled", True):
432432
pport = int(cfg_get(cfg, "services.openclaw_proxy.port", 8791))
433433
oport = int(cfg_get(cfg, "services.openclaw.port", 18789))
@@ -440,6 +440,18 @@ def service_specs(cfg: dict) -> dict:
440440
"tool": "python",
441441
"env": {"AIOS_PROXY_PORT": str(pport), "AIOS_PROXY_TARGET": f"http://127.0.0.1:{oport}"},
442442
}
443+
if hm and cfg_get(cfg, "services.hermes_proxy.enabled", True):
444+
pport = int(cfg_get(cfg, "services.hermes_proxy.port", 8792))
445+
hp = int(cfg_get(cfg, "services.hermes.port", 9119))
446+
specs["hermes-proxy"] = {
447+
"enabled": True,
448+
"cwd": ROOT,
449+
"cmd": [sys.executable, str(ROOT / "aios_proxy.py")],
450+
"port": pport,
451+
"health": cfg_get(cfg, "health.hermes_proxy", f"http://127.0.0.1:{pport}/"),
452+
"tool": "python",
453+
"env": {"AIOS_PROXY_PORT": str(pport), "AIOS_PROXY_TARGET": f"http://127.0.0.1:{hp}"},
454+
}
443455
# The AIOS Hub — unified dashboard + interconnect. Always available (stdlib).
444456
hport = int(cfg_get(cfg, "services.hub.port", 8787))
445457
specs["hub"] = {
@@ -458,8 +470,8 @@ def service_specs(cfg: dict) -> dict:
458470
return specs
459471

460472

461-
START_ORDER = ["opencode", "hermes", "hermes-gateway", "openclaw", "openclaw-proxy",
462-
"crewai", "claudecode", "hub"]
473+
START_ORDER = ["opencode", "hermes", "hermes-gateway", "hermes-proxy", "openclaw",
474+
"openclaw-proxy", "crewai", "claudecode", "hub"]
463475

464476

465477
def openclaw_env() -> dict:
@@ -477,6 +489,7 @@ def interconnect_env(cfg: dict) -> dict:
477489
crp = cfg_get(cfg, "services.crewai.port", 4788)
478490
ccp = cfg_get(cfg, "services.claudecode.port", 8000)
479491
pxp = cfg_get(cfg, "services.openclaw_proxy.port", 8791)
492+
hxp = cfg_get(cfg, "services.hermes_proxy.port", 8792)
480493
hbp = cfg_get(cfg, "services.hub.port", 8787)
481494
return {
482495
"AIOS_ROOT": str(ROOT),
@@ -490,6 +503,7 @@ def interconnect_env(cfg: dict) -> dict:
490503
"AIOS_CREWAI_PORT": str(crp),
491504
"AIOS_CLAUDECODE_URL": f"http://127.0.0.1:{ccp}",
492505
"AIOS_OPENCLAWPROXY_URL": f"http://127.0.0.1:{pxp}/",
506+
"AIOS_HERMESPROXY_URL": f"http://127.0.0.1:{hxp}/",
493507
"AIOS_OPENCLAWOS_URL": f"http://127.0.0.1:{clp}/plugins/openclawos/",
494508
}
495509

@@ -1156,7 +1170,12 @@ def wire_openclaw_os(quiet=False):
11561170
def cmd_start(args):
11571171
cfg = load_config()
11581172
secrets = load_env(ENV_PATH)
1159-
if cfg_get(cfg, "updates.check_on_start", True):
1173+
if cfg_get(cfg, "updates.auto_update", False):
1174+
if _git_pull() is True: # new commits arrived → refresh deps/config
1175+
_install_all(cfg)
1176+
render_native(cfg, secrets)
1177+
cfg = load_config()
1178+
elif cfg_get(cfg, "updates.check_on_start", True):
11601179
_check_updates_notice()
11611180
if not secrets.get("AIOS_LLM_API_KEY") and not any(secrets.get(v) for v in PROVIDER_VAR.values()):
11621181
warn("no model API key set — the stack runs, but agents need a key to answer "
@@ -1622,3 +1641,10 @@ def main(argv=None):
16221641
except KeyboardInterrupt:
16231642
say("\ninterrupted")
16241643
sys.exit(130)
1644+
except BrokenPipeError:
1645+
# output was piped into head/Select-Object -First etc.; not an error
1646+
try:
1647+
sys.stdout.close()
1648+
except Exception:
1649+
pass
1650+
sys.exit(0)

aios_hub.py

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ def run_aios(*a, background=False):
9191
}
9292
CLAUDECODE = os.environ.get("AIOS_CLAUDECODE_URL", "http://127.0.0.1:8000").rstrip("/")
9393
OPENCLAW_EMBED = os.environ.get("AIOS_OPENCLAWPROXY_URL", "http://127.0.0.1:8791/")
94+
HERMES_EMBED = os.environ.get("AIOS_HERMESPROXY_URL", "http://127.0.0.1:8792/")
9495
SCHEDULES_FILE = ROOT / ".aios" / "schedules.json"
9596

9697

@@ -195,12 +196,62 @@ def route(target: str, message: str, history: list | None = None) -> str:
195196
return ask_opencode(message)
196197
if target in ("claudecode", "claude-code"):
197198
return ask_claudecode(message, history)
199+
if target in ("team", "auto", "merge"):
200+
return run_team(message, history)
198201
if target == "all":
199202
parts = []
200203
for t in ("brain", "crewai", "opencode", "claudecode"):
201204
parts.append(f"### {t}\n{route(t, message, history)}")
202205
return "\n\n".join(parts)
203-
return f"⚠️ Unknown target '{target}'. Use brain | crewai | opencode | claudecode | all."
206+
return f"⚠️ Unknown target '{target}'. Use brain | team | crewai | opencode | claudecode | all."
207+
208+
209+
# --------------------------------------------------------------------------- #
210+
# "Team" — a single agent that orchestrates the others (the practical merge) #
211+
# --------------------------------------------------------------------------- #
212+
AGENT_TOOLS = {
213+
"opencode": "writing/editing/running code, repos, technical build tasks",
214+
"crewai": "multi-step research or workflows that benefit from a crew of roles",
215+
"claudecode": "Claude Code — coding with the Claude Code CLI",
216+
}
217+
218+
219+
def run_team(message: str, history: list | None = None) -> str:
220+
"""One agent, backed by the whole team: the Brain plans, delegates subtasks to
221+
the specialist agents, then synthesizes one answer. Delegation format the model
222+
emits: lines like `CALL opencode: <subtask>` (or `ANSWER: ...` to reply directly)."""
223+
roster = "\n".join(f"- {k}: {v}" for k, v in AGENT_TOOLS.items())
224+
plan_sys = (
225+
"You are The AI OS — one assistant backed by a team of specialist agents. "
226+
"Decide how to handle the user's request. You may delegate subtasks to agents by writing "
227+
"one directive per line in the form `CALL <agent>: <subtask>`. Available agents:\n" + roster +
228+
"\nIf you can answer directly with no agent, write `ANSWER: <your answer>`. "
229+
"Only delegate when it genuinely helps. Output directives only.")
230+
plan = llm_chat((history or []) + [{"role": "user", "content": message}], system=plan_sys)
231+
if plan.lstrip().upper().startswith("ANSWER:"):
232+
return plan.split(":", 1)[1].strip()
233+
234+
calls = []
235+
for line in plan.splitlines():
236+
s = line.strip()
237+
if s.upper().startswith("CALL "):
238+
body = s[5:]
239+
agent, _, sub = body.partition(":")
240+
agent = agent.strip().lower()
241+
if agent in AGENT_TOOLS and sub.strip():
242+
calls.append((agent, sub.strip()))
243+
if not calls:
244+
# model didn't delegate cleanly — just answer as the Brain
245+
return route("brain", message, history)
246+
247+
results = []
248+
for agent, sub in calls[:3]: # cap fan-out
249+
results.append(f"[{agent}] {sub}\n{route(agent, sub)}")
250+
synth_sys = ("You are The AI OS. Synthesize a single, clear answer to the user's request from the "
251+
"agent results below. Don't mention the internal delegation unless useful.")
252+
joined = "\n\n".join(results)
253+
return llm_chat([{"role": "user", "content": f"Request: {message}\n\nAgent results:\n{joined}"}],
254+
system=synth_sys)
204255

205256

206257
# --------------------------------------------------------------------------- #
@@ -306,7 +357,7 @@ def do_GET(self):
306357
elif self.path == "/api/services":
307358
self._send(200, services_status())
308359
elif self.path == "/api/peers":
309-
self._send(200, {**PEERS, "openclaw-embed": OPENCLAW_EMBED})
360+
self._send(200, {**PEERS, "openclaw-embed": OPENCLAW_EMBED, "hermes-embed": HERMES_EMBED})
310361
elif self.path == "/api/schedules":
311362
self._send(200, load_schedules())
312363
elif self.path == "/api/config":

docs/dashboard.html

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -177,11 +177,12 @@
177177
<div id="chat" class="view active">
178178
<div class="msgs" id="msgs">
179179
<div class="msg"><div class="av"></div><div><div class="who">AIOS Brain</div>
180-
<div class="bubble">Welcome to your <b>AI OS workspace</b>. Ask anything, or pick a target below — <b>Brain</b> (orchestrator), <b>CrewAI</b> (crews), <b>opencode</b> (coding), or <b>All</b>. Open any agent in the sidebar to use its own panel, or head to <b>Settings</b> to configure everything.</div></div></div>
180+
<div class="bubble">Welcome to your <b>AI OS workspace</b>. Ask anything, or pick a target below — <b>Brain</b> (orchestrator), <b>✦ Team</b> (one assistant that delegates to all the others), <b>CrewAI</b>, <b>opencode</b>, <b>claude-code</b>, or <b>All</b>. Open <b>hermes</b> or <b>openclaw</b> in the sidebar to configure them fully inside the hub — channels, connectors, providers, MCP, skills, cron.</div></div></div>
181181
</div>
182182
<div class="composer">
183183
<div class="targets" id="targets">
184184
<button class="chip active" data-t="brain">◆ Brain</button>
185+
<button class="chip" data-t="team">✦ Team</button>
185186
<button class="chip" data-t="crewai">👥 CrewAI</button>
186187
<button class="chip" data-t="opencode">⌨️ opencode</button>
187188
<button class="chip" data-t="claudecode">◈ claude-code</button>
@@ -195,7 +196,7 @@
195196
</div>
196197

197198
<!-- IFRAME AGENTS -->
198-
<div id="hermes" class="view"><div class="vhead"><h2>☤ hermes</h2><span class="sub">autonomous agent · dashboard</span><div class="right"><button class="btn sm" onclick="act('restart','hermes')">↻ Restart</button><a class="btn sm" id="hermesNew" target="_blank">Open ↗</a></div></div><div class="embed"><iframe id="hermesFrame"></iframe></div></div>
199+
<div id="hermes" class="view"><div class="vhead"><h2>☤ hermes</h2><span class="sub">autonomous agent · full dashboard &amp; settings</span><div class="right"><button class="btn sm" onclick="act('restart','hermes')">↻ Restart</button><a class="btn sm" id="hermesNew" target="_blank">Open ↗</a></div></div><div class="embed"><iframe id="hermesFrame"></iframe></div></div>
199200
<div id="openclawos" class="view"><div class="vhead"><h2>🪟 openclaw-os</h2><span class="sub">openclaw's generative-UI dashboard</span><div class="right"><a class="btn sm" id="osNew" target="_blank">Open ↗</a></div></div><div class="embed"><iframe id="osFrame"></iframe></div></div>
200201
<div id="openclaw" class="view"><div class="vhead"><h2>🦞 openclaw</h2><span class="sub">channels · connectors · providers · skills · MCP · automations · sessions</span><div class="right"><button class="btn sm" onclick="act('restart','openclaw')">↻ Restart</button><a class="btn sm" id="clawNew" target="_blank">Open ↗</a></div></div><div class="embed"><iframe id="clawFrame"></iframe></div></div>
201202

@@ -266,7 +267,7 @@ <h3>🧩 Advanced — aios.config.yaml</h3>
266267

267268
<script>
268269
const ROLES={opencode:"coding engine",hermes:"autonomous agent",openclaw:"channel gateway",crewai:"multi-agent crews",claudecode:"Claude Code API","openclaw-os":"dashboard",hub:"brain + interconnect"};
269-
const ICON={opencode:"⌨️",hermes:"☤",openclaw:"🦞",crewai:"👥",claudecode:"◈","openclaw-os":"🪟",hub:"◆"};
270+
const ICON={opencode:"⌨️",hermes:"☤",openclaw:"🦞",crewai:"👥",claudecode:"◈","openclaw-os":"🪟",hub:"◆",brain:"◆",team:"✦",all:"🌐"};
270271
let target="brain", peers={}, svc={}, history=[];
271272

272273
// themes
@@ -280,7 +281,7 @@ <h3>🧩 Advanced — aios.config.yaml</h3>
280281
document.querySelectorAll('.view').forEach(x=>x.classList.toggle('active',x.id===v));
281282
document.querySelectorAll('.nav').forEach(b=>b.classList.toggle('active',b.dataset.v===v));
282283
document.querySelector('aside').classList.remove('open');
283-
if(v==='hermes')frame('hermesFrame','hermesNew',peers.hermes);
284+
if(v==='hermes')frame('hermesFrame','hermesNew',peers['hermes-embed']||peers.hermes);
284285
if(v==='openclawos')frame('osFrame','osNew',peers['openclaw-os']);
285286
if(v==='openclaw')frame('clawFrame','clawNew',peers['openclaw-embed']||peers.openclaw);
286287
if(v==='opencode')agentPanel('opencode','opencodeBody','The headless coding-agent engine. Chat with it from the Chat tab (target “opencode”) or call its HTTP API/SDK.',peers.opencode);

0 commit comments

Comments
 (0)