@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,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
|