@bitseek/hermes-webui 0.1.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +213 -0
- package/bin/hermes-webui.mjs +588 -0
- package/package.json +25 -0
- package/scripts/sync-vendor.mjs +74 -0
- package/templates/launchd/com.bitseek.hermes-webui.plist +21 -0
- package/templates/systemd/hermes-webui.service +13 -0
- package/templates/windows/hermes-webui-task.ps1 +3 -0
- package/vendor/agent-frontend-shell/.bitseek-source.json +6 -0
- package/vendor/agent-frontend-shell/.dockerignore +7 -0
- package/vendor/agent-frontend-shell/.env.docker.example +89 -0
- package/vendor/agent-frontend-shell/.env.example +34 -0
- package/vendor/agent-frontend-shell/.github/FUNDING.yml +3 -0
- package/vendor/agent-frontend-shell/.github/workflows/browser-smoke.yml +42 -0
- package/vendor/agent-frontend-shell/.github/workflows/docker-smoke.yml +233 -0
- package/vendor/agent-frontend-shell/.github/workflows/native-windows-startup.yml +132 -0
- package/vendor/agent-frontend-shell/.github/workflows/release.yml +57 -0
- package/vendor/agent-frontend-shell/.github/workflows/tests.yml +88 -0
- package/vendor/agent-frontend-shell/.vscode/launch.json +59 -0
- package/vendor/agent-frontend-shell/.vscode/settings.json +13 -0
- package/vendor/agent-frontend-shell/AGENTS.md +80 -0
- package/vendor/agent-frontend-shell/ARCHITECTURE.md +1658 -0
- package/vendor/agent-frontend-shell/BUGS.md +52 -0
- package/vendor/agent-frontend-shell/CHANGELOG.md +7295 -0
- package/vendor/agent-frontend-shell/CONTRIBUTING.md +205 -0
- package/vendor/agent-frontend-shell/CONTRIBUTORS.md +107 -0
- package/vendor/agent-frontend-shell/DESIGN.md +173 -0
- package/vendor/agent-frontend-shell/Dockerfile +91 -0
- package/vendor/agent-frontend-shell/LICENSE +21 -0
- package/vendor/agent-frontend-shell/README-CUSTOM.md +76 -0
- package/vendor/agent-frontend-shell/README.md +705 -0
- package/vendor/agent-frontend-shell/ROADMAP.md +351 -0
- package/vendor/agent-frontend-shell/SPRINTS.md +147 -0
- package/vendor/agent-frontend-shell/TESTING.md +1932 -0
- package/vendor/agent-frontend-shell/THEMES.md +170 -0
- package/vendor/agent-frontend-shell/api/__init__.py +1 -0
- package/vendor/agent-frontend-shell/api/agent_health.py +392 -0
- package/vendor/agent-frontend-shell/api/agent_sessions.py +782 -0
- package/vendor/agent-frontend-shell/api/auth.py +592 -0
- package/vendor/agent-frontend-shell/api/background.py +87 -0
- package/vendor/agent-frontend-shell/api/clarify.py +238 -0
- package/vendor/agent-frontend-shell/api/commands.py +124 -0
- package/vendor/agent-frontend-shell/api/compression_anchor.py +134 -0
- package/vendor/agent-frontend-shell/api/config.py +5178 -0
- package/vendor/agent-frontend-shell/api/dashboard_probe.py +255 -0
- package/vendor/agent-frontend-shell/api/extensions.py +253 -0
- package/vendor/agent-frontend-shell/api/gateway_chat.py +435 -0
- package/vendor/agent-frontend-shell/api/gateway_watcher.py +230 -0
- package/vendor/agent-frontend-shell/api/goals.py +608 -0
- package/vendor/agent-frontend-shell/api/helpers.py +474 -0
- package/vendor/agent-frontend-shell/api/kanban_bridge.py +1255 -0
- package/vendor/agent-frontend-shell/api/metering.py +194 -0
- package/vendor/agent-frontend-shell/api/models.py +4210 -0
- package/vendor/agent-frontend-shell/api/oauth.py +770 -0
- package/vendor/agent-frontend-shell/api/onboarding.py +1046 -0
- package/vendor/agent-frontend-shell/api/passkeys.py +365 -0
- package/vendor/agent-frontend-shell/api/profiles.py +1499 -0
- package/vendor/agent-frontend-shell/api/providers.py +2175 -0
- package/vendor/agent-frontend-shell/api/request_diagnostics.py +160 -0
- package/vendor/agent-frontend-shell/api/rollback.py +320 -0
- package/vendor/agent-frontend-shell/api/routes.py +13990 -0
- package/vendor/agent-frontend-shell/api/run_journal.py +284 -0
- package/vendor/agent-frontend-shell/api/runner_client.py +156 -0
- package/vendor/agent-frontend-shell/api/runtime_adapter.py +431 -0
- package/vendor/agent-frontend-shell/api/session_discoverability.py +640 -0
- package/vendor/agent-frontend-shell/api/session_events.py +45 -0
- package/vendor/agent-frontend-shell/api/session_lifecycle.py +208 -0
- package/vendor/agent-frontend-shell/api/session_ops.py +207 -0
- package/vendor/agent-frontend-shell/api/session_recovery.py +655 -0
- package/vendor/agent-frontend-shell/api/skill_usage.py +32 -0
- package/vendor/agent-frontend-shell/api/startup.py +128 -0
- package/vendor/agent-frontend-shell/api/state_sync.py +187 -0
- package/vendor/agent-frontend-shell/api/streaming.py +7048 -0
- package/vendor/agent-frontend-shell/api/system_health.py +167 -0
- package/vendor/agent-frontend-shell/api/terminal.py +410 -0
- package/vendor/agent-frontend-shell/api/turn_journal.py +214 -0
- package/vendor/agent-frontend-shell/api/updates.py +1261 -0
- package/vendor/agent-frontend-shell/api/upload.py +322 -0
- package/vendor/agent-frontend-shell/api/usage.py +26 -0
- package/vendor/agent-frontend-shell/api/workspace.py +867 -0
- package/vendor/agent-frontend-shell/api/workspace_git.py +1261 -0
- package/vendor/agent-frontend-shell/api/worktrees.py +357 -0
- package/vendor/agent-frontend-shell/bootstrap.py +492 -0
- package/vendor/agent-frontend-shell/ctl.sh +427 -0
- package/vendor/agent-frontend-shell/docker-compose.custom.yml +26 -0
- package/vendor/agent-frontend-shell/docker-compose.three-container.yml +168 -0
- package/vendor/agent-frontend-shell/docker-compose.two-container.yml +147 -0
- package/vendor/agent-frontend-shell/docker-compose.yml +57 -0
- package/vendor/agent-frontend-shell/docker_init.bash +459 -0
- package/vendor/agent-frontend-shell/docs/CONTRACTS.md +207 -0
- package/vendor/agent-frontend-shell/docs/EXTENSIONS.md +212 -0
- package/vendor/agent-frontend-shell/docs/ISSUES.md +23 -0
- package/vendor/agent-frontend-shell/docs/UIUX-GUIDE.md +196 -0
- package/vendor/agent-frontend-shell/docs/advanced-chat-setup.md +83 -0
- package/vendor/agent-frontend-shell/docs/docker.md +337 -0
- package/vendor/agent-frontend-shell/docs/onboarding-agent-checklist.md +207 -0
- package/vendor/agent-frontend-shell/docs/onboarding.md +202 -0
- package/vendor/agent-frontend-shell/docs/remote-access.md +75 -0
- package/vendor/agent-frontend-shell/docs/rfcs/README.md +53 -0
- package/vendor/agent-frontend-shell/docs/rfcs/agent-source-boundary.md +70 -0
- package/vendor/agent-frontend-shell/docs/rfcs/canonical-session-resolution.md +124 -0
- package/vendor/agent-frontend-shell/docs/rfcs/hermes-run-adapter-contract.md +1079 -0
- package/vendor/agent-frontend-shell/docs/rfcs/turn-journal.md +195 -0
- package/vendor/agent-frontend-shell/docs/rfcs/webui-run-state-consistency-contract.md +157 -0
- package/vendor/agent-frontend-shell/docs/supervisor.md +280 -0
- package/vendor/agent-frontend-shell/docs/troubleshooting.md +132 -0
- package/vendor/agent-frontend-shell/docs/ui-ux/index.html +863 -0
- package/vendor/agent-frontend-shell/docs/ui-ux/two-stage-proposal.html +768 -0
- package/vendor/agent-frontend-shell/docs/why-hermes.md +489 -0
- package/vendor/agent-frontend-shell/docs/workspace-git.md +92 -0
- package/vendor/agent-frontend-shell/docs/wsl-autostart.md +126 -0
- package/vendor/agent-frontend-shell/eslint.runtime-guard.config.mjs +35 -0
- package/vendor/agent-frontend-shell/extensions/bitseek-design-system.md +330 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/apple-touch-icon.png +0 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/empty-logo.svg +739 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/favicon-192.png +0 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/favicon-32.png +0 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/favicon-512.png +0 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/favicon-512.svg +745 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/favicon.ico +0 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/favicon.svg +745 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/titlebar-icon-v2.svg +751 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/titlebar-icon-v3.svg +739 -0
- package/vendor/agent-frontend-shell/extensions/branding/assets/titlebar-icon.svg +745 -0
- package/vendor/agent-frontend-shell/extensions/branding/branding.js +112 -0
- package/vendor/agent-frontend-shell/extensions/branding/config.json +14 -0
- package/vendor/agent-frontend-shell/extensions/branding/manifest.json +53 -0
- package/vendor/agent-frontend-shell/extensions/index.js +67 -0
- package/vendor/agent-frontend-shell/extensions/loader/hermes-loader.js +77 -0
- package/vendor/agent-frontend-shell/extensions/manifest.json +16 -0
- package/vendor/agent-frontend-shell/extensions/pages/ai-teammates/page.css +333 -0
- package/vendor/agent-frontend-shell/extensions/pages/ai-teammates/page.js +487 -0
- package/vendor/agent-frontend-shell/extensions/pages/manifest.json +6 -0
- package/vendor/agent-frontend-shell/extensions/pages/registry.css +56 -0
- package/vendor/agent-frontend-shell/extensions/pages/registry.js +302 -0
- package/vendor/agent-frontend-shell/extensions/themes/bitseek/index.css +93 -0
- package/vendor/agent-frontend-shell/extensions/themes/bitseek/index.js +98 -0
- package/vendor/agent-frontend-shell/install.sh +63 -0
- package/vendor/agent-frontend-shell/mcp_server.py +567 -0
- package/vendor/agent-frontend-shell/package.json +12 -0
- package/vendor/agent-frontend-shell/pyproject.toml +56 -0
- package/vendor/agent-frontend-shell/pytest.ini +3 -0
- package/vendor/agent-frontend-shell/requirements.txt +5 -0
- package/vendor/agent-frontend-shell/server.py +624 -0
- package/vendor/agent-frontend-shell/start.ps1 +210 -0
- package/vendor/agent-frontend-shell/start.sh +65 -0
- package/vendor/agent-frontend-shell/static/apple-touch-icon.png +0 -0
- package/vendor/agent-frontend-shell/static/boot.js +1990 -0
- package/vendor/agent-frontend-shell/static/commands.js +1402 -0
- package/vendor/agent-frontend-shell/static/favicon-192.png +0 -0
- package/vendor/agent-frontend-shell/static/favicon-32.png +0 -0
- package/vendor/agent-frontend-shell/static/favicon-512.png +0 -0
- package/vendor/agent-frontend-shell/static/favicon-512.svg +18 -0
- package/vendor/agent-frontend-shell/static/favicon.ico +0 -0
- package/vendor/agent-frontend-shell/static/favicon.svg +20 -0
- package/vendor/agent-frontend-shell/static/i18n.js +15389 -0
- package/vendor/agent-frontend-shell/static/icons.js +92 -0
- package/vendor/agent-frontend-shell/static/index.html +1506 -0
- package/vendor/agent-frontend-shell/static/login.js +177 -0
- package/vendor/agent-frontend-shell/static/manifest.json +53 -0
- package/vendor/agent-frontend-shell/static/messages.js +3521 -0
- package/vendor/agent-frontend-shell/static/onboarding.js +800 -0
- package/vendor/agent-frontend-shell/static/panels.js +7995 -0
- package/vendor/agent-frontend-shell/static/pwa-startup.js +83 -0
- package/vendor/agent-frontend-shell/static/sessions.js +5165 -0
- package/vendor/agent-frontend-shell/static/style.css +4774 -0
- package/vendor/agent-frontend-shell/static/sw.js +173 -0
- package/vendor/agent-frontend-shell/static/terminal.js +632 -0
- package/vendor/agent-frontend-shell/static/ui.js +8997 -0
- package/vendor/agent-frontend-shell/static/vendor/js-yaml/4.1.0/js-yaml.min.js +2 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_AMS-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_AMS-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_AMS-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Bold.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Bold.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Bold.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Bold.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-BoldItalic.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Italic.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Italic.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Italic.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-BoldItalic.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-Italic.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-Italic.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-Italic.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Bold.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Italic.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Script-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Script-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Script-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size1-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size1-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size1-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size2-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size2-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size2-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size3-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size3-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size3-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size4-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size4-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size4-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Typewriter-Regular.woff +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/katex.min.css +1 -0
- package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/katex.min.js +1 -0
- package/vendor/agent-frontend-shell/static/vendor/smd.min.js +29 -0
- package/vendor/agent-frontend-shell/static/workspace.js +680 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"""Append-only WebUI run event journal helpers.
|
|
2
|
+
|
|
3
|
+
This is the first #1925 journal/replay slice. It mirrors SSE events emitted by
|
|
4
|
+
the existing in-process streaming path without changing execution ownership.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import re
|
|
11
|
+
import threading
|
|
12
|
+
import time
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Iterable
|
|
15
|
+
|
|
16
|
+
RUN_JOURNAL_DIR_NAME = "_run_journal"
|
|
17
|
+
_SAFE_ID_RE = re.compile(r"^[A-Za-z0-9_.-]+$")
|
|
18
|
+
_WRITER_LOCKS: dict[tuple[str, str, str], threading.Lock] = {}
|
|
19
|
+
_WRITER_LOCKS_GUARD = threading.Lock()
|
|
20
|
+
_TERMINAL_SSE_EVENTS = {"done", "cancel", "apperror", "error", "stream_end"}
|
|
21
|
+
_FSYNC_MODE_ENV = "HERMES_WEBUI_RUN_JOURNAL_FSYNC"
|
|
22
|
+
_FSYNC_MODE_EAGER = "eager"
|
|
23
|
+
_FSYNC_MODE_TERMINAL_ONLY = "terminal-only"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _default_session_dir() -> Path:
|
|
27
|
+
from api.models import SESSION_DIR
|
|
28
|
+
|
|
29
|
+
return Path(SESSION_DIR)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _validate_id(value: str, field: str) -> str:
|
|
33
|
+
cleaned = str(value or "").strip()
|
|
34
|
+
if not cleaned or "/" in cleaned or "\\" in cleaned or not _SAFE_ID_RE.fullmatch(cleaned):
|
|
35
|
+
raise ValueError(f"invalid {field}")
|
|
36
|
+
return cleaned
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _run_path(session_id: str, run_id: str, session_dir: Path | None = None) -> Path:
|
|
40
|
+
sid = _validate_id(session_id, "session_id")
|
|
41
|
+
rid = _validate_id(run_id, "run_id")
|
|
42
|
+
root = Path(session_dir) if session_dir is not None else _default_session_dir()
|
|
43
|
+
return root / RUN_JOURNAL_DIR_NAME / sid / f"{rid}.jsonl"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _lock_for(path: Path) -> threading.Lock:
|
|
47
|
+
key = (str(path.parent), path.name, str(os.getpid()))
|
|
48
|
+
with _WRITER_LOCKS_GUARD:
|
|
49
|
+
lock = _WRITER_LOCKS.get(key)
|
|
50
|
+
if lock is None:
|
|
51
|
+
lock = threading.Lock()
|
|
52
|
+
_WRITER_LOCKS[key] = lock
|
|
53
|
+
return lock
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _read_jsonl(path: Path) -> tuple[list[dict], list[dict]]:
|
|
57
|
+
events: list[dict] = []
|
|
58
|
+
malformed: list[dict] = []
|
|
59
|
+
try:
|
|
60
|
+
lines = path.read_text(encoding="utf-8").splitlines()
|
|
61
|
+
except FileNotFoundError:
|
|
62
|
+
return events, malformed
|
|
63
|
+
for line_no, raw in enumerate(lines, start=1):
|
|
64
|
+
if not raw.strip():
|
|
65
|
+
continue
|
|
66
|
+
try:
|
|
67
|
+
parsed = json.loads(raw)
|
|
68
|
+
except json.JSONDecodeError:
|
|
69
|
+
malformed.append({"line": line_no, "raw": raw})
|
|
70
|
+
continue
|
|
71
|
+
if isinstance(parsed, dict):
|
|
72
|
+
events.append(parsed)
|
|
73
|
+
else:
|
|
74
|
+
malformed.append({"line": line_no, "raw": raw})
|
|
75
|
+
return events, malformed
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _next_seq(path: Path) -> int:
|
|
79
|
+
events, _malformed = _read_jsonl(path)
|
|
80
|
+
seqs = [int(event.get("seq") or 0) for event in events if isinstance(event.get("seq"), int)]
|
|
81
|
+
return (max(seqs) + 1) if seqs else 1
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _terminal_state_for_event(event_name: str, payload) -> str | None:
|
|
85
|
+
name = str(event_name or "")
|
|
86
|
+
if name == "done" or name == "stream_end":
|
|
87
|
+
return "completed"
|
|
88
|
+
if name == "cancel":
|
|
89
|
+
return "interrupted-by-user"
|
|
90
|
+
if name in {"apperror", "error"}:
|
|
91
|
+
err_type = str((payload or {}).get("type") or "").strip().lower() if isinstance(payload, dict) else ""
|
|
92
|
+
if err_type in {"cancelled", "canceled"}:
|
|
93
|
+
return "interrupted-by-user"
|
|
94
|
+
if err_type == "interrupted":
|
|
95
|
+
return "interrupted-by-crash"
|
|
96
|
+
return "errored"
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _run_journal_fsync_mode() -> str:
|
|
101
|
+
raw = os.environ.get(_FSYNC_MODE_ENV, _FSYNC_MODE_TERMINAL_ONLY)
|
|
102
|
+
mode = str(raw or "").strip().lower()
|
|
103
|
+
if mode in {_FSYNC_MODE_EAGER, _FSYNC_MODE_TERMINAL_ONLY}:
|
|
104
|
+
return mode
|
|
105
|
+
return _FSYNC_MODE_TERMINAL_ONLY
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _should_fsync_event(terminal_state: str | None) -> bool:
|
|
109
|
+
if _run_journal_fsync_mode() == _FSYNC_MODE_EAGER:
|
|
110
|
+
return True
|
|
111
|
+
return bool(terminal_state)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _fsync_parent_dir(path: Path) -> None:
|
|
115
|
+
try:
|
|
116
|
+
dir_fd = os.open(path.parent, getattr(os, "O_DIRECTORY", 0))
|
|
117
|
+
try:
|
|
118
|
+
os.fsync(dir_fd)
|
|
119
|
+
finally:
|
|
120
|
+
os.close(dir_fd)
|
|
121
|
+
except OSError:
|
|
122
|
+
pass
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def append_run_event(
|
|
126
|
+
session_id: str,
|
|
127
|
+
run_id: str,
|
|
128
|
+
event_name: str,
|
|
129
|
+
payload=None,
|
|
130
|
+
*,
|
|
131
|
+
session_dir: Path | None = None,
|
|
132
|
+
seq: int | None = None,
|
|
133
|
+
created_at: float | None = None,
|
|
134
|
+
) -> dict:
|
|
135
|
+
"""Append one durable run event and fsync it according to the journal policy."""
|
|
136
|
+
path = _run_path(session_id, run_id, session_dir=session_dir)
|
|
137
|
+
payload = payload if payload is not None else {}
|
|
138
|
+
event_name = str(event_name or "").strip()
|
|
139
|
+
if not event_name:
|
|
140
|
+
raise ValueError("event_name is required")
|
|
141
|
+
with _lock_for(path):
|
|
142
|
+
assigned_seq = int(seq) if seq is not None else _next_seq(path)
|
|
143
|
+
terminal_state = _terminal_state_for_event(event_name, payload)
|
|
144
|
+
event = {
|
|
145
|
+
"version": 1,
|
|
146
|
+
"event_id": f"{run_id}:{assigned_seq}",
|
|
147
|
+
"seq": assigned_seq,
|
|
148
|
+
"run_id": str(run_id),
|
|
149
|
+
"session_id": str(session_id),
|
|
150
|
+
"event": event_name,
|
|
151
|
+
"type": event_name,
|
|
152
|
+
"created_at": float(created_at if created_at is not None else time.time()),
|
|
153
|
+
"terminal": bool(terminal_state),
|
|
154
|
+
"terminal_state": terminal_state,
|
|
155
|
+
"payload": payload,
|
|
156
|
+
}
|
|
157
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
158
|
+
created_file = not path.exists()
|
|
159
|
+
line = json.dumps(event, ensure_ascii=False, separators=(",", ":")) + "\n"
|
|
160
|
+
fd = os.open(path, os.O_CREAT | os.O_APPEND | os.O_WRONLY, 0o600)
|
|
161
|
+
with os.fdopen(fd, "a", encoding="utf-8") as fh:
|
|
162
|
+
fh.write(line)
|
|
163
|
+
fh.flush()
|
|
164
|
+
if _should_fsync_event(terminal_state):
|
|
165
|
+
os.fsync(fh.fileno())
|
|
166
|
+
if created_file:
|
|
167
|
+
_fsync_parent_dir(path)
|
|
168
|
+
return event
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class RunJournalWriter:
|
|
172
|
+
"""Stateful writer for one WebUI stream/run."""
|
|
173
|
+
|
|
174
|
+
def __init__(self, session_id: str, run_id: str, *, session_dir: Path | None = None):
|
|
175
|
+
self.session_id = _validate_id(session_id, "session_id")
|
|
176
|
+
self.run_id = _validate_id(run_id, "run_id")
|
|
177
|
+
self.session_dir = Path(session_dir) if session_dir is not None else None
|
|
178
|
+
self._path = _run_path(self.session_id, self.run_id, session_dir=self.session_dir)
|
|
179
|
+
self._lock = _lock_for(self._path)
|
|
180
|
+
with self._lock:
|
|
181
|
+
self._next_seq = _next_seq(self._path)
|
|
182
|
+
|
|
183
|
+
def append_sse_event(self, event_name: str, payload=None) -> dict:
|
|
184
|
+
with self._lock:
|
|
185
|
+
seq = self._next_seq
|
|
186
|
+
self._next_seq += 1
|
|
187
|
+
return append_run_event(
|
|
188
|
+
self.session_id,
|
|
189
|
+
self.run_id,
|
|
190
|
+
event_name,
|
|
191
|
+
payload or {},
|
|
192
|
+
session_dir=self.session_dir,
|
|
193
|
+
seq=seq,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def read_run_events(
|
|
198
|
+
session_id: str,
|
|
199
|
+
run_id: str,
|
|
200
|
+
*,
|
|
201
|
+
after_seq: int | None = None,
|
|
202
|
+
session_dir: Path | None = None,
|
|
203
|
+
) -> dict:
|
|
204
|
+
path = _run_path(session_id, run_id, session_dir=session_dir)
|
|
205
|
+
events, malformed = _read_jsonl(path)
|
|
206
|
+
if after_seq is not None:
|
|
207
|
+
events = [event for event in events if int(event.get("seq") or 0) > int(after_seq)]
|
|
208
|
+
return {
|
|
209
|
+
"session_id": str(session_id),
|
|
210
|
+
"run_id": str(run_id),
|
|
211
|
+
"events": events,
|
|
212
|
+
"malformed": malformed,
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _summary_from_events(session_id: str, run_id: str, events: Iterable[dict]) -> dict:
|
|
217
|
+
ordered = [event for event in events if isinstance(event, dict)]
|
|
218
|
+
last = ordered[-1] if ordered else None
|
|
219
|
+
terminal_events = [event for event in ordered if event.get("terminal")]
|
|
220
|
+
terminal = next(
|
|
221
|
+
(event for event in reversed(terminal_events) if event.get("event") != "stream_end"),
|
|
222
|
+
terminal_events[-1] if terminal_events else None,
|
|
223
|
+
)
|
|
224
|
+
status = terminal.get("terminal_state") if terminal else ("running" if ordered else "unknown")
|
|
225
|
+
return {
|
|
226
|
+
"session_id": str(session_id),
|
|
227
|
+
"run_id": str(run_id),
|
|
228
|
+
"stream_id": str(run_id),
|
|
229
|
+
"event_count": len(ordered),
|
|
230
|
+
"last_seq": int((last or {}).get("seq") or 0),
|
|
231
|
+
"last_event_id": (last or {}).get("event_id"),
|
|
232
|
+
"terminal": bool(terminal),
|
|
233
|
+
"terminal_state": status,
|
|
234
|
+
"last_event": (last or {}).get("event"),
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def latest_run_summary(session_id: str, run_id: str, *, session_dir: Path | None = None) -> dict:
|
|
239
|
+
journal = read_run_events(session_id, run_id, session_dir=session_dir)
|
|
240
|
+
return _summary_from_events(session_id, run_id, journal.get("events") or [])
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def find_run_summary(run_id: str, *, session_dir: Path | None = None) -> dict | None:
|
|
244
|
+
rid = _validate_id(run_id, "run_id")
|
|
245
|
+
root = Path(session_dir) if session_dir is not None else _default_session_dir()
|
|
246
|
+
journal_root = root / RUN_JOURNAL_DIR_NAME
|
|
247
|
+
for path in journal_root.glob(f"*/{rid}.jsonl"):
|
|
248
|
+
session_id = path.parent.name
|
|
249
|
+
events, _malformed = _read_jsonl(path)
|
|
250
|
+
summary = _summary_from_events(session_id, rid, events)
|
|
251
|
+
summary["path"] = str(path)
|
|
252
|
+
return summary
|
|
253
|
+
return None
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def stale_interrupted_event(session_id: str, run_id: str, *, after_seq: int | None = None) -> dict | None:
|
|
257
|
+
summary = latest_run_summary(session_id, run_id)
|
|
258
|
+
if summary.get("terminal") or not summary.get("event_count"):
|
|
259
|
+
return None
|
|
260
|
+
seq = int(summary.get("last_seq") or 0) + 1
|
|
261
|
+
if after_seq is not None and seq <= int(after_seq):
|
|
262
|
+
return None
|
|
263
|
+
payload = {
|
|
264
|
+
"type": "interrupted",
|
|
265
|
+
"message": "The live worker stopped before this run finished.",
|
|
266
|
+
"hint": "The transcript was restored to the last journaled event. Start a new turn if you still need the task to continue.",
|
|
267
|
+
"session_id": session_id,
|
|
268
|
+
"stream_id": run_id,
|
|
269
|
+
"journal_last_seq": summary.get("last_seq"),
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
"version": 1,
|
|
273
|
+
"event_id": f"{run_id}:{seq}",
|
|
274
|
+
"seq": seq,
|
|
275
|
+
"run_id": run_id,
|
|
276
|
+
"session_id": session_id,
|
|
277
|
+
"event": "apperror",
|
|
278
|
+
"type": "apperror",
|
|
279
|
+
"created_at": time.time(),
|
|
280
|
+
"terminal": True,
|
|
281
|
+
"terminal_state": "lost-worker-bookkeeping",
|
|
282
|
+
"payload": payload,
|
|
283
|
+
"synthetic": True,
|
|
284
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""HTTP client boundary for a supervised Hermes WebUI runner backend.
|
|
2
|
+
|
|
3
|
+
This module intentionally contains no process-local run maps, stream queues,
|
|
4
|
+
cancellation registries, approval/clarify queues, or cached agent instances. It
|
|
5
|
+
is only a JSON-over-HTTP transport used by ``RunnerRuntimeAdapter`` when an
|
|
6
|
+
operator explicitly configures a runner endpoint.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import urllib.error
|
|
13
|
+
import urllib.parse
|
|
14
|
+
import urllib.request
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
_RUNNER_BASE_URL_ENV = "HERMES_WEBUI_RUNNER_BASE_URL"
|
|
19
|
+
_RUNNER_API_KEY_ENV = "HERMES_WEBUI_RUNNER_API_KEY"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class RunnerClientError(RuntimeError):
|
|
23
|
+
"""Raised when a configured runner endpoint rejects or fails a request."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def runner_client_configured(environ: dict[str, str] | None = None) -> bool:
|
|
27
|
+
source = os.environ if environ is None else environ
|
|
28
|
+
return bool(str(source.get(_RUNNER_BASE_URL_ENV) or "").strip())
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class HttpRunnerClient:
|
|
32
|
+
"""Small JSON HTTP client for the external/supervised runner boundary."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, *, base_url: str, api_key: str = ""):
|
|
35
|
+
self.base_url = str(base_url or "").strip().rstrip("/")
|
|
36
|
+
if not self.base_url:
|
|
37
|
+
raise ValueError("runner base_url is required")
|
|
38
|
+
# Hardening: the runner endpoint is operator-configured, but reject any
|
|
39
|
+
# non-HTTP(S) scheme so a misconfigured HERMES_WEBUI_RUNNER_BASE_URL
|
|
40
|
+
# (e.g. file:///etc/passwd or ftp://) can never be handed to urlopen.
|
|
41
|
+
_scheme = urllib.parse.urlsplit(self.base_url).scheme.lower()
|
|
42
|
+
if _scheme not in ("http", "https"):
|
|
43
|
+
raise ValueError(
|
|
44
|
+
f"runner base_url must be http(s); got scheme '{_scheme or '(none)'}'"
|
|
45
|
+
)
|
|
46
|
+
self.api_key = str(api_key or "").strip()
|
|
47
|
+
|
|
48
|
+
@classmethod
|
|
49
|
+
def from_env(cls, environ: dict[str, str] | None = None) -> "HttpRunnerClient":
|
|
50
|
+
source = os.environ if environ is None else environ
|
|
51
|
+
base_url = str(source.get(_RUNNER_BASE_URL_ENV) or "").strip()
|
|
52
|
+
if not base_url:
|
|
53
|
+
raise NotImplementedError("runner-local chat backend is not configured")
|
|
54
|
+
return cls(base_url=base_url, api_key=str(source.get(_RUNNER_API_KEY_ENV) or ""))
|
|
55
|
+
|
|
56
|
+
def start_run(self, request) -> dict[str, Any]:
|
|
57
|
+
return self._post("/v1/runs", {
|
|
58
|
+
"session_id": request.session_id,
|
|
59
|
+
"message": request.message,
|
|
60
|
+
"attachments": list(request.attachments or []),
|
|
61
|
+
"workspace": request.workspace,
|
|
62
|
+
"profile": request.profile,
|
|
63
|
+
"provider": request.provider,
|
|
64
|
+
"model": request.model,
|
|
65
|
+
"toolsets": list(request.toolsets or []),
|
|
66
|
+
"source": request.source,
|
|
67
|
+
"metadata": dict(request.metadata or {}),
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
def observe_run(self, run_id: str, *, cursor: str | None = None) -> dict[str, Any]:
|
|
71
|
+
query = ""
|
|
72
|
+
if cursor not in (None, ""):
|
|
73
|
+
query = "?cursor=" + urllib.parse.quote(str(cursor), safe="")
|
|
74
|
+
return self._get(f"/v1/runs/{urllib.parse.quote(str(run_id), safe='')}/events{query}")
|
|
75
|
+
|
|
76
|
+
def get_run(self, run_id: str) -> dict[str, Any]:
|
|
77
|
+
return self._get(f"/v1/runs/{urllib.parse.quote(str(run_id), safe='')}")
|
|
78
|
+
|
|
79
|
+
def cancel_run(self, run_id: str) -> dict[str, Any]:
|
|
80
|
+
return self._post(f"/v1/runs/{urllib.parse.quote(str(run_id), safe='')}/cancel", {})
|
|
81
|
+
|
|
82
|
+
def respond_approval(self, run_id: str, approval_id: str, choice: str) -> dict[str, Any]:
|
|
83
|
+
return self._post(
|
|
84
|
+
f"/v1/runs/{urllib.parse.quote(str(run_id), safe='')}/approvals/{urllib.parse.quote(str(approval_id), safe='')}/respond",
|
|
85
|
+
{"choice": choice},
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
def respond_clarify(self, run_id: str, clarify_id: str, response: str) -> dict[str, Any]:
|
|
89
|
+
return self._post(
|
|
90
|
+
f"/v1/runs/{urllib.parse.quote(str(run_id), safe='')}/clarifications/{urllib.parse.quote(str(clarify_id), safe='')}/respond",
|
|
91
|
+
{"response": response},
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
def queue_message(self, run_id: str, message: str, *, mode: str = "queue") -> dict[str, Any]:
|
|
95
|
+
return self._post(
|
|
96
|
+
f"/v1/runs/{urllib.parse.quote(str(run_id), safe='')}/messages",
|
|
97
|
+
{"message": message, "mode": mode},
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
def update_goal(self, session_id: str, action: str, text: str = "") -> dict[str, Any]:
|
|
101
|
+
return self._post(
|
|
102
|
+
f"/v1/sessions/{urllib.parse.quote(str(session_id), safe='')}/goal",
|
|
103
|
+
{"action": action, "text": text},
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
def _headers(self) -> dict[str, str]:
|
|
107
|
+
headers = {
|
|
108
|
+
"Accept": "application/json",
|
|
109
|
+
"Content-Type": "application/json",
|
|
110
|
+
"User-Agent": "Hermes-WebUI-RunnerClient",
|
|
111
|
+
}
|
|
112
|
+
if self.api_key:
|
|
113
|
+
headers["Authorization"] = f"Bearer {self.api_key}"
|
|
114
|
+
return headers
|
|
115
|
+
|
|
116
|
+
def _get(self, path: str) -> dict[str, Any]:
|
|
117
|
+
req = urllib.request.Request(self.base_url + path, headers=self._headers(), method="GET")
|
|
118
|
+
return self._request_json(req)
|
|
119
|
+
|
|
120
|
+
def _post(self, path: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
121
|
+
req = urllib.request.Request(
|
|
122
|
+
self.base_url + path,
|
|
123
|
+
data=json.dumps(payload).encode("utf-8"),
|
|
124
|
+
headers=self._headers(),
|
|
125
|
+
method="POST",
|
|
126
|
+
)
|
|
127
|
+
return self._request_json(req)
|
|
128
|
+
|
|
129
|
+
def _opener(self) -> urllib.request.OpenerDirector:
|
|
130
|
+
# Hardening: do NOT follow redirects. A misbehaving/compromised runner
|
|
131
|
+
# returning 3xx Location could otherwise smuggle the Bearer token to
|
|
132
|
+
# another host. Treat any redirect as an error instead.
|
|
133
|
+
class _NoRedirect(urllib.request.HTTPRedirectHandler):
|
|
134
|
+
def redirect_request(self, *args, **kwargs):
|
|
135
|
+
return None
|
|
136
|
+
return urllib.request.build_opener(_NoRedirect)
|
|
137
|
+
|
|
138
|
+
def _request_json(self, req: urllib.request.Request) -> dict[str, Any]:
|
|
139
|
+
try:
|
|
140
|
+
with self._opener().open(req, timeout=60) as resp:
|
|
141
|
+
raw = resp.read().decode("utf-8", errors="replace")
|
|
142
|
+
except urllib.error.HTTPError as exc:
|
|
143
|
+
try:
|
|
144
|
+
detail = exc.read(2048).decode("utf-8", errors="replace")
|
|
145
|
+
except Exception:
|
|
146
|
+
detail = ""
|
|
147
|
+
raise RunnerClientError(f"Runner returned HTTP {exc.code}: {detail[:500]}") from exc
|
|
148
|
+
except Exception as exc:
|
|
149
|
+
raise RunnerClientError(f"Runner request failed: {exc}") from exc
|
|
150
|
+
try:
|
|
151
|
+
payload = json.loads(raw or "{}")
|
|
152
|
+
except json.JSONDecodeError as exc:
|
|
153
|
+
raise RunnerClientError("Runner returned invalid JSON") from exc
|
|
154
|
+
if not isinstance(payload, dict):
|
|
155
|
+
raise RunnerClientError("Runner returned a non-object JSON payload")
|
|
156
|
+
return payload
|