@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.
Files changed (233) hide show
  1. package/README.md +213 -0
  2. package/bin/hermes-webui.mjs +588 -0
  3. package/package.json +25 -0
  4. package/scripts/sync-vendor.mjs +74 -0
  5. package/templates/launchd/com.bitseek.hermes-webui.plist +21 -0
  6. package/templates/systemd/hermes-webui.service +13 -0
  7. package/templates/windows/hermes-webui-task.ps1 +3 -0
  8. package/vendor/agent-frontend-shell/.bitseek-source.json +6 -0
  9. package/vendor/agent-frontend-shell/.dockerignore +7 -0
  10. package/vendor/agent-frontend-shell/.env.docker.example +89 -0
  11. package/vendor/agent-frontend-shell/.env.example +34 -0
  12. package/vendor/agent-frontend-shell/.github/FUNDING.yml +3 -0
  13. package/vendor/agent-frontend-shell/.github/workflows/browser-smoke.yml +42 -0
  14. package/vendor/agent-frontend-shell/.github/workflows/docker-smoke.yml +233 -0
  15. package/vendor/agent-frontend-shell/.github/workflows/native-windows-startup.yml +132 -0
  16. package/vendor/agent-frontend-shell/.github/workflows/release.yml +57 -0
  17. package/vendor/agent-frontend-shell/.github/workflows/tests.yml +88 -0
  18. package/vendor/agent-frontend-shell/.vscode/launch.json +59 -0
  19. package/vendor/agent-frontend-shell/.vscode/settings.json +13 -0
  20. package/vendor/agent-frontend-shell/AGENTS.md +80 -0
  21. package/vendor/agent-frontend-shell/ARCHITECTURE.md +1658 -0
  22. package/vendor/agent-frontend-shell/BUGS.md +52 -0
  23. package/vendor/agent-frontend-shell/CHANGELOG.md +7295 -0
  24. package/vendor/agent-frontend-shell/CONTRIBUTING.md +205 -0
  25. package/vendor/agent-frontend-shell/CONTRIBUTORS.md +107 -0
  26. package/vendor/agent-frontend-shell/DESIGN.md +173 -0
  27. package/vendor/agent-frontend-shell/Dockerfile +91 -0
  28. package/vendor/agent-frontend-shell/LICENSE +21 -0
  29. package/vendor/agent-frontend-shell/README-CUSTOM.md +76 -0
  30. package/vendor/agent-frontend-shell/README.md +705 -0
  31. package/vendor/agent-frontend-shell/ROADMAP.md +351 -0
  32. package/vendor/agent-frontend-shell/SPRINTS.md +147 -0
  33. package/vendor/agent-frontend-shell/TESTING.md +1932 -0
  34. package/vendor/agent-frontend-shell/THEMES.md +170 -0
  35. package/vendor/agent-frontend-shell/api/__init__.py +1 -0
  36. package/vendor/agent-frontend-shell/api/agent_health.py +392 -0
  37. package/vendor/agent-frontend-shell/api/agent_sessions.py +782 -0
  38. package/vendor/agent-frontend-shell/api/auth.py +592 -0
  39. package/vendor/agent-frontend-shell/api/background.py +87 -0
  40. package/vendor/agent-frontend-shell/api/clarify.py +238 -0
  41. package/vendor/agent-frontend-shell/api/commands.py +124 -0
  42. package/vendor/agent-frontend-shell/api/compression_anchor.py +134 -0
  43. package/vendor/agent-frontend-shell/api/config.py +5178 -0
  44. package/vendor/agent-frontend-shell/api/dashboard_probe.py +255 -0
  45. package/vendor/agent-frontend-shell/api/extensions.py +253 -0
  46. package/vendor/agent-frontend-shell/api/gateway_chat.py +435 -0
  47. package/vendor/agent-frontend-shell/api/gateway_watcher.py +230 -0
  48. package/vendor/agent-frontend-shell/api/goals.py +608 -0
  49. package/vendor/agent-frontend-shell/api/helpers.py +474 -0
  50. package/vendor/agent-frontend-shell/api/kanban_bridge.py +1255 -0
  51. package/vendor/agent-frontend-shell/api/metering.py +194 -0
  52. package/vendor/agent-frontend-shell/api/models.py +4210 -0
  53. package/vendor/agent-frontend-shell/api/oauth.py +770 -0
  54. package/vendor/agent-frontend-shell/api/onboarding.py +1046 -0
  55. package/vendor/agent-frontend-shell/api/passkeys.py +365 -0
  56. package/vendor/agent-frontend-shell/api/profiles.py +1499 -0
  57. package/vendor/agent-frontend-shell/api/providers.py +2175 -0
  58. package/vendor/agent-frontend-shell/api/request_diagnostics.py +160 -0
  59. package/vendor/agent-frontend-shell/api/rollback.py +320 -0
  60. package/vendor/agent-frontend-shell/api/routes.py +13990 -0
  61. package/vendor/agent-frontend-shell/api/run_journal.py +284 -0
  62. package/vendor/agent-frontend-shell/api/runner_client.py +156 -0
  63. package/vendor/agent-frontend-shell/api/runtime_adapter.py +431 -0
  64. package/vendor/agent-frontend-shell/api/session_discoverability.py +640 -0
  65. package/vendor/agent-frontend-shell/api/session_events.py +45 -0
  66. package/vendor/agent-frontend-shell/api/session_lifecycle.py +208 -0
  67. package/vendor/agent-frontend-shell/api/session_ops.py +207 -0
  68. package/vendor/agent-frontend-shell/api/session_recovery.py +655 -0
  69. package/vendor/agent-frontend-shell/api/skill_usage.py +32 -0
  70. package/vendor/agent-frontend-shell/api/startup.py +128 -0
  71. package/vendor/agent-frontend-shell/api/state_sync.py +187 -0
  72. package/vendor/agent-frontend-shell/api/streaming.py +7048 -0
  73. package/vendor/agent-frontend-shell/api/system_health.py +167 -0
  74. package/vendor/agent-frontend-shell/api/terminal.py +410 -0
  75. package/vendor/agent-frontend-shell/api/turn_journal.py +214 -0
  76. package/vendor/agent-frontend-shell/api/updates.py +1261 -0
  77. package/vendor/agent-frontend-shell/api/upload.py +322 -0
  78. package/vendor/agent-frontend-shell/api/usage.py +26 -0
  79. package/vendor/agent-frontend-shell/api/workspace.py +867 -0
  80. package/vendor/agent-frontend-shell/api/workspace_git.py +1261 -0
  81. package/vendor/agent-frontend-shell/api/worktrees.py +357 -0
  82. package/vendor/agent-frontend-shell/bootstrap.py +492 -0
  83. package/vendor/agent-frontend-shell/ctl.sh +427 -0
  84. package/vendor/agent-frontend-shell/docker-compose.custom.yml +26 -0
  85. package/vendor/agent-frontend-shell/docker-compose.three-container.yml +168 -0
  86. package/vendor/agent-frontend-shell/docker-compose.two-container.yml +147 -0
  87. package/vendor/agent-frontend-shell/docker-compose.yml +57 -0
  88. package/vendor/agent-frontend-shell/docker_init.bash +459 -0
  89. package/vendor/agent-frontend-shell/docs/CONTRACTS.md +207 -0
  90. package/vendor/agent-frontend-shell/docs/EXTENSIONS.md +212 -0
  91. package/vendor/agent-frontend-shell/docs/ISSUES.md +23 -0
  92. package/vendor/agent-frontend-shell/docs/UIUX-GUIDE.md +196 -0
  93. package/vendor/agent-frontend-shell/docs/advanced-chat-setup.md +83 -0
  94. package/vendor/agent-frontend-shell/docs/docker.md +337 -0
  95. package/vendor/agent-frontend-shell/docs/onboarding-agent-checklist.md +207 -0
  96. package/vendor/agent-frontend-shell/docs/onboarding.md +202 -0
  97. package/vendor/agent-frontend-shell/docs/remote-access.md +75 -0
  98. package/vendor/agent-frontend-shell/docs/rfcs/README.md +53 -0
  99. package/vendor/agent-frontend-shell/docs/rfcs/agent-source-boundary.md +70 -0
  100. package/vendor/agent-frontend-shell/docs/rfcs/canonical-session-resolution.md +124 -0
  101. package/vendor/agent-frontend-shell/docs/rfcs/hermes-run-adapter-contract.md +1079 -0
  102. package/vendor/agent-frontend-shell/docs/rfcs/turn-journal.md +195 -0
  103. package/vendor/agent-frontend-shell/docs/rfcs/webui-run-state-consistency-contract.md +157 -0
  104. package/vendor/agent-frontend-shell/docs/supervisor.md +280 -0
  105. package/vendor/agent-frontend-shell/docs/troubleshooting.md +132 -0
  106. package/vendor/agent-frontend-shell/docs/ui-ux/index.html +863 -0
  107. package/vendor/agent-frontend-shell/docs/ui-ux/two-stage-proposal.html +768 -0
  108. package/vendor/agent-frontend-shell/docs/why-hermes.md +489 -0
  109. package/vendor/agent-frontend-shell/docs/workspace-git.md +92 -0
  110. package/vendor/agent-frontend-shell/docs/wsl-autostart.md +126 -0
  111. package/vendor/agent-frontend-shell/eslint.runtime-guard.config.mjs +35 -0
  112. package/vendor/agent-frontend-shell/extensions/bitseek-design-system.md +330 -0
  113. package/vendor/agent-frontend-shell/extensions/branding/assets/apple-touch-icon.png +0 -0
  114. package/vendor/agent-frontend-shell/extensions/branding/assets/empty-logo.svg +739 -0
  115. package/vendor/agent-frontend-shell/extensions/branding/assets/favicon-192.png +0 -0
  116. package/vendor/agent-frontend-shell/extensions/branding/assets/favicon-32.png +0 -0
  117. package/vendor/agent-frontend-shell/extensions/branding/assets/favicon-512.png +0 -0
  118. package/vendor/agent-frontend-shell/extensions/branding/assets/favicon-512.svg +745 -0
  119. package/vendor/agent-frontend-shell/extensions/branding/assets/favicon.ico +0 -0
  120. package/vendor/agent-frontend-shell/extensions/branding/assets/favicon.svg +745 -0
  121. package/vendor/agent-frontend-shell/extensions/branding/assets/titlebar-icon-v2.svg +751 -0
  122. package/vendor/agent-frontend-shell/extensions/branding/assets/titlebar-icon-v3.svg +739 -0
  123. package/vendor/agent-frontend-shell/extensions/branding/assets/titlebar-icon.svg +745 -0
  124. package/vendor/agent-frontend-shell/extensions/branding/branding.js +112 -0
  125. package/vendor/agent-frontend-shell/extensions/branding/config.json +14 -0
  126. package/vendor/agent-frontend-shell/extensions/branding/manifest.json +53 -0
  127. package/vendor/agent-frontend-shell/extensions/index.js +67 -0
  128. package/vendor/agent-frontend-shell/extensions/loader/hermes-loader.js +77 -0
  129. package/vendor/agent-frontend-shell/extensions/manifest.json +16 -0
  130. package/vendor/agent-frontend-shell/extensions/pages/ai-teammates/page.css +333 -0
  131. package/vendor/agent-frontend-shell/extensions/pages/ai-teammates/page.js +487 -0
  132. package/vendor/agent-frontend-shell/extensions/pages/manifest.json +6 -0
  133. package/vendor/agent-frontend-shell/extensions/pages/registry.css +56 -0
  134. package/vendor/agent-frontend-shell/extensions/pages/registry.js +302 -0
  135. package/vendor/agent-frontend-shell/extensions/themes/bitseek/index.css +93 -0
  136. package/vendor/agent-frontend-shell/extensions/themes/bitseek/index.js +98 -0
  137. package/vendor/agent-frontend-shell/install.sh +63 -0
  138. package/vendor/agent-frontend-shell/mcp_server.py +567 -0
  139. package/vendor/agent-frontend-shell/package.json +12 -0
  140. package/vendor/agent-frontend-shell/pyproject.toml +56 -0
  141. package/vendor/agent-frontend-shell/pytest.ini +3 -0
  142. package/vendor/agent-frontend-shell/requirements.txt +5 -0
  143. package/vendor/agent-frontend-shell/server.py +624 -0
  144. package/vendor/agent-frontend-shell/start.ps1 +210 -0
  145. package/vendor/agent-frontend-shell/start.sh +65 -0
  146. package/vendor/agent-frontend-shell/static/apple-touch-icon.png +0 -0
  147. package/vendor/agent-frontend-shell/static/boot.js +1990 -0
  148. package/vendor/agent-frontend-shell/static/commands.js +1402 -0
  149. package/vendor/agent-frontend-shell/static/favicon-192.png +0 -0
  150. package/vendor/agent-frontend-shell/static/favicon-32.png +0 -0
  151. package/vendor/agent-frontend-shell/static/favicon-512.png +0 -0
  152. package/vendor/agent-frontend-shell/static/favicon-512.svg +18 -0
  153. package/vendor/agent-frontend-shell/static/favicon.ico +0 -0
  154. package/vendor/agent-frontend-shell/static/favicon.svg +20 -0
  155. package/vendor/agent-frontend-shell/static/i18n.js +15389 -0
  156. package/vendor/agent-frontend-shell/static/icons.js +92 -0
  157. package/vendor/agent-frontend-shell/static/index.html +1506 -0
  158. package/vendor/agent-frontend-shell/static/login.js +177 -0
  159. package/vendor/agent-frontend-shell/static/manifest.json +53 -0
  160. package/vendor/agent-frontend-shell/static/messages.js +3521 -0
  161. package/vendor/agent-frontend-shell/static/onboarding.js +800 -0
  162. package/vendor/agent-frontend-shell/static/panels.js +7995 -0
  163. package/vendor/agent-frontend-shell/static/pwa-startup.js +83 -0
  164. package/vendor/agent-frontend-shell/static/sessions.js +5165 -0
  165. package/vendor/agent-frontend-shell/static/style.css +4774 -0
  166. package/vendor/agent-frontend-shell/static/sw.js +173 -0
  167. package/vendor/agent-frontend-shell/static/terminal.js +632 -0
  168. package/vendor/agent-frontend-shell/static/ui.js +8997 -0
  169. package/vendor/agent-frontend-shell/static/vendor/js-yaml/4.1.0/js-yaml.min.js +2 -0
  170. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_AMS-Regular.ttf +0 -0
  171. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_AMS-Regular.woff +0 -0
  172. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  173. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
  174. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
  175. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  176. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
  177. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
  178. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  179. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
  180. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Bold.woff +0 -0
  181. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  182. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
  183. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Regular.woff +0 -0
  184. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  185. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Bold.ttf +0 -0
  186. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Bold.woff +0 -0
  187. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Bold.woff2 +0 -0
  188. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
  189. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-BoldItalic.woff +0 -0
  190. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  191. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Italic.ttf +0 -0
  192. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Italic.woff +0 -0
  193. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Italic.woff2 +0 -0
  194. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Regular.ttf +0 -0
  195. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Regular.woff +0 -0
  196. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Main-Regular.woff2 +0 -0
  197. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
  198. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-BoldItalic.woff +0 -0
  199. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  200. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-Italic.ttf +0 -0
  201. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-Italic.woff +0 -0
  202. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Math-Italic.woff2 +0 -0
  203. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
  204. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Bold.woff +0 -0
  205. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  206. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
  207. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Italic.woff +0 -0
  208. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  209. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
  210. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Regular.woff +0 -0
  211. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  212. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Script-Regular.ttf +0 -0
  213. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Script-Regular.woff +0 -0
  214. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Script-Regular.woff2 +0 -0
  215. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size1-Regular.ttf +0 -0
  216. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size1-Regular.woff +0 -0
  217. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  218. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size2-Regular.ttf +0 -0
  219. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size2-Regular.woff +0 -0
  220. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  221. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size3-Regular.ttf +0 -0
  222. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size3-Regular.woff +0 -0
  223. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  224. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size4-Regular.ttf +0 -0
  225. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size4-Regular.woff +0 -0
  226. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  227. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
  228. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Typewriter-Regular.woff +0 -0
  229. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  230. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/katex.min.css +1 -0
  231. package/vendor/agent-frontend-shell/static/vendor/katex/0.16.22/katex.min.js +1 -0
  232. package/vendor/agent-frontend-shell/static/vendor/smd.min.js +29 -0
  233. package/vendor/agent-frontend-shell/static/workspace.js +680 -0
@@ -0,0 +1,608 @@
1
+ """WebUI bridge for Hermes persistent session goals."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import copy
6
+ import logging
7
+ import re
8
+ import time
9
+ from pathlib import Path
10
+ from typing import Any, Dict, Optional
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ try: # Exposed as a module attribute so tests can monkeypatch it directly.
15
+ from hermes_cli.goals import ( # type: ignore
16
+ CONTINUATION_PROMPT_TEMPLATE,
17
+ DEFAULT_MAX_TURNS,
18
+ GoalManager as _NativeGoalManager,
19
+ GoalState,
20
+ judge_goal,
21
+ )
22
+ except Exception: # pragma: no cover - depends on installed hermes-agent
23
+ CONTINUATION_PROMPT_TEMPLATE = "" # type: ignore
24
+ DEFAULT_MAX_TURNS = 20 # type: ignore
25
+ _NativeGoalManager = None # type: ignore
26
+ GoalState = None # type: ignore
27
+ judge_goal = None # type: ignore
28
+
29
+ GoalManager = _NativeGoalManager # type: ignore
30
+
31
+ _DB_CACHE: dict[str, Any] = {}
32
+
33
+
34
+ def _default_max_turns() -> int:
35
+ """Return the configured /goal turn budget, defaulting to Hermes' 20 turns."""
36
+ try:
37
+ from api import config as _config
38
+
39
+ cfg = getattr(_config, "cfg", {}) or {}
40
+ goals_cfg = cfg.get("goals", {}) if isinstance(cfg, dict) else {}
41
+ if not isinstance(goals_cfg, dict):
42
+ return int(DEFAULT_MAX_TURNS or 20)
43
+ return max(1, int(goals_cfg.get("max_turns", DEFAULT_MAX_TURNS or 20) or 20))
44
+ except Exception:
45
+ return int(DEFAULT_MAX_TURNS or 20)
46
+
47
+
48
+ def _meta_key(session_id: str) -> str:
49
+ return f"goal:{session_id}"
50
+
51
+
52
+ def _profile_db(profile_home: str | Path):
53
+ """Return a SessionDB pinned to *profile_home*, without reading HERMES_HOME.
54
+
55
+ The upstream Hermes GoalManager persists through hermes_cli.goals.load_goal(),
56
+ which resolves SessionDB from process-global HERMES_HOME. WebUI sessions are
57
+ profile-scoped and can run concurrently, so the WebUI bridge uses an explicit
58
+ state.db path whenever the caller provides the session's profile home.
59
+ """
60
+ home = Path(profile_home).expanduser().resolve()
61
+ key = str(home)
62
+ cached = _DB_CACHE.get(key)
63
+ if cached is not None:
64
+ return cached
65
+ try:
66
+ from hermes_state import SessionDB # type: ignore
67
+
68
+ db = SessionDB(db_path=home / "state.db")
69
+ except Exception as exc: # pragma: no cover - import/env dependent
70
+ logger.debug("GoalManager profile DB unavailable for %s: %s", home, exc)
71
+ return None
72
+ _DB_CACHE[key] = db
73
+ return db
74
+
75
+
76
+ class _ProfileGoalManager:
77
+ """Small WebUI-local GoalManager adapter with explicit profile persistence."""
78
+
79
+ def __init__(self, session_id: str, *, profile_home: str | Path, default_max_turns: int = 20):
80
+ if GoalState is None:
81
+ raise RuntimeError("Hermes goal state unavailable")
82
+ self.session_id = session_id
83
+ self.profile_home = Path(profile_home).expanduser().resolve()
84
+ self.default_max_turns = int(default_max_turns or DEFAULT_MAX_TURNS or 20)
85
+ self._state = self._load()
86
+
87
+ @property
88
+ def state(self):
89
+ return self._state
90
+
91
+ def _load(self):
92
+ db = _profile_db(self.profile_home)
93
+ if db is None or not self.session_id:
94
+ return None
95
+ try:
96
+ raw = db.get_meta(_meta_key(self.session_id))
97
+ except Exception as exc:
98
+ logger.debug("GoalManager profile get_meta failed: %s", exc)
99
+ return None
100
+ if not raw:
101
+ return None
102
+ try:
103
+ return GoalState.from_json(raw) # type: ignore[union-attr]
104
+ except Exception as exc:
105
+ logger.warning("GoalManager profile state parse failed for %s: %s", self.session_id, exc)
106
+ return None
107
+
108
+ def _save(self, state) -> None:
109
+ db = _profile_db(self.profile_home)
110
+ if db is None or not self.session_id or state is None:
111
+ return
112
+ try:
113
+ db.set_meta(_meta_key(self.session_id), state.to_json())
114
+ except Exception as exc:
115
+ logger.debug("GoalManager profile set_meta failed: %s", exc)
116
+
117
+ def is_active(self) -> bool:
118
+ return self._state is not None and self._state.status == "active"
119
+
120
+ def has_goal(self) -> bool:
121
+ return self._state is not None and self._state.status in ("active", "paused")
122
+
123
+ def status_line(self) -> str:
124
+ s = self._state
125
+ if s is None or s.status in ("cleared",):
126
+ return "No active goal. Set one with /goal <text>."
127
+ turns = f"{s.turns_used}/{s.max_turns} turns"
128
+ if s.status == "active":
129
+ return f"⊙ Goal (active, {turns}): {s.goal}"
130
+ if s.status == "paused":
131
+ extra = f" — {s.paused_reason}" if s.paused_reason else ""
132
+ return f"⏸ Goal (paused, {turns}{extra}): {s.goal}"
133
+ if s.status == "done":
134
+ return f"✓ Goal done ({turns}): {s.goal}"
135
+ return f"Goal ({s.status}, {turns}): {s.goal}"
136
+
137
+ def set(self, goal: str, *, max_turns: Optional[int] = None):
138
+ goal = (goal or "").strip()
139
+ if not goal:
140
+ raise ValueError("goal text is empty")
141
+ state = GoalState( # type: ignore[operator]
142
+ goal=goal,
143
+ status="active",
144
+ turns_used=0,
145
+ max_turns=int(max_turns) if max_turns else self.default_max_turns,
146
+ created_at=time.time(),
147
+ last_turn_at=0.0,
148
+ )
149
+ self._state = state
150
+ self._save(state)
151
+ return state
152
+
153
+ def pause(self, reason: str = "user-paused"):
154
+ if not self._state:
155
+ return None
156
+ self._state.status = "paused"
157
+ self._state.paused_reason = reason
158
+ self._save(self._state)
159
+ return self._state
160
+
161
+ def resume(self, *, reset_budget: bool = True):
162
+ if not self._state:
163
+ return None
164
+ self._state.status = "active"
165
+ self._state.paused_reason = None
166
+ if reset_budget:
167
+ self._state.turns_used = 0
168
+ self._save(self._state)
169
+ return self._state
170
+
171
+ def clear(self) -> None:
172
+ if self._state is None:
173
+ return
174
+ self._state.status = "cleared"
175
+ self._save(self._state)
176
+ self._state = None
177
+
178
+ def evaluate_after_turn(self, last_response: str, *, user_initiated: bool = True) -> Dict[str, Any]:
179
+ state = self._state
180
+ if state is None or state.status != "active":
181
+ return {
182
+ "status": state.status if state else None,
183
+ "should_continue": False,
184
+ "continuation_prompt": None,
185
+ "verdict": "inactive",
186
+ "reason": "no active goal",
187
+ "message": "",
188
+ }
189
+
190
+ state.turns_used += 1
191
+ state.last_turn_at = time.time()
192
+
193
+ if judge_goal is None:
194
+ verdict, reason = "continue", "goal judge unavailable"
195
+ else:
196
+ verdict, reason = judge_goal(state.goal, str(last_response or ""))
197
+ state.last_verdict = verdict
198
+ state.last_reason = reason
199
+
200
+ if verdict == "done":
201
+ state.status = "done"
202
+ self._save(state)
203
+ return {
204
+ "status": "done",
205
+ "should_continue": False,
206
+ "continuation_prompt": None,
207
+ "verdict": "done",
208
+ "reason": reason,
209
+ "message": f"✓ Goal achieved: {reason}",
210
+ }
211
+
212
+ if state.turns_used >= state.max_turns:
213
+ state.status = "paused"
214
+ state.paused_reason = f"turn budget exhausted ({state.turns_used}/{state.max_turns})"
215
+ self._save(state)
216
+ return {
217
+ "status": "paused",
218
+ "should_continue": False,
219
+ "continuation_prompt": None,
220
+ "verdict": "continue",
221
+ "reason": reason,
222
+ "message": (
223
+ f"⏸ Goal paused — {state.turns_used}/{state.max_turns} turns used. "
224
+ "Use /goal resume to keep going, or /goal clear to stop."
225
+ ),
226
+ }
227
+
228
+ self._save(state)
229
+ return {
230
+ "status": "active",
231
+ "should_continue": True,
232
+ "continuation_prompt": self.next_continuation_prompt(),
233
+ "verdict": "continue",
234
+ "reason": reason,
235
+ "message": f"↻ Continuing toward goal ({state.turns_used}/{state.max_turns}): {reason}",
236
+ }
237
+
238
+ def next_continuation_prompt(self) -> Optional[str]:
239
+ if not self._state or self._state.status != "active":
240
+ return None
241
+ return CONTINUATION_PROMPT_TEMPLATE.format(goal=self._state.goal)
242
+
243
+
244
+ def _manager(session_id: str, *, profile_home: str | Path | None = None):
245
+ if GoalManager is None:
246
+ return None
247
+ if profile_home and GoalManager is _NativeGoalManager and GoalState is not None:
248
+ try:
249
+ return _ProfileGoalManager(
250
+ session_id=session_id,
251
+ profile_home=profile_home,
252
+ default_max_turns=_default_max_turns(),
253
+ )
254
+ except Exception as exc:
255
+ logger.debug("Profile-scoped GoalManager unavailable: %s", exc)
256
+ return None
257
+ return GoalManager(session_id=session_id, default_max_turns=_default_max_turns())
258
+
259
+
260
+ def _state_payload(state: Any) -> Optional[Dict[str, Any]]:
261
+ if state is None:
262
+ return None
263
+ return {
264
+ "goal": getattr(state, "goal", "") or "",
265
+ "status": getattr(state, "status", "") or "",
266
+ "turns_used": int(getattr(state, "turns_used", 0) or 0),
267
+ "max_turns": int(getattr(state, "max_turns", 0) or 0),
268
+ "last_verdict": getattr(state, "last_verdict", None),
269
+ "last_reason": getattr(state, "last_reason", None),
270
+ "paused_reason": getattr(state, "paused_reason", None),
271
+ }
272
+
273
+
274
+ def _payload(
275
+ *,
276
+ ok: bool = True,
277
+ action: str,
278
+ message: str,
279
+ state: Any = None,
280
+ error: str | None = None,
281
+ kickoff_prompt: str | None = None,
282
+ decision: Dict[str, Any] | None = None,
283
+ message_key: str | None = None,
284
+ message_args: list[Any] | None = None,
285
+ ) -> Dict[str, Any]:
286
+ body: Dict[str, Any] = {
287
+ "ok": bool(ok),
288
+ "action": action,
289
+ "message": message,
290
+ "goal": _state_payload(state),
291
+ }
292
+ if error:
293
+ body["error"] = error
294
+ if kickoff_prompt:
295
+ body["kickoff_prompt"] = kickoff_prompt
296
+ if decision is not None:
297
+ body["decision"] = decision
298
+ if message_key:
299
+ body["message_key"] = message_key
300
+ if message_args is not None:
301
+ body["message_args"] = [a for a in message_args if a is not None]
302
+ return body
303
+
304
+
305
+ def _goal_status_payload(state: Any, *, default_message: str | None = None) -> Dict[str, Any]:
306
+ """Build localized-status style payload fields from a goal state."""
307
+ if default_message is None:
308
+ default_message = "No active goal. Set one with /goal <text>."
309
+ if state is None:
310
+ return {"message": default_message, "message_key": "goal_status_none"}
311
+ status = str(getattr(state, "status", "") or "").strip()
312
+ if status in ("cleared",):
313
+ return {"message": default_message, "message_key": "goal_status_none"}
314
+ turns_used = int(getattr(state, "turns_used", 0) or 0)
315
+ max_turns = int(getattr(state, "max_turns", 0) or 0)
316
+ goal = str(getattr(state, "goal", "") or "")
317
+ if status == "active":
318
+ return {
319
+ "message": f"⊙ Goal (active, {turns_used}/{max_turns} turns): {goal}",
320
+ "message_key": "goal_status_active",
321
+ "message_args": [turns_used, max_turns, goal],
322
+ }
323
+ if status == "paused":
324
+ reason = str(getattr(state, "paused_reason", "") or "")
325
+ return {
326
+ "message": f"⏸ Goal (paused, {turns_used}/{max_turns}{' — ' + reason if reason else ''}): {goal}",
327
+ "message_key": "goal_status_paused",
328
+ "message_args": [turns_used, max_turns, reason, goal],
329
+ }
330
+ if status == "done":
331
+ return {
332
+ "message": f"✓ Goal done ({turns_used}/{max_turns}): {goal}",
333
+ "message_key": "goal_status_done",
334
+ "message_args": [turns_used, max_turns, goal],
335
+ }
336
+ return {
337
+ "message": f"Goal ({status}, {turns_used}/{max_turns}): {goal}",
338
+ "message_args": [status, turns_used, max_turns, goal],
339
+ }
340
+
341
+
342
+ def _extract_goal_turns_from_message(message: str) -> tuple[int, int]:
343
+ """Best-effort extraction for continuation messages like '(1/20)'."""
344
+ if not message:
345
+ return 0, 0
346
+ match = re.search(r"\((\d+)\s*/\s*(\d+)\)", message)
347
+ if not match:
348
+ return 0, 0
349
+ try:
350
+ return int(match.group(1)), int(match.group(2))
351
+ except Exception:
352
+ return 0, 0
353
+
354
+
355
+ def _goal_decision_payload(
356
+ decision: Dict[str, Any],
357
+ state: Any,
358
+ ) -> Dict[str, Any]:
359
+ """Attach goal message i18n key/args to an evaluation decision."""
360
+ if not isinstance(decision, dict):
361
+ return decision
362
+ status = str(decision.get("status") or "").strip()
363
+ reason = str(decision.get("reason") or "").strip()
364
+ turns_used = int(getattr(state, "turns_used", 0) or 0)
365
+ max_turns = int(getattr(state, "max_turns", 0) or 0)
366
+ if (turns_used, max_turns) == (0, 0):
367
+ turns_used, max_turns = _extract_goal_turns_from_message(str(decision.get("message") or ""))
368
+
369
+ if status == "done":
370
+ return {
371
+ **decision,
372
+ "message_key": "goal_achieved",
373
+ "message_args": [reason],
374
+ }
375
+ if status == "paused":
376
+ return {
377
+ **decision,
378
+ "message_key": "goal_paused_budget_exhausted",
379
+ "message_args": [turns_used, max_turns],
380
+ }
381
+ if decision.get("should_continue"):
382
+ return {
383
+ **decision,
384
+ "message_key": "goal_continuing",
385
+ "message_args": [turns_used, max_turns, reason],
386
+ }
387
+ return decision
388
+
389
+
390
+ def goal_state_snapshot(session_id: str, *, profile_home: str | Path | None = None) -> Any:
391
+ """Return a deep copy of current goal state for rollback before kickoff."""
392
+ mgr = _manager(str(session_id or ""), profile_home=profile_home)
393
+ if mgr is None:
394
+ return None
395
+ return copy.deepcopy(getattr(mgr, "state", None))
396
+
397
+
398
+ def restore_goal_state(session_id: str, snapshot: Any, *, profile_home: str | Path | None = None) -> None:
399
+ """Restore a prior goal state after kickoff stream creation fails."""
400
+ mgr = _manager(str(session_id or ""), profile_home=profile_home)
401
+ if mgr is None:
402
+ return
403
+ if snapshot is None:
404
+ try:
405
+ mgr.clear()
406
+ except Exception:
407
+ pass
408
+ return
409
+ if isinstance(mgr, _ProfileGoalManager):
410
+ mgr._state = snapshot
411
+ mgr._save(snapshot)
412
+ return
413
+ try:
414
+ from hermes_cli.goals import save_goal # type: ignore
415
+
416
+ save_goal(str(session_id or ""), snapshot)
417
+ except Exception as exc: # pragma: no cover - native fallback only
418
+ logger.debug("Goal state restore failed for %s: %s", session_id, exc)
419
+
420
+
421
+ def goal_command_payload(
422
+ session_id: str,
423
+ args: str = "",
424
+ *,
425
+ stream_running: bool = False,
426
+ profile_home: str | Path | None = None,
427
+ ) -> Dict[str, Any]:
428
+ """Return the WebUI response payload for a /goal command.
429
+
430
+ Mirrors the gateway command semantics:
431
+ - /goal or /goal status shows status
432
+ - /goal pause pauses
433
+ - /goal resume resumes without auto-starting a turn
434
+ - /goal clear|stop|done clears
435
+ - /goal <text> sets a new active goal and returns kickoff_prompt so the
436
+ caller can start the first normal user-role turn immediately.
437
+ """
438
+ sid = str(session_id or "").strip()
439
+ if not sid:
440
+ return _payload(ok=False, action="error", error="missing_session", message="session_id required")
441
+
442
+ mgr = _manager(sid, profile_home=profile_home)
443
+ if mgr is None:
444
+ return _payload(ok=False, action="error", error="unavailable", message="Goals unavailable on this session.")
445
+
446
+ text = str(args or "").strip()
447
+ lower = text.lower()
448
+
449
+ if not text or lower == "status":
450
+ state = getattr(mgr, "state", None)
451
+ status_payload = _goal_status_payload(state)
452
+ return _payload(action="status", state=state, **status_payload)
453
+
454
+ if lower == "pause":
455
+ state = mgr.pause(reason="user-paused")
456
+ if state is None:
457
+ return _payload(
458
+ ok=False,
459
+ action="pause",
460
+ error="no_goal",
461
+ message="No goal set.",
462
+ message_key="goal_no_goal",
463
+ )
464
+ return _payload(
465
+ action="pause",
466
+ message=f"⏸ Goal paused: {state.goal}",
467
+ message_key="goal_paused",
468
+ message_args=[str(state.goal)],
469
+ state=state,
470
+ )
471
+
472
+ if lower == "resume":
473
+ state = mgr.resume()
474
+ if state is None:
475
+ return _payload(
476
+ ok=False,
477
+ action="resume",
478
+ error="no_goal",
479
+ message="No goal to resume.",
480
+ message_key="goal_no_goal",
481
+ )
482
+ return _payload(
483
+ action="resume",
484
+ message=(
485
+ f"▶ Goal resumed: {state.goal}\n"
486
+ "Send a new message, or type continue, to kick it off."
487
+ ),
488
+ message_key="goal_resumed",
489
+ message_args=[str(state.goal)],
490
+ state=state,
491
+ )
492
+
493
+ if lower in ("clear", "stop", "done"):
494
+ had = bool(mgr.has_goal())
495
+ mgr.clear()
496
+ return _payload(
497
+ action="clear",
498
+ message="Goal cleared." if had else "No active goal.",
499
+ message_key="goal_cleared" if had else "goal_no_goal",
500
+ state=getattr(mgr, "state", None),
501
+ )
502
+
503
+ if stream_running:
504
+ return _payload(
505
+ ok=False,
506
+ action="set",
507
+ error="agent_running",
508
+ message=(
509
+ "Agent is running — use /goal status / pause / clear mid-run, "
510
+ "or /stop before setting a new goal."
511
+ ),
512
+ )
513
+
514
+ try:
515
+ state = mgr.set(text)
516
+ except ValueError as exc:
517
+ return _payload(ok=False, action="set", error="invalid_goal", message=f"Invalid goal: {exc}")
518
+
519
+ return _payload(
520
+ action="set",
521
+ message=(
522
+ f"⊙ Goal set ({state.max_turns}-turn budget): {state.goal}\n"
523
+ "I'll keep working until the goal is done, you pause/clear it, or the budget is exhausted.\n"
524
+ "Controls: /goal status · /goal pause · /goal resume · /goal clear"
525
+ ),
526
+ message_key="goal_set",
527
+ message_args=[state.max_turns, state.goal],
528
+ state=state,
529
+ kickoff_prompt=state.goal,
530
+ )
531
+
532
+
533
+ def has_active_goal(
534
+ session_id: str,
535
+ *,
536
+ profile_home: str | Path | None = None,
537
+ ) -> bool:
538
+ """Return True when the session has an active standing goal to evaluate."""
539
+ sid = str(session_id or "").strip()
540
+ if not sid:
541
+ return False
542
+ mgr = _manager(sid, profile_home=profile_home)
543
+ if mgr is None:
544
+ return False
545
+ try:
546
+ return bool(mgr.is_active())
547
+ except Exception as exc:
548
+ logger.debug("goal active-state check failed for session=%s: %s", sid, exc)
549
+ return False
550
+
551
+
552
+ def evaluate_goal_after_turn(
553
+ session_id: str,
554
+ last_response: str,
555
+ *,
556
+ user_initiated: bool = True,
557
+ profile_home: str | Path | None = None,
558
+ ) -> Dict[str, Any]:
559
+ """Evaluate a completed turn against the standing goal, if any."""
560
+ sid = str(session_id or "").strip()
561
+ if not sid:
562
+ return {
563
+ "status": None,
564
+ "should_continue": False,
565
+ "continuation_prompt": None,
566
+ "verdict": "inactive",
567
+ "reason": "missing session_id",
568
+ "message": "",
569
+ }
570
+ mgr = _manager(sid, profile_home=profile_home)
571
+ if mgr is None:
572
+ return {
573
+ "status": None,
574
+ "should_continue": False,
575
+ "continuation_prompt": None,
576
+ "verdict": "inactive",
577
+ "reason": "goals unavailable",
578
+ "message": "",
579
+ }
580
+ try:
581
+ if not mgr.is_active():
582
+ return {
583
+ "status": getattr(getattr(mgr, "state", None), "status", None),
584
+ "should_continue": False,
585
+ "continuation_prompt": None,
586
+ "verdict": "inactive",
587
+ "reason": "no active goal",
588
+ "message": "",
589
+ }
590
+ decision = mgr.evaluate_after_turn(str(last_response or ""), user_initiated=user_initiated)
591
+ except Exception as exc:
592
+ logger.debug("goal evaluation failed for session=%s: %s", sid, exc)
593
+ return {
594
+ "status": None,
595
+ "should_continue": False,
596
+ "continuation_prompt": None,
597
+ "verdict": "error",
598
+ "reason": f"goal evaluation failed: {type(exc).__name__}",
599
+ "message": "",
600
+ }
601
+ if not isinstance(decision, dict):
602
+ decision = {}
603
+ decision.setdefault("should_continue", False)
604
+ decision.setdefault("continuation_prompt", None)
605
+ decision.setdefault("message", "")
606
+ decision = dict(decision)
607
+ decision = _goal_decision_payload(decision, getattr(mgr, "state", None))
608
+ return decision