Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
10ab54b
feat: add Telegram-native KB journey cards
acoastalfog May 15, 2026
953c910
fix: harden Telegram KB action cards
acoastalfog May 15, 2026
0be1a95
fix: render KB cockpit status from live summary packets
acoastalfog May 15, 2026
69fe2fa
feat: render live KB dashboard
acoastalfog May 15, 2026
1a6f9d8
Merge live KB dashboard
acoastalfog May 15, 2026
2bd4e9c
fix: make KB queue review item scoped
acoastalfog May 15, 2026
0254031
Merge KB queue UX patch
acoastalfog May 15, 2026
838e022
fix: clarify Telegram KB dashboard commands
acoastalfog May 15, 2026
81e7dcd
Merge KB dashboard clarity patch
acoastalfog May 15, 2026
35e0143
fix: make cron workflow watchers fail noisy
acoastalfog May 15, 2026
743048e
fix: make Telegram KB queue interactions responsive
acoastalfog May 15, 2026
66bee65
fix: clarify Telegram KB queue review UX
acoastalfog May 15, 2026
19d286b
fix: simplify Telegram KB command UX
acoastalfog May 15, 2026
90ebb88
fix: hide raw skill commands from Telegram menu
acoastalfog May 15, 2026
91bd7ca
fix: clarify Telegram KB queue decisions
acoastalfog May 15, 2026
c8ae203
feat: add Telegram KB publish flow
acoastalfog May 15, 2026
7281725
Render TODO-native KB queue actions
acoastalfog May 15, 2026
54ab405
Keep Hermes CLI version in release lockstep
acoastalfog May 15, 2026
a18eb15
Polish Telegram queue dashboard hints
acoastalfog May 15, 2026
3163f1a
fix: validate cron enabled toolsets
acoastalfog May 16, 2026
ce34a33
Add KB reasoning control surface
acoastalfog May 16, 2026
21dd589
Show live Hermes reasoning in KB status
acoastalfog May 16, 2026
4ff8a9c
Bind iterative KB queue replies to visible item
acoastalfog May 16, 2026
3d3c861
Fix KB status fallback and progress detail
acoastalfog May 16, 2026
dad6064
Avoid in-gateway systemd restart deadlock
acoastalfog May 16, 2026
a5e96e1
Refresh env for KB web dashboard
acoastalfog May 16, 2026
17d11fd
Fix KB dashboard SSH workspace cwd
acoastalfog May 17, 2026
e3fc73c
Merge upstream Hermes v2026.5.16
acoastalfog May 17, 2026
0723e71
Improve KB workflow progress visibility
acoastalfog May 18, 2026
06a8a58
Make Telegram conversation posture-aware (#11)
acoastalfog May 23, 2026
01f6c57
Bump Hermes Agent to v0.15.24
acoastalfog May 23, 2026
72e7dc4
Ship Hermes conversation posture canary
acoastalfog May 23, 2026
f1ad327
Expose KB human dashboard in Hermes
acoastalfog May 23, 2026
ac5f7f3
Bump Hermes Agent to v0.15.26
acoastalfog May 23, 2026
9cb2334
Bump Hermes Agent to v0.15.27
acoastalfog May 23, 2026
20e2f5c
Align Hermes ACP registry manifest version
acoastalfog May 23, 2026
2ccee77
Bridge KB dashboard plugin to standalone app
acoastalfog May 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions acp_registry/agent.json
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
{
"id": "hermes-agent",
"name": "Hermes Agent",
"version": "0.13.0",
"version": "0.15.28",
"description": "Self-improving open-source AI agent by Nous Research with ACP editor integration, persistent memory, skills, and rich tool support.",
"repository": "https://github.com/NousResearch/hermes-agent",
"website": "https://hermes-agent.nousresearch.com/docs/user-guide/features/acp",
"authors": ["Nous Research"],
"authors": [
"Nous Research"
],
"license": "MIT",
"distribution": {
"uvx": {
"package": "hermes-agent[acp]==0.13.0",
"args": ["hermes-acp"]
"package": "hermes-agent[acp]==0.15.28",
"args": [
"hermes-acp"
]
}
}
}
6 changes: 3 additions & 3 deletions cli-config.yaml.example
Original file line number Diff line number Diff line change
Expand Up @@ -946,10 +946,10 @@ display:
# Controls how chatty the process watcher is when you use
# terminal(background=true, notify_on_complete=true) from Telegram/Discord/etc.
# off: No watcher messages at all
# result: Only the final completion message
# result: Only the final completion message (default)
# error: Only the final message when exit code != 0
# all: Running output updates + final message (default)
background_process_notifications: all
# all: Running output updates + final message
background_process_notifications: result


# Play terminal bell when agent finishes a response.
Expand Down
11 changes: 11 additions & 0 deletions cron/jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,8 @@ def create_job(
enabled_toolsets: Optional[List[str]] = None,
workdir: Optional[str] = None,
no_agent: bool = False,
silent_ok: bool = True,
require_tool_call: bool = False,
) -> Dict[str, Any]:
"""
Create a new cron job.
Expand Down Expand Up @@ -540,6 +542,11 @@ def create_job(
and deliver its stdout directly. Empty stdout = silent (no
delivery). Requires ``script`` to be set. Ideal for classic
watchdogs and periodic alerts that don't need LLM reasoning.
silent_ok: When False, ``[SILENT]`` responses are treated as watcher
failures and delivered instead of suppressed.
require_tool_call: When True, an LLM-driven cron job must call at least
one tool before it can complete successfully. Use this for
watchdogs that must query live state.

Returns:
The created job dict
Expand Down Expand Up @@ -574,6 +581,8 @@ def create_job(
normalized_toolsets = normalized_toolsets or None
normalized_workdir = _normalize_workdir(workdir)
normalized_no_agent = bool(no_agent)
normalized_silent_ok = bool(silent_ok)
normalized_require_tool_call = bool(require_tool_call)

# no_agent jobs are meaningless without a script — the script IS the job.
# Surface this as a clear ValueError at create time so bad configs never
Expand Down Expand Up @@ -605,6 +614,8 @@ def create_job(
"base_url": normalized_base_url,
"script": normalized_script,
"no_agent": normalized_no_agent,
"silent_ok": normalized_silent_ok,
"require_tool_call": normalized_require_tool_call,
"context_from": context_from,
"schedule": parsed_schedule,
"schedule_display": parsed_schedule.get("display", schedule),
Expand Down
45 changes: 43 additions & 2 deletions cron/scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -942,6 +942,19 @@ def _build_job_prompt(job: dict, prerun_script: Optional[tuple] = None) -> str:
"findings normally, or say [SILENT] and nothing more.]\n\n"
)
prompt = cron_hint + prompt
job_contract: list[str] = []
if job.get("silent_ok") is False:
job_contract.append(
"[SILENT] is forbidden for this job: always return a delivered status "
"message, even if the status is 'still running' or the watcher cannot inspect state."
)
if job.get("require_tool_call"):
job_contract.append(
"You must call at least one relevant tool before your final response; "
"do not answer from memory, prior chat context, or the prompt alone."
)
if job_contract:
prompt = "[JOB CONTRACT: " + " ".join(job_contract) + "]\n\n" + prompt
if skills is None:
legacy = job.get("skill")
skills = [legacy] if legacy else []
Expand Down Expand Up @@ -1576,6 +1589,21 @@ def run_job(job: dict) -> tuple[bool, str, str, Optional[str]]:
)
raise RuntimeError(_err_text)

if job.get("require_tool_call"):
messages = result.get("messages") if isinstance(result.get("messages"), list) else []
tool_turns = sum(
1
for message in messages
if isinstance(message, dict)
and message.get("role") == "assistant"
and message.get("tool_calls")
)
if tool_turns < 1:
raise RuntimeError(
"cron job requires at least one tool call, but the agent completed "
"without calling tools"
)

final_response = result.get("final_response", "") or ""
# Strip leaked placeholder text that upstream may inject on empty completions.
if final_response.strip() == "(No response generated)":
Expand Down Expand Up @@ -1755,8 +1783,21 @@ def _process_job(job: dict) -> bool:
deliver_content = final_response if success else f"⚠️ Cron job '{job.get('name', job['id'])}' failed:\n{error}"
should_deliver = bool(deliver_content)
if should_deliver and success and SILENT_MARKER in deliver_content.strip().upper():
logger.info("Job '%s': agent returned %s — skipping delivery", job["id"], SILENT_MARKER)
should_deliver = False
if job.get("silent_ok") is False:
error = (
"Agent returned [SILENT], but this cron job requires "
"a delivered status update."
)
success = False
deliver_content = (
f"⚠️ Cron job '{job.get('name', job['id'])}' produced no status update.\n\n"
f"{error}\n\n"
"This usually means a watcher prompt did not call the live status tool."
)
logger.warning("Job '%s': %s", job["id"], error)
else:
logger.info("Job '%s': agent returned %s — skipping delivery", job["id"], SILENT_MARKER)
should_deliver = False

delivery_error = None
if should_deliver:
Expand Down
26 changes: 26 additions & 0 deletions gateway/platforms/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1749,6 +1749,32 @@ async def send_slash_confirm(
"""
return SendResult(success=False, error="Not supported")

async def send_kb_actions(
self,
chat_id: str,
text: str,
actions: list,
metadata: Optional[Dict[str, Any]] = None,
reply_to: Optional[str] = None,
) -> SendResult:
"""Send a generic KB action card.

Platforms with native buttons override this. The safe default keeps
the user-visible content and degrades actions to plain text labels.
"""
labels: list[str] = []
for action in actions or []:
label = getattr(action, "label", None)
if label is None and isinstance(action, dict):
label = action.get("label")
if label:
labels.append(str(label))

fallback_text = text
if labels:
fallback_text = f"{text}\n\nActions: {', '.join(labels)}"
return await self.send(chat_id, fallback_text, reply_to=reply_to, metadata=metadata)

async def send_clarify(
self,
chat_id: str,
Expand Down
Loading