@inline-chat/hermes-agent-adapter 1.0.0-alpha.1 → 1.0.0-alpha.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inline-chat/hermes-agent-adapter",
3
- "version": "1.0.0-alpha.1",
3
+ "version": "1.0.0-alpha.3",
4
4
  "description": "Hermes Agent platform adapter for Inline, with a native Python plugin and bundled Inline realtime sidecar.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -88,9 +88,9 @@
88
88
  "testedHermesVersion": "0.22.5",
89
89
  "testedHermesCommit": "bf0203a794a07aac3ff764972ab9087baa4e4f4e"
90
90
  },
91
- "gitHead": "da6de417e9cf98f5f36d44c2d380267e91335905",
91
+ "gitHead": "6294aa252af0ba2226afbed22d26a3fdf767a37d",
92
92
  "inlineRelease": {
93
- "tag": "hermes-agent-v1.0.0-alpha.1",
94
- "sha": "da6de417e9cf98f5f36d44c2d380267e91335905"
93
+ "tag": "hermes-agent-v1.0.0-alpha.3",
94
+ "sha": "6294aa252af0ba2226afbed22d26a3fdf767a37d"
95
95
  }
96
96
  }
@@ -1176,18 +1176,19 @@ class InlineAdapter(BasePlatformAdapter):
1176
1176
  return {**(metadata or {}), "thread_id": thread_id} if thread_id else metadata
1177
1177
 
1178
1178
  def set_reply_threads_from_tool(
1179
- self, *, chat_id: str, chat_type: str, in_reply_thread: bool, actor_id: str, actor_is_bot: bool, mode: str,
1179
+ self, *, chat_id: str, chat_type: str, parent_chat_id: str, actor_id: str, actor_is_bot: bool, mode: str,
1180
1180
  ) -> Dict[str, Any]:
1181
- """Change this chat's reply threads because the person talking to the agent asked,
1182
- with the access /threads needs."""
1181
+ """Change reply threads because the person talking to the agent asked, with the access
1182
+ /threads needs. Inside a reply thread it changes the parent chat, as /threads does."""
1183
1183
  if actor_is_bot or not actor_id:
1184
1184
  raise PermissionError("only a person can change reply threads")
1185
- if in_reply_thread:
1186
- raise PermissionError("change reply threads from the parent chat, not inside a thread")
1185
+ target_chat_id = parent_chat_id or chat_id
1186
+ if parent_chat_id and self._actor_authorization(chat_type, actor_id, parent_chat_id) is not True:
1187
+ raise PermissionError("parent chat access could not be confirmed")
1187
1188
  if not self._may_change_reply_threads(actor_id, chat_type):
1188
1189
  raise PermissionError("changing reply threads needs access to the threads command")
1189
- self._set_reply_threads_for_chat(chat_id, None if mode in {"default", "reset"} else _reply_thread_mode(mode))
1190
- return {"chat_id": self._chat_key(chat_id), "mode": self._reply_thread_mode_for_chat(chat_id)}
1190
+ self._set_reply_threads_for_chat(target_chat_id, None if mode in {"default", "reset"} else _reply_thread_mode(mode))
1191
+ return {"chat_id": self._chat_key(target_chat_id), "mode": self._reply_thread_mode_for_chat(target_chat_id)}
1191
1192
 
1192
1193
  @staticmethod
1193
1194
  def _bot_settings_chat_type(info: Dict[str, Any]) -> str:
@@ -2795,7 +2796,8 @@ class InlineAdapter(BasePlatformAdapter):
2795
2796
  _inline_tools.remember_turn_message(
2796
2797
  session_chat_id=session_chat_id, thread_id=str(thread_id or ""), message_id=msg_id,
2797
2798
  chat_id=chat_id, sender_id=from_id, sender_is_bot=sender_profile.get("bot") is True,
2798
- chat_type=chat_type, in_reply_thread=bool(parent_chat_id) and parent_chat_id != chat_id,
2799
+ chat_type=chat_type,
2800
+ parent_chat_id=parent_chat_id if parent_chat_id and parent_chat_id != chat_id else "",
2799
2801
  )
2800
2802
  except Exception:
2801
2803
  logger.debug("[inline] failed to record the turn's message for tools", exc_info=True)
@@ -27,7 +27,7 @@ _CLI_INSTALL_URL = "https://inline.chat/cli/install.sh"
27
27
  _MAX_TOKEN_BYTES = 16 * 1024
28
28
  _MAX_PROBE_RESPONSE_BYTES = 64 * 1024
29
29
  _MACHINE_SETUP_PROTOCOL_VERSION = 1
30
- _PROBE_USER_AGENT = "inline-hermes-agent-adapter/1.0.0-alpha.1"
30
+ _PROBE_USER_AGENT = "inline-hermes-agent-adapter/1.0.0-alpha.3"
31
31
  _ENV_REFERENCE_RE = re.compile(r"^\$\{([A-Za-z_][A-Za-z0-9_]*)\}$")
32
32
 
33
33
 
@@ -3,7 +3,7 @@ label: Inline
3
3
  kind: platform
4
4
  provides_tools:
5
5
  - inline
6
- version: 1.0.0-alpha.1
6
+ version: 1.0.0-alpha.3
7
7
  requires_hermes: ">=0.21.3"
8
8
  description: >
9
9
  Inline platform adapter for Hermes Agent. The adapter runs as a native
@@ -2,6 +2,7 @@
2
2
  from __future__ import annotations
3
3
 
4
4
  import json
5
+ import http.client
5
6
  from collections import OrderedDict
6
7
  import os
7
8
  import re
@@ -87,12 +88,13 @@ _TURN_MESSAGES_MAX = 1000
87
88
 
88
89
  def remember_turn_message(
89
90
  *, session_chat_id: str, thread_id: str, message_id: str, chat_id: str, sender_id: str, sender_is_bot: bool,
90
- chat_type: str = "", in_reply_thread: bool = False,
91
+ chat_type: str = "", parent_chat_id: str = "",
91
92
  ) -> None:
93
+ """``parent_chat_id`` is set only when the message itself was sent inside a reply thread."""
92
94
  key = (str(hermes_home()), str(session_chat_id or ""), str(thread_id or ""), str(message_id or ""))
93
95
  _turn_messages[key] = {
94
96
  "chatId": str(chat_id), "senderId": str(sender_id or ""), "senderIsBot": bool(sender_is_bot),
95
- "chatType": str(chat_type or ""), "inReplyThread": bool(in_reply_thread),
97
+ "chatType": str(chat_type or ""), "parentChatId": str(parent_chat_id or ""),
96
98
  }
97
99
  _turn_messages.move_to_end(key)
98
100
  while len(_turn_messages) > _TURN_MESSAGES_MAX:
@@ -107,14 +109,15 @@ def _current_turn() -> Dict[str, Any]:
107
109
  known = _turn_messages.get((str(hermes_home()), session_chat, thread_id, message_id)) or {}
108
110
  # The turn already runs in a thread when reply threads moved it there (its message lives
109
111
  # in the parent chat) or when its message was sent inside the thread.
110
- in_thread = bool(thread_id) and (bool(known.get("inReplyThread")) or (known.get("chatId") or session_chat) != thread_id)
112
+ parent_chat_id = known.get("parentChatId") or ""
113
+ in_thread = bool(thread_id) and (bool(parent_chat_id) or (known.get("chatId") or session_chat) != thread_id)
111
114
  return {
112
115
  "messageId": message_id,
113
116
  "chatId": known.get("chatId") or session_chat,
114
117
  "senderId": known.get("senderId") or "",
115
118
  "senderIsBot": known.get("senderIsBot"),
116
119
  "chatType": known.get("chatType") or "",
117
- "inReplyThread": bool(known.get("inReplyThread")),
120
+ "parentChatId": parent_chat_id,
118
121
  "threadId": thread_id if in_thread else "",
119
122
  }
120
123
 
@@ -242,7 +245,10 @@ def tool_context_prompt(
242
245
  lines.append(f"- Current Inline reply thread: `{thread_id}`. Use this as `chat_id` for thread-scoped reads.")
243
246
  lines.append(f"- Link the current thread as `[this thread](inline://thread?id={thread_id})` when asked for a thread link.")
244
247
  if parent_chat_id:
245
- lines.append(f"- Parent Inline chat: `{parent_chat_id}`.")
248
+ lines.append(
249
+ f"- Parent Inline chat: `{parent_chat_id}`. Earlier conversation lives there; "
250
+ "read it with get_history or search_messages using that chat_id."
251
+ )
246
252
  else:
247
253
  lines.append(f"- Current Inline chat: `{chat_id}`.")
248
254
  lines.append(f"- Link the current chat as `[this chat](inline://chat?id={chat_id})` when asked for a chat link.")
@@ -265,7 +271,7 @@ INLINE_TOOL_SCHEMA = {
265
271
  "When chat_id is omitted, the tool uses the current Inline reply thread if present, otherwise the current chat. "
266
272
  "Return normal assistant replies as text. Use send_message only for a button card or a different destination. "
267
273
  "Agent-owned button presses return as ordinary turns, and the normal response replaces the source card while clearing omitted buttons. "
268
- "Use create_thread on the current triggering message when someone asks to continue in a thread; your answer then goes into it, so don't also post it in the chat. "
274
+ "Use create_thread on the current triggering message when someone asks to continue in a thread; your answer then goes into it, so don't also post it in the chat, and it starts with a short summary of the conversation so far. "
269
275
  "Use set_reply_threads when someone asks you to always or never answer in threads here. "
270
276
  "Use create_chat to create a new top-level destination outside the current conversation; it defaults to private, and public creation must explicitly set is_public with a space_id. "
271
277
  "Use search_messages for exact catch-up across older chat history. "
@@ -619,7 +625,7 @@ def _set_reply_threads(args: Dict[str, Any]) -> Dict[str, Any]:
619
625
  return owner.set_reply_threads_from_tool(
620
626
  chat_id=turn["chatId"],
621
627
  chat_type=turn.get("chatType") or "group",
622
- in_reply_thread=bool(turn.get("inReplyThread")),
628
+ parent_chat_id=turn.get("parentChatId") or "",
623
629
  actor_id=turn["senderId"],
624
630
  actor_is_bot=bool(turn.get("senderIsBot")),
625
631
  mode=mode,
@@ -646,9 +652,18 @@ def _create_thread(args: Dict[str, Any]) -> Dict[str, Any]:
646
652
  thread_id = _inline_id(result.get("chatId"))
647
653
  if for_this_turn and owner is not None and thread_id:
648
654
  owner.note_turn_thread(turn["chatId"], turn["messageId"], thread_id)
655
+ result["message"] = _NEW_TURN_THREAD_MESSAGE
649
656
  return result
650
657
 
651
658
 
659
+ # Later messages in a thread opened mid-turn start a new session that reads the thread's
660
+ # history, so the answer that opens it carries what matters from this conversation.
661
+ _NEW_TURN_THREAD_MESSAGE = (
662
+ "Your answer goes into this thread. Start it with a short summary of what matters from the "
663
+ "conversation so far; later messages here start a new session that reads this thread."
664
+ )
665
+
666
+
652
667
  _FOLLOW_CONFIRMATIONS = {
653
668
  "following": "Following this chat.",
654
669
  "unfollowed": "Unfollowed; mentions and replies still reach me.",
@@ -680,13 +695,16 @@ def _sidecar_call(path: str, body: Dict[str, Any]) -> Dict[str, Any]:
680
695
  method="POST",
681
696
  )
682
697
  try:
683
- with urllib.request.urlopen(request, timeout=15) as response:
698
+ # IPC stays on loopback even when the host configures an HTTP proxy.
699
+ # The sidecar token must never be forwarded to that proxy.
700
+ opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))
701
+ with opener.open(request, timeout=15) as response:
684
702
  raw = response.read().decode("utf-8")
685
703
  status = response.status
686
704
  except urllib.error.HTTPError as exc:
687
705
  raw = exc.read().decode("utf-8", errors="replace")
688
706
  status = exc.code
689
- except urllib.error.URLError as exc:
707
+ except (OSError, http.client.HTTPException) as exc:
690
708
  raise InlineToolError(f"Inline sidecar is not reachable: {exc}", "transient") from exc
691
709
 
692
710
  try: