Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
454 changes: 384 additions & 70 deletions gateway/run.py

Large diffs are not rendered by default.

48 changes: 44 additions & 4 deletions hermes_cli/kanban_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -858,6 +858,10 @@ class Event:
chat_id TEXT NOT NULL,
thread_id TEXT NOT NULL DEFAULT '',
user_id TEXT,
notification_mode TEXT NOT NULL DEFAULT 'direct',
origin_session_id TEXT,
origin_profile TEXT,
origin_context TEXT,
created_at INTEGER NOT NULL,
last_event_id INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (task_id, platform, chat_id, thread_id)
Expand Down Expand Up @@ -1082,6 +1086,21 @@ def _migrate_add_optional_columns(conn: sqlite3.Connection) -> None:
"ON task_events(run_id, id)"
)

notify_cols = {
row["name"] for row in conn.execute("PRAGMA table_info(kanban_notify_subs)")
}
if "notification_mode" not in notify_cols:
conn.execute(
"ALTER TABLE kanban_notify_subs ADD COLUMN "
"notification_mode TEXT NOT NULL DEFAULT 'direct'"
)
if "origin_session_id" not in notify_cols:
conn.execute("ALTER TABLE kanban_notify_subs ADD COLUMN origin_session_id TEXT")
if "origin_profile" not in notify_cols:
conn.execute("ALTER TABLE kanban_notify_subs ADD COLUMN origin_profile TEXT")
if "origin_context" not in notify_cols:
conn.execute("ALTER TABLE kanban_notify_subs ADD COLUMN origin_context TEXT")

# One-shot backfill: any task that is 'running' before runs existed
# had its claim_lock / claim_expires / worker_pid on the task row.
# Synthesize a matching task_runs row so subsequent end-run / heartbeat
Expand Down Expand Up @@ -4183,6 +4202,11 @@ def task_age(task: Task) -> dict:
# Notification subscriptions (used by the gateway kanban-notifier)
# ---------------------------------------------------------------------------

def _normalize_notification_mode(mode: Optional[str]) -> str:
value = str(mode or "direct").strip().lower()
return value if value in {"direct", "synthesize", "silent"} else "direct"


def add_notify_sub(
conn: sqlite3.Connection,
*,
Expand All @@ -4191,18 +4215,34 @@ def add_notify_sub(
chat_id: str,
thread_id: Optional[str] = None,
user_id: Optional[str] = None,
notification_mode: Optional[str] = "direct",
origin_session_id: Optional[str] = None,
origin_profile: Optional[str] = None,
origin_context: Optional[str] = None,
) -> None:
"""Register a gateway source that wants terminal-state notifications
for ``task_id``. Idempotent on (task, platform, chat, thread)."""
now = int(time.time())
mode = _normalize_notification_mode(notification_mode)
with write_txn(conn):
conn.execute(
"""
INSERT OR IGNORE INTO kanban_notify_subs
(task_id, platform, chat_id, thread_id, user_id, created_at)
VALUES (?, ?, ?, ?, ?, ?)
INSERT INTO kanban_notify_subs
(task_id, platform, chat_id, thread_id, user_id,
notification_mode, origin_session_id, origin_profile,
origin_context, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(task_id, platform, chat_id, thread_id) DO UPDATE SET
notification_mode = excluded.notification_mode,
user_id = COALESCE(excluded.user_id, user_id),
origin_session_id = COALESCE(excluded.origin_session_id, origin_session_id),
origin_profile = COALESCE(excluded.origin_profile, origin_profile),
origin_context = COALESCE(excluded.origin_context, origin_context)
""",
(task_id, platform, chat_id, thread_id or "", user_id, now),
(
task_id, platform, chat_id, thread_id or "", user_id,
mode, origin_session_id, origin_profile, origin_context, now,
),
)


Expand Down
1 change: 1 addition & 0 deletions hermes_cli/tools_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
("messaging", "📨 Cross-Platform Messaging", "send_message"),
("rl", "🧪 RL Training", "Tinker-Atropos training tools"),
("homeassistant", "🏠 Home Assistant", "smart home device control"),
("kanban", "🗂️ Kanban Coordination", "show, create, complete, block, heartbeat"),
("spotify", "🎵 Spotify", "playback, search, playlists, library"),
("discord", "💬 Discord (read/participate)", "fetch messages, search members, create thread"),
("discord_admin", "🛡️ Discord Server Admin", "list channels/roles, pin, assign roles"),
Expand Down
16 changes: 16 additions & 0 deletions model_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -702,6 +702,10 @@ def handle_function_call(
task_id: Optional[str] = None,
tool_call_id: Optional[str] = None,
session_id: Optional[str] = None,
platform: Optional[str] = None,
chat_id: Optional[str] = None,
thread_id: Optional[str] = None,
user_id: Optional[str] = None,
user_task: Optional[str] = None,
enabled_tools: Optional[List[str]] = None,
skip_pre_tool_call_hook: bool = False,
Expand All @@ -713,6 +717,8 @@ def handle_function_call(
function_name: Name of the function to call.
function_args: Arguments for the function.
task_id: Unique identifier for terminal/browser session isolation.
session_id: Hermes conversation/session id for provenance-aware tools.
platform/chat_id/thread_id/user_id: Optional origin surface metadata.
user_task: The user's original task (for browser_snapshot context).
enabled_tools: Tool names enabled for this session. When provided,
execute_code uses this list to determine which sandbox
Expand Down Expand Up @@ -781,12 +787,22 @@ def handle_function_call(
function_name, function_args,
task_id=task_id,
enabled_tools=sandbox_enabled,
session_id=session_id,
platform=platform,
chat_id=chat_id,
thread_id=thread_id,
user_id=user_id,
)
else:
result = registry.dispatch(
function_name, function_args,
task_id=task_id,
user_task=user_task,
session_id=session_id,
platform=platform,
chat_id=chat_id,
thread_id=thread_id,
user_id=user_id,
)
duration_ms = int((time.monotonic() - _dispatch_start) * 1000)

Expand Down
16 changes: 16 additions & 0 deletions run_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -10006,6 +10006,11 @@ def _invoke_tool(self, function_name: str, function_args: dict, effective_task_i
function_name, function_args, effective_task_id,
tool_call_id=tool_call_id,
session_id=self.session_id or "",
platform=self.platform or "",
chat_id=self._chat_id or "",
thread_id=self._thread_id or "",
user_id=self._user_id or "",
user_task=getattr(self, "_current_user_message_for_tools", ""),
enabled_tools=list(self.valid_tool_names) if self.valid_tool_names else None,
skip_pre_tool_call_hook=True,
)
Expand Down Expand Up @@ -10717,6 +10722,11 @@ def _execute_tool_calls_sequential(self, assistant_message, messages: list, effe
function_name, function_args, effective_task_id,
tool_call_id=tool_call.id,
session_id=self.session_id or "",
platform=self.platform or "",
chat_id=self._chat_id or "",
thread_id=self._thread_id or "",
user_id=self._user_id or "",
user_task=getattr(self, "_current_user_message_for_tools", ""),
enabled_tools=list(self.valid_tool_names) if self.valid_tool_names else None,
skip_pre_tool_call_hook=True,
)
Expand All @@ -10737,6 +10747,11 @@ def _execute_tool_calls_sequential(self, assistant_message, messages: list, effe
function_name, function_args, effective_task_id,
tool_call_id=tool_call.id,
session_id=self.session_id or "",
platform=self.platform or "",
chat_id=self._chat_id or "",
thread_id=self._thread_id or "",
user_id=self._user_id or "",
user_task=getattr(self, "_current_user_message_for_tools", ""),
enabled_tools=list(self.valid_tool_names) if self.valid_tool_names else None,
skip_pre_tool_call_hook=True,
)
Expand Down Expand Up @@ -11145,6 +11160,7 @@ def run_conversation(
user_message = _sanitize_surrogates(user_message)
if isinstance(persist_user_message, str):
persist_user_message = _sanitize_surrogates(persist_user_message)
self._current_user_message_for_tools = str(user_message or "")[:4000]

# Store stream callback for _interruptible_api_call to pick up
self._stream_callback = stream_callback
Expand Down
Loading