@inline-chat/hermes-agent-adapter 0.0.2 → 0.0.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/README.md +7 -7
- package/package.json +2 -2
- package/plugin/inline/adapter.py +385 -49
- package/plugin/inline/plugin.yaml +5 -5
- package/plugin/inline/sidecar/index.mjs +70 -17
- package/plugin/inline/tools.py +12 -34
package/README.md
CHANGED
|
@@ -27,7 +27,7 @@ Supported:
|
|
|
27
27
|
- Realtime inbound messages, catch-up, replies to bot messages, and action callbacks.
|
|
28
28
|
- Outbound text, Markdown parsing, opt-in edit-message streaming, long-message splitting, edits, deletes, typing, and presence.
|
|
29
29
|
- Inline reply-thread routing, default DM/group auto-threading, `/threads` controls, parent chat metadata, parent/thread prompt fallback, and thread-specific skill bindings.
|
|
30
|
-
- Native Hermes `inline` tool for current-chat/thread reads, bounded history and search, exact message lookup,
|
|
30
|
+
- Native Hermes `inline` tool for current-chat/thread reads, bounded history and search, exact message lookup, editing/deleting bot-owned messages, reactions, pin/unpin/list pins, reply-thread creation, and avatar presence/status.
|
|
31
31
|
- Per-turn Inline sender/chat/thread IDs, selective reply/thread/observed context, and parent-thread context, with prompt guidance for sender mentions and current chat/thread Markdown links.
|
|
32
32
|
- OpenClaw-style entity summaries for live turns and tool-fetched history, including mentions, text links, thread links, thread-title links, code/pre blocks, bot commands, and group mentions as untrusted Hermes context.
|
|
33
33
|
- DM and group policies, user allowlists, group sender allowlists, mention requirements, strict mention mode, allowed chats, and free-response chats.
|
|
@@ -116,7 +116,7 @@ uv run ./hermes plugins list --plain --no-bundled
|
|
|
116
116
|
Expected local output includes:
|
|
117
117
|
|
|
118
118
|
```text
|
|
119
|
-
enabled user 0.0.
|
|
119
|
+
enabled user 0.0.3 inline-platform
|
|
120
120
|
```
|
|
121
121
|
|
|
122
122
|
## Update Or Reinstall
|
|
@@ -142,7 +142,7 @@ mismatch, rerun the same command after rebuilding or upgrading the package.
|
|
|
142
142
|
against Hermes Agent `0.17.0` from source commit `824f2279`.
|
|
143
143
|
- Node.js: `>=20` is required for the bundled sidecar. Hermes-managed Node 22,
|
|
144
144
|
system Node, or an explicit `INLINE_NODE_BIN` path all work.
|
|
145
|
-
- Inline transport: the sidecar uses `@inline-chat/realtime-sdk@0.0.
|
|
145
|
+
- Inline transport: the sidecar uses `@inline-chat/realtime-sdk@0.0.12` and is
|
|
146
146
|
bundled into the npm package, so Hermes startup does not run `npm install`.
|
|
147
147
|
- Live sends require a valid Inline user or bot token in `INLINE_TOKEN`,
|
|
148
148
|
`INLINE_BOT_TOKEN`, `platforms.inline.token`, or `inline.token`.
|
|
@@ -249,18 +249,18 @@ Access control follows Hermes' native platform model:
|
|
|
249
249
|
| `INLINE_DM_POLICY=open|allowlist|disabled` | Controls direct-message intake. `allowlist` requires `INLINE_ALLOWED_USERS` or config `allow_from`. |
|
|
250
250
|
| `INLINE_GROUP_POLICY=open|allowlist|disabled` | Controls group intake. `allowlist` requires `INLINE_GROUP_ALLOW_FROM`. |
|
|
251
251
|
| `INLINE_GROUP_ALLOW_FROM` | Comma-separated Inline user ids allowed to invoke the bot from group chats. |
|
|
252
|
-
| `INLINE_REQUIRE_MENTION` | Requires a mention or wake word in groups by default. Replies to the bot are accepted without the wake word. |
|
|
253
|
-
| `INLINE_STRICT_MENTION` | Requires a mention or wake word on every group turn, including replies to the bot. Defaults to `false`. |
|
|
252
|
+
| `INLINE_REQUIRE_MENTION` | Requires a mention or wake word in groups by default. Replies to the bot and followed eligible reply/fresh threads are accepted without the wake word. |
|
|
253
|
+
| `INLINE_STRICT_MENTION` | Requires a mention or wake word on every group turn, including replies to the bot and followed threads. Defaults to `false`. |
|
|
254
254
|
| `INLINE_ALLOWED_CHATS` | Comma-separated group/thread chat ids where the bot may respond. Parent chat ids also match their Inline reply threads. DMs are not filtered. Empty means no chat restriction. |
|
|
255
255
|
| `INLINE_FREE_RESPONSE_CHATS` | Comma-separated group/thread chat ids where no mention is required. Parent chat ids also match their Inline reply threads. Useful for dedicated agent rooms. |
|
|
256
|
-
| `INLINE_REPLY_THREADS` |
|
|
256
|
+
| `INLINE_REPLY_THREADS` | Controls top-level reply-thread creation. `auto` creates child reply threads only for large parent chats (`messageId >= 50`) or explicit thread requests; `on` always creates; `off` stays flat. Defaults to `auto`. |
|
|
257
257
|
| `INLINE_CONTEXT_BACKFILL` | Automatic context mode. `selective` is the default, `off` disables automatic history windows, and `always` restores recent-history backfill on every turn. |
|
|
258
258
|
| `INLINE_THREAD_CONTEXT_LIMIT` | Max current chat/thread messages for selective thread or mention-gap context. Must be `0` through `100`; defaults to `30`. |
|
|
259
259
|
| `INLINE_REPLY_CONTEXT_LIMIT` | Max messages in the anchored window around a replied-to Inline message. Must be `0` through `50`; defaults to `10`. |
|
|
260
260
|
| `INLINE_OBSERVED_CONTEXT_LIMIT` | Max unmentioned group messages kept in the observed-context buffer. Must be `0` through `100`; defaults to `20`. |
|
|
261
261
|
| `INLINE_OBSERVE_UNMENTIONED_MESSAGES` | Buffers unmentioned group messages that pass chat/user policy but do not wake the bot. Defaults to `true`; set to `false` to disable. |
|
|
262
262
|
| `INLINE_CONTEXT_HISTORY_LIMIT` | Legacy compatibility shortcut. `0` maps to `INLINE_CONTEXT_BACKFILL=off`; `1` through `20` maps to `always` with that thread-context limit. Prefer the explicit settings above. |
|
|
263
|
-
| `INLINE_SETTINGS_PATH` | JSON settings file for per-chat `/threads` overrides. Defaults next to `INLINE_STATE_PATH`; `.env`-like paths are refused. |
|
|
263
|
+
| `INLINE_SETTINGS_PATH` | JSON settings file for per-chat `/threads` overrides. `/threads` shows native Auto/On/Off buttons; `reset` clears the chat override back to the global default. Defaults next to `INLINE_STATE_PATH`; `.env`-like paths are refused. |
|
|
264
264
|
| `INLINE_SYSTEM_EVENTS` | Delivers Inline lifecycle events such as edits, deletes, and participant changes as synthetic messages. Defaults to `false`. Reactions on bot messages are always delivered. |
|
|
265
265
|
| `INLINE_MENTION_PATTERNS` | JSON list, comma-separated, or newline-separated regex patterns for group wake words. |
|
|
266
266
|
| `INLINE_PARSE_MARKDOWN` | Controls whether outbound Hermes Markdown is parsed by Inline. Defaults to `true`. |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inline-chat/hermes-agent-adapter",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.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",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"check": "bun run typecheck && bun run lint && bun run test"
|
|
60
60
|
},
|
|
61
61
|
"dependencies": {
|
|
62
|
-
"@inline-chat/realtime-sdk": "0.0.
|
|
62
|
+
"@inline-chat/realtime-sdk": "0.0.12",
|
|
63
63
|
"yaml": "2.9.0"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
package/plugin/inline/adapter.py
CHANGED
|
@@ -78,12 +78,34 @@ _INLINE_COMMAND_LIMIT = 100
|
|
|
78
78
|
_INLINE_COMMAND_DESCRIPTION_LIMIT = 256
|
|
79
79
|
_INLINE_COMMAND_RETRY_RATIO = 0.8
|
|
80
80
|
_INLINE_COMMAND_RE = re.compile(r"^[a-z0-9_]{1,32}$")
|
|
81
|
+
_REPLY_THREAD_AUTO_CREATE_MIN_MESSAGES = 50
|
|
81
82
|
_INLINE_THREADS_COMMAND_DESCRIPTION = "Configure Inline reply-thread routing"
|
|
82
|
-
_INLINE_THREADS_COMMAND_ARGS = "[status|on|off|auto]"
|
|
83
|
+
_INLINE_THREADS_COMMAND_ARGS = "[status|on|off|auto|reset]"
|
|
84
|
+
_INLINE_THREADS_ACTION_PREFIX = "th:"
|
|
85
|
+
_INLINE_THREADS_ACTION_TTL_SECONDS = 15 * 60
|
|
83
86
|
_INLINE_LOCAL_COMMANDS = (
|
|
84
87
|
("threads", _INLINE_THREADS_COMMAND_DESCRIPTION),
|
|
85
88
|
)
|
|
86
89
|
_INLINE_THREAD_COMMAND_RE = re.compile(r"^/(?:thread|threads)(?:@[A-Za-z0-9_]+)?(?:\s+(.*))?$", re.IGNORECASE)
|
|
90
|
+
_INLINE_REPLY_THREAD_NEGATION_RE = re.compile(
|
|
91
|
+
r"\b(?:do\s+not|don't|dont|please\s+don't|please\s+dont|no\s+need\s+to)\s+"
|
|
92
|
+
r"(?:create|start|open|make|use|move|take|reply|respond|answer|send|thread)\b[^.!?\n]*\bthread\b|"
|
|
93
|
+
r"\b(?:reply|respond|answer|keep)\s+(?:here|in\s+the\s+main\s+chat|in\s+main\s+chat|"
|
|
94
|
+
r"in\s+the\s+parent\s+chat|in\s+parent\s+chat)\b",
|
|
95
|
+
re.IGNORECASE,
|
|
96
|
+
)
|
|
97
|
+
_INLINE_REPLY_THREAD_INTENT_RE = re.compile(
|
|
98
|
+
r"\b(?:reply|respond|answer|send)\s+(?:in|inside|into|to)\s+(?:a\s+)?"
|
|
99
|
+
r"(?:(?:new|child|reply)\s+)?thread\b|"
|
|
100
|
+
r"\b(?:create|start|open|make|use)\s+(?:a\s+)?(?:(?:new|child|reply)\s+)?thread\b|"
|
|
101
|
+
r"\bkeep\s+(?:(?:this|it|the\s+answer|the\s+reply|the\s+response)\s+)?"
|
|
102
|
+
r"(?:in|inside|into)\s+(?:a\s+)?(?:(?:new|child|reply)\s+)?thread\b|"
|
|
103
|
+
r"\b(?:move|take)\s+(?:(?:this|it|the\s+answer|the\s+reply|the\s+response)\s+)?"
|
|
104
|
+
r"(?:to|into)\s+(?:a\s+)?(?:(?:new|child|reply)\s+)?thread\b|"
|
|
105
|
+
r"\bthread\s+(?:this|the\s+answer|the\s+reply|the\s+response)\b|"
|
|
106
|
+
r"\b(?:threaded\s+(?:reply|response)|(?:reply|respond|answer)\s+threaded)\b",
|
|
107
|
+
re.IGNORECASE,
|
|
108
|
+
)
|
|
87
109
|
_INLINE_SETTINGS_VERSION = 1
|
|
88
110
|
_INLINE_ENTITY_LIMIT = 12
|
|
89
111
|
_INLINE_ENTITY_TEXT_LIMIT = 120
|
|
@@ -165,14 +187,16 @@ def _install_inline_display_defaults() -> None:
|
|
|
165
187
|
logger.debug("[inline] failed to install display defaults", exc_info=True)
|
|
166
188
|
|
|
167
189
|
|
|
168
|
-
def
|
|
190
|
+
def _reply_thread_mode(value: Any, default: str = "auto") -> str:
|
|
169
191
|
if value is None or str(value).strip() == "":
|
|
170
192
|
return default
|
|
171
193
|
text = str(value).strip().lower()
|
|
172
|
-
if text in {"
|
|
173
|
-
return
|
|
174
|
-
if text in {"
|
|
175
|
-
return
|
|
194
|
+
if text in {"auto", "default", "reset", "config"}:
|
|
195
|
+
return "auto"
|
|
196
|
+
if text in {"1", "true", "yes", "on", "always", "thread", "threads"}:
|
|
197
|
+
return "on"
|
|
198
|
+
if text in {"0", "false", "no", "off", "never", "flat", "channel", "main"}:
|
|
199
|
+
return "off"
|
|
176
200
|
logger.warning("[inline] unknown reply_threads value %r; using %s", value, default)
|
|
177
201
|
return default
|
|
178
202
|
|
|
@@ -655,9 +679,9 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
655
679
|
else os.getenv("INLINE_OBSERVE_UNMENTIONED_MESSAGES"),
|
|
656
680
|
True,
|
|
657
681
|
)
|
|
658
|
-
self.
|
|
682
|
+
self._reply_thread_mode = _reply_thread_mode(
|
|
659
683
|
extra.get("reply_threads") if "reply_threads" in extra else os.getenv("INLINE_REPLY_THREADS"),
|
|
660
|
-
|
|
684
|
+
"auto",
|
|
661
685
|
)
|
|
662
686
|
|
|
663
687
|
state_path = extra.get("state_path") or os.getenv("INLINE_STATE_PATH")
|
|
@@ -719,6 +743,7 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
719
743
|
|
|
720
744
|
self._sidecar_proc: Optional[subprocess.Popen] = None
|
|
721
745
|
self._sidecar_supervisor_task: Optional[asyncio.Task] = None
|
|
746
|
+
self._command_sync_task: Optional[asyncio.Task] = None
|
|
722
747
|
self._inbound_task: Optional[asyncio.Task] = None
|
|
723
748
|
self._inbound_running = False
|
|
724
749
|
self._http_client: Optional[httpx.AsyncClient] = None
|
|
@@ -729,9 +754,13 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
729
754
|
self._approval_sessions: "OrderedDict[str, str]" = OrderedDict()
|
|
730
755
|
self._slash_sessions: "OrderedDict[str, str]" = OrderedDict()
|
|
731
756
|
self._model_picker_sessions: "OrderedDict[str, Dict[str, Any]]" = OrderedDict()
|
|
757
|
+
self._thread_action_sessions: "OrderedDict[str, Dict[str, Any]]" = OrderedDict()
|
|
732
758
|
self._chat_info_cache: "OrderedDict[str, tuple[float, Dict[str, Any]]]" = OrderedDict()
|
|
733
759
|
self._reply_thread_cache: "OrderedDict[str, str]" = OrderedDict()
|
|
734
760
|
self._reply_thread_parent_reply_ids: "OrderedDict[str, set[str]]" = OrderedDict()
|
|
761
|
+
self._reply_thread_parent_typing_targets: "OrderedDict[str, str]" = OrderedDict()
|
|
762
|
+
self._visible_reply_thread_targets: "OrderedDict[str, None]" = OrderedDict()
|
|
763
|
+
self._active_typing_targets: Dict[str, Dict[str, str]] = {}
|
|
735
764
|
self._observed_context: "OrderedDict[str, List[Dict[str, Any]]]" = OrderedDict()
|
|
736
765
|
self._context_backfill_seen: "OrderedDict[str, float]" = OrderedDict()
|
|
737
766
|
self._reply_thread_overrides = self._load_reply_thread_overrides()
|
|
@@ -771,7 +800,7 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
771
800
|
return False
|
|
772
801
|
return True
|
|
773
802
|
|
|
774
|
-
def _load_reply_thread_overrides(self) -> Dict[str,
|
|
803
|
+
def _load_reply_thread_overrides(self) -> Dict[str, str]:
|
|
775
804
|
if not self._settings_path_allowed():
|
|
776
805
|
return {}
|
|
777
806
|
try:
|
|
@@ -784,11 +813,17 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
784
813
|
raw = data.get("reply_threads") if isinstance(data, dict) else None
|
|
785
814
|
if not isinstance(raw, dict):
|
|
786
815
|
return {}
|
|
787
|
-
overrides: Dict[str,
|
|
816
|
+
overrides: Dict[str, str] = {}
|
|
788
817
|
for chat_id, enabled in raw.items():
|
|
789
818
|
key = self._chat_key(chat_id)
|
|
790
|
-
if key
|
|
791
|
-
|
|
819
|
+
if not key:
|
|
820
|
+
continue
|
|
821
|
+
if isinstance(enabled, bool):
|
|
822
|
+
overrides[key] = "on" if enabled else "off"
|
|
823
|
+
continue
|
|
824
|
+
mode = _reply_thread_mode(enabled, "")
|
|
825
|
+
if mode:
|
|
826
|
+
overrides[key] = mode
|
|
792
827
|
return overrides
|
|
793
828
|
|
|
794
829
|
def _save_reply_thread_overrides(self) -> None:
|
|
@@ -806,22 +841,62 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
806
841
|
except Exception as exc:
|
|
807
842
|
logger.warning("[inline] failed to save Inline adapter settings: %s", exc)
|
|
808
843
|
|
|
809
|
-
def
|
|
844
|
+
def _reply_thread_mode_for_chat(self, chat_id: str, parent_chat_id: Optional[str] = None) -> str:
|
|
810
845
|
key = self._chat_key(parent_chat_id or chat_id)
|
|
811
846
|
if key in self._reply_thread_overrides:
|
|
812
847
|
return self._reply_thread_overrides[key]
|
|
813
|
-
return self.
|
|
848
|
+
return self._reply_thread_mode
|
|
814
849
|
|
|
815
|
-
def
|
|
850
|
+
def _reply_threads_for_chat(self, chat_id: str, parent_chat_id: Optional[str] = None) -> bool:
|
|
851
|
+
return self._reply_thread_mode_for_chat(chat_id, parent_chat_id) != "off"
|
|
852
|
+
|
|
853
|
+
def _set_reply_threads_for_chat(self, chat_id: str, value: Optional[str]) -> None:
|
|
816
854
|
key = self._chat_key(chat_id)
|
|
817
855
|
if not key:
|
|
818
856
|
return
|
|
819
857
|
if value is None:
|
|
820
858
|
self._reply_thread_overrides.pop(key, None)
|
|
821
859
|
else:
|
|
822
|
-
self._reply_thread_overrides[key] = value
|
|
860
|
+
self._reply_thread_overrides[key] = _reply_thread_mode(value, "auto")
|
|
823
861
|
self._save_reply_thread_overrides()
|
|
824
862
|
|
|
863
|
+
@staticmethod
|
|
864
|
+
def _positive_int(value: Any) -> Optional[int]:
|
|
865
|
+
text = str(value or "").strip()
|
|
866
|
+
if not re.fullmatch(r"[1-9]\d*", text):
|
|
867
|
+
return None
|
|
868
|
+
try:
|
|
869
|
+
return int(text)
|
|
870
|
+
except ValueError:
|
|
871
|
+
return None
|
|
872
|
+
|
|
873
|
+
@staticmethod
|
|
874
|
+
def _has_reply_thread_intent(text: str) -> bool:
|
|
875
|
+
normalized = re.sub(r"\s+", " ", str(text or "").strip())
|
|
876
|
+
if not normalized:
|
|
877
|
+
return False
|
|
878
|
+
if _INLINE_REPLY_THREAD_NEGATION_RE.search(normalized):
|
|
879
|
+
return False
|
|
880
|
+
return bool(_INLINE_REPLY_THREAD_INTENT_RE.search(normalized))
|
|
881
|
+
|
|
882
|
+
def _should_create_reply_thread_for_message(
|
|
883
|
+
self,
|
|
884
|
+
*,
|
|
885
|
+
chat_id: str,
|
|
886
|
+
parent_chat_id: Optional[str],
|
|
887
|
+
msg_id: str,
|
|
888
|
+
text: str,
|
|
889
|
+
) -> bool:
|
|
890
|
+
mode = self._reply_thread_mode_for_chat(chat_id, parent_chat_id)
|
|
891
|
+
if mode == "off":
|
|
892
|
+
return False
|
|
893
|
+
if mode == "on":
|
|
894
|
+
return True
|
|
895
|
+
if self._has_reply_thread_intent(text):
|
|
896
|
+
return True
|
|
897
|
+
message_id = self._positive_int(msg_id)
|
|
898
|
+
return message_id is not None and message_id >= _REPLY_THREAD_AUTO_CREATE_MIN_MESSAGES
|
|
899
|
+
|
|
825
900
|
@staticmethod
|
|
826
901
|
def _id_allowed(entries: set[str], value: str) -> bool:
|
|
827
902
|
normalized = str(value or "").strip().lower()
|
|
@@ -889,10 +964,89 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
889
964
|
return "on"
|
|
890
965
|
if action in {"off", "disable", "disabled", "false", "flat", "channel"}:
|
|
891
966
|
return "off"
|
|
892
|
-
if action in {"auto", "
|
|
967
|
+
if action in {"auto", "config"}:
|
|
893
968
|
return "auto"
|
|
969
|
+
if action in {"reset", "default", "inherit", "clear"}:
|
|
970
|
+
return "reset"
|
|
894
971
|
return "help"
|
|
895
972
|
|
|
973
|
+
def _reply_thread_status_body(self, target_chat_id: str, *, controls: bool = True, updated: bool = False) -> str:
|
|
974
|
+
mode = self._reply_thread_mode_for_chat(target_chat_id)
|
|
975
|
+
key = self._chat_key(target_chat_id)
|
|
976
|
+
has_override = key in self._reply_thread_overrides
|
|
977
|
+
scope = "chat override" if has_override else "global default"
|
|
978
|
+
if mode == "on":
|
|
979
|
+
behavior = "Top-level replies will start or reuse Inline reply threads."
|
|
980
|
+
elif mode == "auto":
|
|
981
|
+
behavior = (
|
|
982
|
+
"Top-level replies stay in the parent chat until "
|
|
983
|
+
f"message #{_REPLY_THREAD_AUTO_CREATE_MIN_MESSAGES} or an explicit thread request."
|
|
984
|
+
)
|
|
985
|
+
else:
|
|
986
|
+
behavior = "Top-level replies stay in the parent chat."
|
|
987
|
+
first_line = f"Inline reply threads are {mode} for this chat ({scope})."
|
|
988
|
+
if updated:
|
|
989
|
+
first_line = f"Inline reply threads updated: {mode} for this chat ({scope})."
|
|
990
|
+
footer = "Existing Inline reply threads are always preserved."
|
|
991
|
+
if controls:
|
|
992
|
+
footer = f"{footer} Use Auto, On, Off, or Reset."
|
|
993
|
+
return (
|
|
994
|
+
f"{first_line}\n"
|
|
995
|
+
f"{behavior}\n"
|
|
996
|
+
f"{footer}"
|
|
997
|
+
)
|
|
998
|
+
|
|
999
|
+
def _new_thread_action_session(self, *, display_chat_id: str, target_chat_id: str) -> str:
|
|
1000
|
+
session_id = secrets.token_hex(6)
|
|
1001
|
+
self._remember(self._thread_action_sessions, session_id, {
|
|
1002
|
+
"created_at": time.time(),
|
|
1003
|
+
"display_chat_id": self._chat_key(display_chat_id),
|
|
1004
|
+
"target_chat_id": self._chat_key(target_chat_id),
|
|
1005
|
+
})
|
|
1006
|
+
return session_id
|
|
1007
|
+
|
|
1008
|
+
def _thread_action_state(self, session_id: str, chat_id: str) -> Optional[Dict[str, Any]]:
|
|
1009
|
+
state = self._thread_action_sessions.get(session_id)
|
|
1010
|
+
if not state:
|
|
1011
|
+
return None
|
|
1012
|
+
created_at = float(state.get("created_at") or 0)
|
|
1013
|
+
if created_at and time.time() - created_at > _INLINE_THREADS_ACTION_TTL_SECONDS:
|
|
1014
|
+
self._thread_action_sessions.pop(session_id, None)
|
|
1015
|
+
return None
|
|
1016
|
+
display_chat_id = self._chat_key(state.get("display_chat_id"))
|
|
1017
|
+
if display_chat_id and display_chat_id != self._chat_key(chat_id):
|
|
1018
|
+
return None
|
|
1019
|
+
return state
|
|
1020
|
+
|
|
1021
|
+
def _thread_status_actions(self, *, display_chat_id: str, target_chat_id: str, session_id: Optional[str] = None) -> Dict[str, Any]:
|
|
1022
|
+
if not session_id:
|
|
1023
|
+
session_id = self._new_thread_action_session(display_chat_id=display_chat_id, target_chat_id=target_chat_id)
|
|
1024
|
+
prefix = f"{_INLINE_THREADS_ACTION_PREFIX}{session_id}:"
|
|
1025
|
+
rows = [{"actions": [
|
|
1026
|
+
self._action(f"{prefix}auto", "Auto"),
|
|
1027
|
+
self._action(f"{prefix}on", "On"),
|
|
1028
|
+
self._action(f"{prefix}off", "Off"),
|
|
1029
|
+
]}]
|
|
1030
|
+
if self._chat_key(target_chat_id) in self._reply_thread_overrides:
|
|
1031
|
+
rows.append({"actions": [self._action(f"{prefix}reset", "Reset")]})
|
|
1032
|
+
return {"rows": rows}
|
|
1033
|
+
|
|
1034
|
+
async def _send_thread_status(
|
|
1035
|
+
self,
|
|
1036
|
+
*,
|
|
1037
|
+
chat_id: str,
|
|
1038
|
+
target_chat_id: str,
|
|
1039
|
+
reply_to: Optional[str],
|
|
1040
|
+
metadata: Optional[Dict[str, Any]],
|
|
1041
|
+
) -> SendResult:
|
|
1042
|
+
return await self.send(
|
|
1043
|
+
chat_id,
|
|
1044
|
+
self._reply_thread_status_body(target_chat_id),
|
|
1045
|
+
reply_to=reply_to,
|
|
1046
|
+
metadata=metadata,
|
|
1047
|
+
actions=self._thread_status_actions(display_chat_id=chat_id, target_chat_id=target_chat_id),
|
|
1048
|
+
)
|
|
1049
|
+
|
|
896
1050
|
async def _handle_thread_command(
|
|
897
1051
|
self,
|
|
898
1052
|
*,
|
|
@@ -910,31 +1064,24 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
910
1064
|
metadata = {"thread_id": thread_id} if thread_id else None
|
|
911
1065
|
target_chat_id = parent_chat_id or chat_id
|
|
912
1066
|
if action == "on":
|
|
913
|
-
self._set_reply_threads_for_chat(target_chat_id,
|
|
1067
|
+
self._set_reply_threads_for_chat(target_chat_id, "on")
|
|
914
1068
|
elif action == "off":
|
|
915
|
-
self._set_reply_threads_for_chat(target_chat_id,
|
|
1069
|
+
self._set_reply_threads_for_chat(target_chat_id, "off")
|
|
916
1070
|
elif action == "auto":
|
|
1071
|
+
self._set_reply_threads_for_chat(target_chat_id, "auto")
|
|
1072
|
+
elif action == "reset":
|
|
917
1073
|
self._set_reply_threads_for_chat(target_chat_id, None)
|
|
918
1074
|
|
|
919
1075
|
if action == "help":
|
|
920
|
-
body = "Usage: /threads status, /threads on, /threads off, or /threads
|
|
1076
|
+
body = "Usage: /threads status, /threads auto, /threads on, /threads off, or /threads reset."
|
|
1077
|
+
await self.send(chat_id, body, reply_to=msg_id, metadata=metadata)
|
|
921
1078
|
else:
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
behavior = (
|
|
928
|
-
"Top-level replies will start or reuse Inline reply threads."
|
|
929
|
-
if enabled
|
|
930
|
-
else "Top-level replies stay in the parent chat."
|
|
931
|
-
)
|
|
932
|
-
body = (
|
|
933
|
-
f"Inline reply threads are {state} for this chat ({scope}).\n"
|
|
934
|
-
f"{behavior}\n"
|
|
935
|
-
"Existing Inline reply threads are always preserved. Use /threads on, /threads off, or /threads auto."
|
|
1079
|
+
await self._send_thread_status(
|
|
1080
|
+
chat_id=chat_id,
|
|
1081
|
+
target_chat_id=target_chat_id,
|
|
1082
|
+
reply_to=msg_id,
|
|
1083
|
+
metadata=metadata,
|
|
936
1084
|
)
|
|
937
|
-
await self.send(chat_id, body, reply_to=msg_id, metadata=metadata)
|
|
938
1085
|
return True
|
|
939
1086
|
|
|
940
1087
|
@property
|
|
@@ -953,7 +1100,7 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
953
1100
|
if node_error:
|
|
954
1101
|
self._set_fatal_error("NODE_UNSUPPORTED", node_error, retryable=False)
|
|
955
1102
|
return False
|
|
956
|
-
self._http_client = httpx.AsyncClient(timeout=30.0)
|
|
1103
|
+
self._http_client = httpx.AsyncClient(timeout=30.0, trust_env=False)
|
|
957
1104
|
if self._autostart_sidecar:
|
|
958
1105
|
try:
|
|
959
1106
|
await self._start_sidecar()
|
|
@@ -963,15 +1110,24 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
963
1110
|
await self._http_client.aclose()
|
|
964
1111
|
self._http_client = None
|
|
965
1112
|
return False
|
|
966
|
-
await self._sync_bot_commands()
|
|
967
1113
|
self._inbound_running = True
|
|
968
1114
|
self._inbound_task = asyncio.get_event_loop().create_task(self._inbound_loop())
|
|
969
1115
|
self._mark_connected()
|
|
970
1116
|
logger.info("[inline] connected via sidecar on %s:%d", self._sidecar_bind, self._sidecar_port)
|
|
1117
|
+
self._schedule_bot_command_sync()
|
|
971
1118
|
return True
|
|
972
1119
|
|
|
973
1120
|
async def disconnect(self) -> None:
|
|
974
1121
|
self._inbound_running = False
|
|
1122
|
+
if self._command_sync_task is not None:
|
|
1123
|
+
self._command_sync_task.cancel()
|
|
1124
|
+
try:
|
|
1125
|
+
await self._command_sync_task
|
|
1126
|
+
except asyncio.CancelledError:
|
|
1127
|
+
pass
|
|
1128
|
+
except Exception:
|
|
1129
|
+
pass
|
|
1130
|
+
self._command_sync_task = None
|
|
975
1131
|
if self._inbound_task is not None:
|
|
976
1132
|
self._inbound_task.cancel()
|
|
977
1133
|
try:
|
|
@@ -999,7 +1155,7 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
999
1155
|
env["INLINE_SIDECAR_TOKEN"] = self._sidecar_token
|
|
1000
1156
|
env["INLINE_STATE_PATH"] = str(self._state_path)
|
|
1001
1157
|
env["INLINE_UPLOAD_MAX_MB"] = f"{self._upload_max_mb:g}"
|
|
1002
|
-
env["INLINE_SIDECAR_WATCH_STDIN"] = "1"
|
|
1158
|
+
env["INLINE_SIDECAR_WATCH_STDIN"] = "0" if env.get("INLINE_SIDECAR_TEST_MOCK") == "1" else "1"
|
|
1003
1159
|
|
|
1004
1160
|
self._sidecar_proc = subprocess.Popen(
|
|
1005
1161
|
[self._node_bin, str(_SIDECAR_ENTRY)],
|
|
@@ -1013,7 +1169,7 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
1013
1169
|
|
|
1014
1170
|
deadline = time.time() + (self._connect_timeout_ms / 1000.0)
|
|
1015
1171
|
last_error: Optional[Exception] = None
|
|
1016
|
-
async with httpx.AsyncClient(timeout=2.0) as client:
|
|
1172
|
+
async with httpx.AsyncClient(timeout=2.0, trust_env=False) as client:
|
|
1017
1173
|
while time.time() < deadline:
|
|
1018
1174
|
if self._sidecar_proc.poll() is not None:
|
|
1019
1175
|
raise RuntimeError(f"Inline sidecar exited with code {self._sidecar_proc.returncode}")
|
|
@@ -1093,6 +1249,24 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
1093
1249
|
self._sidecar_supervisor_task.cancel()
|
|
1094
1250
|
self._sidecar_supervisor_task = None
|
|
1095
1251
|
|
|
1252
|
+
def _schedule_bot_command_sync(self) -> None:
|
|
1253
|
+
if not self._sync_commands:
|
|
1254
|
+
return
|
|
1255
|
+
if self._command_sync_task is not None and not self._command_sync_task.done():
|
|
1256
|
+
return
|
|
1257
|
+
self._command_sync_task = asyncio.get_event_loop().create_task(self._run_bot_command_sync())
|
|
1258
|
+
|
|
1259
|
+
async def _run_bot_command_sync(self) -> None:
|
|
1260
|
+
try:
|
|
1261
|
+
await self._sync_bot_commands()
|
|
1262
|
+
except asyncio.CancelledError:
|
|
1263
|
+
raise
|
|
1264
|
+
except Exception as exc:
|
|
1265
|
+
logger.warning("[inline] bot command sync failed: %s", exc)
|
|
1266
|
+
finally:
|
|
1267
|
+
if asyncio.current_task() is self._command_sync_task:
|
|
1268
|
+
self._command_sync_task = None
|
|
1269
|
+
|
|
1096
1270
|
async def _sync_bot_commands(self) -> None:
|
|
1097
1271
|
if not self._sync_commands:
|
|
1098
1272
|
return
|
|
@@ -1324,7 +1498,12 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
1324
1498
|
if mention_gate_active:
|
|
1325
1499
|
mentioned = bool(msg.get("mentioned")) or self._matches_mention(text)
|
|
1326
1500
|
reply_wakes_thread = reply_to_is_own and not self._strict_mention
|
|
1327
|
-
|
|
1501
|
+
follow_mode_wakes_thread = (
|
|
1502
|
+
self._chat_follow_mode_following(chat_info)
|
|
1503
|
+
and self._chat_follow_mode_mention_eligible(chat_info)
|
|
1504
|
+
and not self._strict_mention
|
|
1505
|
+
)
|
|
1506
|
+
if not mentioned and not reply_wakes_thread and not follow_mode_wakes_thread:
|
|
1328
1507
|
self._remember_observed_context(chat_id, msg, text)
|
|
1329
1508
|
return
|
|
1330
1509
|
if mentioned:
|
|
@@ -1334,7 +1513,12 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
1334
1513
|
if (
|
|
1335
1514
|
not edit
|
|
1336
1515
|
and not thread_id
|
|
1337
|
-
and self.
|
|
1516
|
+
and self._should_create_reply_thread_for_message(
|
|
1517
|
+
chat_id=chat_id,
|
|
1518
|
+
parent_chat_id=parent_chat_id,
|
|
1519
|
+
msg_id=msg_id,
|
|
1520
|
+
text=text,
|
|
1521
|
+
)
|
|
1338
1522
|
):
|
|
1339
1523
|
created_thread_id = await self._create_reply_thread(chat_id, msg_id, text, reply_to_id)
|
|
1340
1524
|
if created_thread_id:
|
|
@@ -2203,6 +2387,7 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
2203
2387
|
if cached:
|
|
2204
2388
|
self._reply_thread_cache.move_to_end(key)
|
|
2205
2389
|
self._remember_reply_thread_parent_reply_ids(cached, msg_id, reply_to_id)
|
|
2390
|
+
self._remember_reply_thread_parent_typing_target(cached, chat_id)
|
|
2206
2391
|
return cached
|
|
2207
2392
|
body = {
|
|
2208
2393
|
"parentChatId": str(chat_id),
|
|
@@ -2222,6 +2407,7 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
2222
2407
|
if len(self._reply_thread_cache) > _CHAT_INFO_CACHE_MAX_SIZE:
|
|
2223
2408
|
self._reply_thread_cache.popitem(last=False)
|
|
2224
2409
|
self._remember_reply_thread_parent_reply_ids(thread_id, msg_id, reply_to_id)
|
|
2410
|
+
self._remember_reply_thread_parent_typing_target(thread_id, chat_id)
|
|
2225
2411
|
return thread_id
|
|
2226
2412
|
|
|
2227
2413
|
def _remember_reply_thread_parent_reply_ids(self, thread_id: str, *message_ids: Optional[str]) -> None:
|
|
@@ -2235,6 +2421,27 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
2235
2421
|
if len(self._reply_thread_parent_reply_ids) > _CHAT_INFO_CACHE_MAX_SIZE:
|
|
2236
2422
|
self._reply_thread_parent_reply_ids.popitem(last=False)
|
|
2237
2423
|
|
|
2424
|
+
def _remember_reply_thread_parent_typing_target(self, thread_id: str, parent_chat_id: str) -> None:
|
|
2425
|
+
thread_key = self._chat_key(thread_id)
|
|
2426
|
+
parent_key = self._chat_key(parent_chat_id)
|
|
2427
|
+
if not thread_key or not parent_key:
|
|
2428
|
+
return
|
|
2429
|
+
if thread_key in self._visible_reply_thread_targets:
|
|
2430
|
+
return
|
|
2431
|
+
self._reply_thread_parent_typing_targets[thread_key] = parent_key
|
|
2432
|
+
self._reply_thread_parent_typing_targets.move_to_end(thread_key)
|
|
2433
|
+
if len(self._reply_thread_parent_typing_targets) > _CHAT_INFO_CACHE_MAX_SIZE:
|
|
2434
|
+
self._reply_thread_parent_typing_targets.popitem(last=False)
|
|
2435
|
+
|
|
2436
|
+
def _mark_reply_thread_visible(self, target: Dict[str, str]) -> None:
|
|
2437
|
+
chat_id = self._chat_key(target.get("chatId"))
|
|
2438
|
+
if chat_id:
|
|
2439
|
+
self._reply_thread_parent_typing_targets.pop(chat_id, None)
|
|
2440
|
+
self._visible_reply_thread_targets[chat_id] = None
|
|
2441
|
+
self._visible_reply_thread_targets.move_to_end(chat_id)
|
|
2442
|
+
if len(self._visible_reply_thread_targets) > _CHAT_INFO_CACHE_MAX_SIZE:
|
|
2443
|
+
self._visible_reply_thread_targets.popitem(last=False)
|
|
2444
|
+
|
|
2238
2445
|
def _reply_to_for_target(self, reply_to: Optional[str], target: Dict[str, str]) -> Optional[str]:
|
|
2239
2446
|
if not reply_to:
|
|
2240
2447
|
return None
|
|
@@ -2277,6 +2484,34 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
2277
2484
|
text = str(value).strip()
|
|
2278
2485
|
return text or None
|
|
2279
2486
|
|
|
2487
|
+
@staticmethod
|
|
2488
|
+
def _chat_follow_mode_following(info: Dict[str, Any]) -> bool:
|
|
2489
|
+
value = info.get("dialogFollowMode")
|
|
2490
|
+
dialog = info.get("dialog")
|
|
2491
|
+
if value is None and isinstance(dialog, dict):
|
|
2492
|
+
value = dialog.get("followMode")
|
|
2493
|
+
if value is None and isinstance(dialog, dict):
|
|
2494
|
+
value = dialog.get("follow_mode")
|
|
2495
|
+
if value is None:
|
|
2496
|
+
return False
|
|
2497
|
+
if isinstance(value, bool):
|
|
2498
|
+
return False
|
|
2499
|
+
if isinstance(value, int):
|
|
2500
|
+
return value == 1
|
|
2501
|
+
text = str(value).strip().lower()
|
|
2502
|
+
return text in {"1", "following", "follow_mode_following", "dialog_following"}
|
|
2503
|
+
|
|
2504
|
+
@staticmethod
|
|
2505
|
+
def _chat_follow_mode_mention_eligible(info: Dict[str, Any]) -> bool:
|
|
2506
|
+
value = info.get("followModeMentionEligible")
|
|
2507
|
+
if isinstance(value, bool):
|
|
2508
|
+
return value
|
|
2509
|
+
if value is None:
|
|
2510
|
+
return False
|
|
2511
|
+
if isinstance(value, int):
|
|
2512
|
+
return value == 1
|
|
2513
|
+
return str(value).strip().lower() in {"1", "true", "yes", "on"}
|
|
2514
|
+
|
|
2280
2515
|
@staticmethod
|
|
2281
2516
|
def _chat_title_from_info(info: Dict[str, Any]) -> Optional[str]:
|
|
2282
2517
|
value = info.get("title")
|
|
@@ -2370,6 +2605,8 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
2370
2605
|
if not await self._action_allowed(event):
|
|
2371
2606
|
return True
|
|
2372
2607
|
return await self._handle_slash_action(action_id, chat_id, interaction_id)
|
|
2608
|
+
if action_id.startswith(_INLINE_THREADS_ACTION_PREFIX):
|
|
2609
|
+
return await self._handle_thread_action(event)
|
|
2373
2610
|
return False
|
|
2374
2611
|
|
|
2375
2612
|
async def _action_allowed(self, event: Dict[str, Any]) -> bool:
|
|
@@ -2498,6 +2735,65 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
2498
2735
|
logger.exception("[inline] slash confirm action failed")
|
|
2499
2736
|
return True
|
|
2500
2737
|
|
|
2738
|
+
async def _handle_thread_action(self, event: Dict[str, Any]) -> bool:
|
|
2739
|
+
action_id = str(event.get("actionId") or "")
|
|
2740
|
+
chat_id = str(event.get("chatId") or "")
|
|
2741
|
+
interaction_id = str(event.get("interactionId") or "")
|
|
2742
|
+
parts = action_id.split(":", 2)
|
|
2743
|
+
if len(parts) != 3:
|
|
2744
|
+
await self._answer_action(interaction_id, "Thread controls expired")
|
|
2745
|
+
return True
|
|
2746
|
+
_, session_id, choice = parts
|
|
2747
|
+
if choice not in {"auto", "on", "off", "reset"}:
|
|
2748
|
+
await self._answer_action(interaction_id, "Thread controls expired")
|
|
2749
|
+
return True
|
|
2750
|
+
state = self._thread_action_state(session_id, chat_id)
|
|
2751
|
+
if not state:
|
|
2752
|
+
await self._answer_action(interaction_id, "Thread controls expired")
|
|
2753
|
+
return True
|
|
2754
|
+
target_chat_id = self._chat_key(state.get("target_chat_id"))
|
|
2755
|
+
if not target_chat_id:
|
|
2756
|
+
await self._answer_action(interaction_id, "Thread controls expired")
|
|
2757
|
+
return True
|
|
2758
|
+
if not await self._thread_action_allowed(event, state):
|
|
2759
|
+
return True
|
|
2760
|
+
try:
|
|
2761
|
+
if choice == "reset":
|
|
2762
|
+
self._set_reply_threads_for_chat(target_chat_id, None)
|
|
2763
|
+
else:
|
|
2764
|
+
self._set_reply_threads_for_chat(target_chat_id, choice)
|
|
2765
|
+
mode = self._reply_thread_mode_for_chat(target_chat_id)
|
|
2766
|
+
await self._edit_action_message(
|
|
2767
|
+
event,
|
|
2768
|
+
self._reply_thread_status_body(target_chat_id, controls=False, updated=True),
|
|
2769
|
+
{"rows": []},
|
|
2770
|
+
)
|
|
2771
|
+
self._thread_action_sessions.pop(session_id, None)
|
|
2772
|
+
await self._answer_action(interaction_id, f"Reply threads: {mode}")
|
|
2773
|
+
return True
|
|
2774
|
+
except Exception:
|
|
2775
|
+
logger.exception("[inline] thread action failed")
|
|
2776
|
+
await self._answer_action(interaction_id, "Thread setting failed")
|
|
2777
|
+
return True
|
|
2778
|
+
|
|
2779
|
+
async def _thread_action_allowed(self, event: Dict[str, Any], state: Dict[str, Any]) -> bool:
|
|
2780
|
+
actor_id = str(event.get("actorUserId") or "").strip()
|
|
2781
|
+
interaction_id = str(event.get("interactionId") or "")
|
|
2782
|
+
chat_type = await self._action_chat_type(event)
|
|
2783
|
+
if self._actor_authorized(chat_type, actor_id):
|
|
2784
|
+
return True
|
|
2785
|
+
display_chat_id = self._chat_key(state.get("display_chat_id"))
|
|
2786
|
+
target_chat_id = self._chat_key(state.get("target_chat_id"))
|
|
2787
|
+
if chat_type and actor_id and self._allowed(chat_type, actor_id):
|
|
2788
|
+
if chat_type == "dm":
|
|
2789
|
+
return True
|
|
2790
|
+
thread_id = display_chat_id if target_chat_id and display_chat_id != target_chat_id else None
|
|
2791
|
+
if self._chat_allowed(display_chat_id or str(event.get("chatId") or ""), thread_id, target_chat_id):
|
|
2792
|
+
return True
|
|
2793
|
+
await self._answer_action(interaction_id, "Not authorized")
|
|
2794
|
+
logger.info("[inline] blocked thread action actor=%s chat_type=%s action=%s", actor_id or "unknown", chat_type or "unknown", event.get("actionId") or "")
|
|
2795
|
+
return False
|
|
2796
|
+
|
|
2501
2797
|
@staticmethod
|
|
2502
2798
|
def _is_model_picker_action(action_id: str) -> bool:
|
|
2503
2799
|
return action_id.startswith(("mp:", "mpg:", "mm:", "mc:", "mg:", "mb:", "mx:"))
|
|
@@ -2699,7 +2995,14 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
2699
2995
|
except Exception:
|
|
2700
2996
|
logger.debug("[inline] answer action failed", exc_info=True)
|
|
2701
2997
|
|
|
2702
|
-
async def send(
|
|
2998
|
+
async def send(
|
|
2999
|
+
self,
|
|
3000
|
+
chat_id: str,
|
|
3001
|
+
content: str,
|
|
3002
|
+
reply_to: Optional[str] = None,
|
|
3003
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
3004
|
+
actions: Optional[Dict[str, Any]] = None,
|
|
3005
|
+
) -> SendResult:
|
|
2703
3006
|
target = self._target_for(chat_id, metadata)
|
|
2704
3007
|
reply_to = self._reply_to_for_target(reply_to, target)
|
|
2705
3008
|
parse_markdown = self._parse_markdown and not self._expects_edits(metadata)
|
|
@@ -2707,6 +3010,7 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
2707
3010
|
message_ids: List[str] = []
|
|
2708
3011
|
raw_responses: List[Any] = []
|
|
2709
3012
|
last_result: Optional[SendResult] = None
|
|
3013
|
+
marked_visible = False
|
|
2710
3014
|
for index, chunk in enumerate(chunks):
|
|
2711
3015
|
body: Dict[str, Any] = {
|
|
2712
3016
|
"target": target,
|
|
@@ -2715,9 +3019,14 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
2715
3019
|
}
|
|
2716
3020
|
if reply_to and index == 0:
|
|
2717
3021
|
body["replyToMsgId"] = str(reply_to)
|
|
3022
|
+
if actions is not None and index == 0:
|
|
3023
|
+
body["actions"] = actions
|
|
2718
3024
|
last_result = await self._send_sidecar("/send", body)
|
|
2719
3025
|
if not last_result.success:
|
|
2720
3026
|
return last_result
|
|
3027
|
+
if not marked_visible:
|
|
3028
|
+
self._mark_reply_thread_visible(target)
|
|
3029
|
+
marked_visible = True
|
|
2721
3030
|
if last_result.message_id:
|
|
2722
3031
|
message_ids.append(str(last_result.message_id))
|
|
2723
3032
|
raw_responses.append(last_result.raw_response)
|
|
@@ -2752,13 +3061,17 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
2752
3061
|
if finalize:
|
|
2753
3062
|
return await self._edit_overflow_split(chat_id, message_id, content, metadata=metadata)
|
|
2754
3063
|
text = self.truncate_message(text, self.MAX_MESSAGE_LENGTH)[0]
|
|
3064
|
+
target = self._target_for(chat_id, metadata)
|
|
2755
3065
|
body = {
|
|
2756
|
-
"target":
|
|
3066
|
+
"target": target,
|
|
2757
3067
|
"messageId": str(message_id),
|
|
2758
3068
|
"text": text,
|
|
2759
3069
|
"parseMarkdown": parse_markdown,
|
|
2760
3070
|
}
|
|
2761
|
-
|
|
3071
|
+
result = await self._send_sidecar("/edit", body)
|
|
3072
|
+
if result.success:
|
|
3073
|
+
self._mark_reply_thread_visible(target)
|
|
3074
|
+
return result
|
|
2762
3075
|
|
|
2763
3076
|
async def _edit_overflow_split(
|
|
2764
3077
|
self,
|
|
@@ -2781,6 +3094,7 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
2781
3094
|
})
|
|
2782
3095
|
if not first_result.success:
|
|
2783
3096
|
return first_result
|
|
3097
|
+
self._mark_reply_thread_visible(target)
|
|
2784
3098
|
|
|
2785
3099
|
continuation_ids: List[str] = []
|
|
2786
3100
|
raw_responses: List[Any] = [first_result.raw_response]
|
|
@@ -2834,13 +3148,17 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
2834
3148
|
|
|
2835
3149
|
async def send_typing(self, chat_id: str, metadata=None) -> None:
|
|
2836
3150
|
try:
|
|
2837
|
-
|
|
3151
|
+
target = self._typing_target_for(chat_id, metadata)
|
|
3152
|
+
await self._sidecar_call("/typing", {"target": target, "state": "start"})
|
|
3153
|
+
self._active_typing_targets[self._typing_target_key(chat_id, metadata)] = target
|
|
2838
3154
|
except Exception as exc:
|
|
2839
3155
|
logger.debug("[inline] typing failed: %s", exc)
|
|
2840
3156
|
|
|
2841
3157
|
async def stop_typing(self, chat_id: str, metadata: Optional[Dict[str, Any]] = None) -> None:
|
|
2842
3158
|
try:
|
|
2843
|
-
|
|
3159
|
+
key = self._typing_target_key(chat_id, metadata)
|
|
3160
|
+
target = self._active_typing_targets.pop(key, None) or self._typing_target_for(chat_id, metadata)
|
|
3161
|
+
await self._sidecar_call("/typing", {"target": target, "state": "stop"})
|
|
2844
3162
|
except Exception:
|
|
2845
3163
|
pass
|
|
2846
3164
|
|
|
@@ -2902,7 +3220,10 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
2902
3220
|
}
|
|
2903
3221
|
if reply_to:
|
|
2904
3222
|
body["replyToMsgId"] = reply_to
|
|
2905
|
-
|
|
3223
|
+
result = await self._send_sidecar("/send-attachment", body)
|
|
3224
|
+
if result.success:
|
|
3225
|
+
self._mark_reply_thread_visible(target)
|
|
3226
|
+
return result
|
|
2906
3227
|
|
|
2907
3228
|
async def create_handoff_thread(self, parent_chat_id: str, name: str) -> Optional[str]:
|
|
2908
3229
|
try:
|
|
@@ -3173,6 +3494,21 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
3173
3494
|
return _target_from_chat_id(str(thread_id))
|
|
3174
3495
|
return _target_from_chat_id(chat_id)
|
|
3175
3496
|
|
|
3497
|
+
def _typing_target_key(self, chat_id: str, metadata: Optional[Dict[str, Any]] = None) -> str:
|
|
3498
|
+
return f"{self._chat_key(chat_id)}:{self._chat_key((metadata or {}).get('thread_id'))}"
|
|
3499
|
+
|
|
3500
|
+
# First auto-created reply-thread turns need parent-chat typing. When Hermes
|
|
3501
|
+
# is replying as the first message in a brand-new Inline reply thread, there
|
|
3502
|
+
# is not yet a thread view for users to watch, so typing should remain on
|
|
3503
|
+
# the parent message's chat until the assistant's first child-thread reply lands.
|
|
3504
|
+
def _typing_target_for(self, chat_id: str, metadata: Optional[Dict[str, Any]] = None) -> Dict[str, str]:
|
|
3505
|
+
thread_id = self._chat_key((metadata or {}).get("thread_id"))
|
|
3506
|
+
chat_key = self._chat_key(chat_id)
|
|
3507
|
+
parent_chat_id = self._reply_thread_parent_typing_targets.get(thread_id) if thread_id else None
|
|
3508
|
+
if thread_id and parent_chat_id and chat_key == self._chat_key(parent_chat_id):
|
|
3509
|
+
return _target_from_chat_id(parent_chat_id)
|
|
3510
|
+
return self._target_for(chat_id, metadata)
|
|
3511
|
+
|
|
3176
3512
|
@staticmethod
|
|
3177
3513
|
def _remember(mapping: OrderedDict, key: str, value: Any, limit: int = 512) -> None:
|
|
3178
3514
|
if key in mapping:
|
|
@@ -3312,7 +3648,7 @@ def _standalone_attachment_kind(path: str, is_voice: bool, force_document: bool)
|
|
|
3312
3648
|
def _inline_threads_command_handler(raw_args: str) -> str:
|
|
3313
3649
|
text = f"/threads {str(raw_args or '').strip()}".strip()
|
|
3314
3650
|
action = InlineAdapter._thread_command_action(text) or "help"
|
|
3315
|
-
usage = "Usage: /threads status, /threads on, /threads off, or /threads
|
|
3651
|
+
usage = "Usage: /threads status, /threads auto, /threads on, /threads off, or /threads reset."
|
|
3316
3652
|
if action == "help":
|
|
3317
3653
|
return usage
|
|
3318
3654
|
return (
|
|
@@ -3347,7 +3683,7 @@ def register(ctx) -> None:
|
|
|
3347
3683
|
allowed_users_env="INLINE_ALLOWED_USERS",
|
|
3348
3684
|
allow_all_env="INLINE_ALLOW_ALL_USERS",
|
|
3349
3685
|
max_message_length=_MAX_MESSAGE_LENGTH,
|
|
3350
|
-
emoji="
|
|
3686
|
+
emoji="💬",
|
|
3351
3687
|
pii_safe=False,
|
|
3352
3688
|
allow_update_command=True,
|
|
3353
3689
|
platform_hint=(
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
name: inline-platform
|
|
2
2
|
label: Inline
|
|
3
3
|
kind: platform
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.3
|
|
5
5
|
description: >
|
|
6
6
|
Inline platform adapter for Hermes Agent. The adapter runs as a native
|
|
7
7
|
Hermes Python platform plugin and supervises a local Node sidecar that uses
|
|
@@ -63,11 +63,11 @@ optional_env:
|
|
|
63
63
|
prompt: "Allowed group sender ids"
|
|
64
64
|
password: false
|
|
65
65
|
- name: INLINE_REQUIRE_MENTION
|
|
66
|
-
description: "Require a mention/wake word in group chats (default true)"
|
|
66
|
+
description: "Require a mention/wake word in group chats. Replies to the bot and followed eligible reply/fresh threads can continue without one unless strict mention is enabled (default true)."
|
|
67
67
|
prompt: "Require group mention? (true/false)"
|
|
68
68
|
password: false
|
|
69
69
|
- name: INLINE_STRICT_MENTION
|
|
70
|
-
description: "Require an explicit mention/wake word on every group-chat turn, including replies to the bot (true/false, default false)"
|
|
70
|
+
description: "Require an explicit mention/wake word on every group-chat turn, including replies to the bot and followed threads (true/false, default false)"
|
|
71
71
|
prompt: "Require every group turn to mention Hermes? (true/false)"
|
|
72
72
|
password: false
|
|
73
73
|
- name: INLINE_ALLOWED_CHATS
|
|
@@ -79,8 +79,8 @@ optional_env:
|
|
|
79
79
|
prompt: "Free-response Inline chat ids"
|
|
80
80
|
password: false
|
|
81
81
|
- name: INLINE_REPLY_THREADS
|
|
82
|
-
description: "
|
|
83
|
-
prompt: "Reply
|
|
82
|
+
description: "Reply-thread mode: auto, on, or off. Auto creates child reply threads only for large parent chats or explicit thread requests (default auto)."
|
|
83
|
+
prompt: "Reply-thread mode (auto/on/off)"
|
|
84
84
|
password: false
|
|
85
85
|
- name: INLINE_CONTEXT_BACKFILL
|
|
86
86
|
description: "Automatic context mode: selective, off, or always (default selective)"
|
|
@@ -22160,6 +22160,7 @@ class WebSocketTransport {
|
|
|
22160
22160
|
lastReconnectDelayMs = null;
|
|
22161
22161
|
lastReconnectCause = null;
|
|
22162
22162
|
reconnectCount = 0;
|
|
22163
|
+
reconnectScheduling = false;
|
|
22163
22164
|
constructor(options) {
|
|
22164
22165
|
this.url = options.url;
|
|
22165
22166
|
this.log = options.logger ?? {};
|
|
@@ -22187,26 +22188,33 @@ class WebSocketTransport {
|
|
|
22187
22188
|
}
|
|
22188
22189
|
async reconnect(options) {
|
|
22189
22190
|
const skipDelay = options?.skipDelay ?? false;
|
|
22190
|
-
if (this.state === "connecting" && this.reconnectionTimer) {
|
|
22191
|
+
if (this.state === "connecting" && (this.reconnectionTimer || this.reconnectScheduling)) {
|
|
22191
22192
|
return;
|
|
22192
22193
|
}
|
|
22193
|
-
|
|
22194
|
-
|
|
22195
|
-
|
|
22196
|
-
|
|
22197
|
-
const delayMs = Math.round(delaySeconds * 1000);
|
|
22198
|
-
const cause = options?.cause?.trim() || "unspecified";
|
|
22199
|
-
this.lastReconnectScheduledAt = Date.now();
|
|
22200
|
-
this.lastReconnectDelayMs = delayMs;
|
|
22201
|
-
this.lastReconnectCause = cause;
|
|
22202
|
-
this.reconnectCount += 1;
|
|
22203
|
-
this.log.warn?.(`WebSocket reconnect scheduled (attempt=${this.connectionAttemptNo}, delayMs=${delayMs}, cause=${cause})`);
|
|
22204
|
-
this.reconnectionTimer = setTimeout(() => {
|
|
22205
|
-
this.reconnectionTimer = null;
|
|
22206
|
-
if (this.state === "idle" || this.state === "connected")
|
|
22194
|
+
this.reconnectScheduling = true;
|
|
22195
|
+
try {
|
|
22196
|
+
await this.setConnecting();
|
|
22197
|
+
if (this.state === "idle" || this.reconnectionTimer)
|
|
22207
22198
|
return;
|
|
22208
|
-
this.
|
|
22209
|
-
|
|
22199
|
+
this.cleanUpPreviousConnection();
|
|
22200
|
+
this.connectionAttemptNo = this.connectionAttemptNo + 1 >>> 0;
|
|
22201
|
+
const delaySeconds = this.getReconnectionDelaySeconds(this.connectionAttemptNo);
|
|
22202
|
+
const delayMs = Math.round(delaySeconds * 1000);
|
|
22203
|
+
const cause = options?.cause?.trim() || "unspecified";
|
|
22204
|
+
this.lastReconnectScheduledAt = Date.now();
|
|
22205
|
+
this.lastReconnectDelayMs = delayMs;
|
|
22206
|
+
this.lastReconnectCause = cause;
|
|
22207
|
+
this.reconnectCount += 1;
|
|
22208
|
+
this.log.warn?.(`WebSocket reconnect scheduled (attempt=${this.connectionAttemptNo}, delayMs=${delayMs}, cause=${cause})`);
|
|
22209
|
+
this.reconnectionTimer = setTimeout(() => {
|
|
22210
|
+
this.reconnectionTimer = null;
|
|
22211
|
+
if (this.state === "idle" || this.state === "connected")
|
|
22212
|
+
return;
|
|
22213
|
+
this.openConnection();
|
|
22214
|
+
}, skipDelay ? 0 : delaySeconds * 1000);
|
|
22215
|
+
} finally {
|
|
22216
|
+
this.reconnectScheduling = false;
|
|
22217
|
+
}
|
|
22210
22218
|
}
|
|
22211
22219
|
cleanUpPreviousConnection() {
|
|
22212
22220
|
if (this.reconnectionTimer) {
|
|
@@ -22676,6 +22684,7 @@ class InlineSdkClient {
|
|
|
22676
22684
|
const chat = result.getChat.chat;
|
|
22677
22685
|
if (!chat)
|
|
22678
22686
|
throw new Error("getChat: missing chat");
|
|
22687
|
+
const dialogFollowMode = result.getChat.dialog?.followMode;
|
|
22679
22688
|
return {
|
|
22680
22689
|
chatId: chat.id,
|
|
22681
22690
|
title: chat.title,
|
|
@@ -22683,6 +22692,8 @@ class InlineSdkClient {
|
|
|
22683
22692
|
...chat.spaceId != null ? { spaceId: chat.spaceId } : {},
|
|
22684
22693
|
...chat.parentChatId != null ? { parentChatId: chat.parentChatId } : {},
|
|
22685
22694
|
...chat.parentMessageId != null ? { parentMessageId: chat.parentMessageId } : {},
|
|
22695
|
+
...chat.lastMsgId != null ? { lastMsgId: chat.lastMsgId } : {},
|
|
22696
|
+
...dialogFollowMode != null ? { dialogFollowMode } : {},
|
|
22686
22697
|
...chat.isPublic != null ? { isPublic: chat.isPublic } : {},
|
|
22687
22698
|
...chat.untitled != null ? { untitled: chat.untitled } : {},
|
|
22688
22699
|
...chat.number != null ? { number: chat.number } : {}
|
|
@@ -23799,6 +23810,36 @@ var resolveUploadFileUrl = (baseUrl) => {
|
|
|
23799
23810
|
return url;
|
|
23800
23811
|
};
|
|
23801
23812
|
var hasMethodMapping = (method) => Object.prototype.hasOwnProperty.call(rpcInputKindByMethod, method) && Object.prototype.hasOwnProperty.call(rpcResultKindByMethod, method);
|
|
23813
|
+
// ../sdk/dist/sdk/thread-mention-gating.js
|
|
23814
|
+
var INLINE_FOLLOW_MODE_MENTION_FRESH_LAST_MESSAGE_ID_LIMIT = 50;
|
|
23815
|
+
function isInlineReplyThreadForMentionGate(chat) {
|
|
23816
|
+
return parsePositiveInteger(chat.parentMessageId) != null;
|
|
23817
|
+
}
|
|
23818
|
+
function isInlineFreshThreadForMentionGate(lastMsgId, limit = INLINE_FOLLOW_MODE_MENTION_FRESH_LAST_MESSAGE_ID_LIMIT) {
|
|
23819
|
+
const normalized = parsePositiveInteger(lastMsgId);
|
|
23820
|
+
return normalized != null && normalized < BigInt(limit);
|
|
23821
|
+
}
|
|
23822
|
+
function isInlineFollowModeMentionGateEligible(chat) {
|
|
23823
|
+
if (isInlineReplyThreadForMentionGate(chat)) {
|
|
23824
|
+
return true;
|
|
23825
|
+
}
|
|
23826
|
+
return isInlineFreshThreadForMentionGate(chat.lastMsgId);
|
|
23827
|
+
}
|
|
23828
|
+
function parsePositiveInteger(value) {
|
|
23829
|
+
if (value == null)
|
|
23830
|
+
return null;
|
|
23831
|
+
if (typeof value === "bigint")
|
|
23832
|
+
return value > 0n ? value : null;
|
|
23833
|
+
if (typeof value === "number") {
|
|
23834
|
+
if (!Number.isSafeInteger(value) || value <= 0)
|
|
23835
|
+
return null;
|
|
23836
|
+
return BigInt(value);
|
|
23837
|
+
}
|
|
23838
|
+
const text = value.trim();
|
|
23839
|
+
if (!/^[1-9]\d*$/.test(text))
|
|
23840
|
+
return null;
|
|
23841
|
+
return BigInt(text);
|
|
23842
|
+
}
|
|
23802
23843
|
// ../sdk/dist/state/json-file-state-store.js
|
|
23803
23844
|
import { readFile, rename, writeFile } from "node:fs/promises";
|
|
23804
23845
|
|
|
@@ -24458,6 +24499,7 @@ async function endpointChat(res, body) {
|
|
|
24458
24499
|
}
|
|
24459
24500
|
const snapshot = await getRawChatSnapshot(target.chatId);
|
|
24460
24501
|
const chat = snapshot.chat;
|
|
24502
|
+
const followModeMentionEligible = isInlineFollowModeMentionGateEligible(chat);
|
|
24461
24503
|
writeJson(res, 200, {
|
|
24462
24504
|
ok: true,
|
|
24463
24505
|
result: {
|
|
@@ -24475,6 +24517,9 @@ async function endpointChat(res, body) {
|
|
|
24475
24517
|
...chat.createdBy != null ? { createdBy: chat.createdBy.toString() } : {},
|
|
24476
24518
|
...chat.untitled != null ? { untitled: chat.untitled } : {},
|
|
24477
24519
|
...chat.number != null ? { number: chat.number } : {},
|
|
24520
|
+
...snapshot.dialog != null ? { dialog: safeJson(snapshot.dialog) } : {},
|
|
24521
|
+
...snapshot.dialogFollowMode != null ? { dialogFollowMode: String(snapshot.dialogFollowMode) } : {},
|
|
24522
|
+
followModeMentionEligible,
|
|
24478
24523
|
pinnedMessageIds: safeJson(snapshot.pinnedMessageIds),
|
|
24479
24524
|
...snapshot.anchorMessage != null ? { anchorMessage: safeJson(snapshot.anchorMessage) } : {},
|
|
24480
24525
|
chat: safeJson(chat)
|
|
@@ -24636,8 +24681,12 @@ async function getRawChatSnapshot(chatId) {
|
|
|
24636
24681
|
const chat = typed.getChat?.chat;
|
|
24637
24682
|
if (!chat)
|
|
24638
24683
|
throw new SidecarError("chat not found", "not_found");
|
|
24684
|
+
const dialog = typed.getChat?.dialog;
|
|
24685
|
+
const dialogFollowMode = dialog?.followMode ?? dialog?.follow_mode ?? undefined;
|
|
24639
24686
|
return {
|
|
24640
24687
|
chat,
|
|
24688
|
+
...dialog != null ? { dialog } : {},
|
|
24689
|
+
...dialogFollowMode != null ? { dialogFollowMode } : {},
|
|
24641
24690
|
pinnedMessageIds: typed.getChat?.pinnedMessageIds ?? [],
|
|
24642
24691
|
...typed.getChat?.anchorMessage != null ? { anchorMessage: typed.getChat.anchorMessage } : {}
|
|
24643
24692
|
};
|
|
@@ -24767,6 +24816,8 @@ class MockInlineClient {
|
|
|
24767
24816
|
title: "Mock reply thread 456",
|
|
24768
24817
|
parentChatId: 123n,
|
|
24769
24818
|
parentMessageId: 9001n,
|
|
24819
|
+
lastMsgId: 9002n,
|
|
24820
|
+
dialogFollowMode: 1,
|
|
24770
24821
|
untitled: true
|
|
24771
24822
|
};
|
|
24772
24823
|
}
|
|
@@ -24834,6 +24885,7 @@ class MockInlineClient {
|
|
|
24834
24885
|
title: "Mock reply thread 456",
|
|
24835
24886
|
parentChatId: 123n,
|
|
24836
24887
|
parentMessageId: 9001n,
|
|
24888
|
+
lastMsgId: 9002n,
|
|
24837
24889
|
untitled: true
|
|
24838
24890
|
} : {
|
|
24839
24891
|
id: chatId,
|
|
@@ -24842,6 +24894,7 @@ class MockInlineClient {
|
|
|
24842
24894
|
return {
|
|
24843
24895
|
getChat: {
|
|
24844
24896
|
chat,
|
|
24897
|
+
dialog: { chatId, followMode: chatId === 456n ? 1 : 0 },
|
|
24845
24898
|
pinnedMessageIds: [8801n],
|
|
24846
24899
|
anchorMessage: {
|
|
24847
24900
|
id: 8801n,
|
package/plugin/inline/tools.py
CHANGED
|
@@ -53,7 +53,6 @@ _ACTION_MANIFEST = [
|
|
|
53
53
|
("get_messages", "(chat_id?, message_ids)", "Fetch specific Inline messages by ID."),
|
|
54
54
|
("get_history", "(chat_id?, limit?, anchor_id?)", "Fetch recent or anchored Inline history."),
|
|
55
55
|
("search_messages", "(chat_id?|user_id?, query, limit?, offset_id?)", "Search Inline message text in a chat, thread, or DM."),
|
|
56
|
-
("send_message", "(chat_id?|user_id?, text, reply_to_msg_id?)", "Send a Markdown message."),
|
|
57
56
|
("edit_message", "(chat_id?|user_id?, message_id, text)", "Edit a bot-owned message."),
|
|
58
57
|
("delete_message", "(chat_id?|user_id?, message_id)", "Delete a bot-owned message."),
|
|
59
58
|
("add_reaction", "(chat_id?|user_id?, message_id?, emoji)", "Add a reaction to an Inline message."),
|
|
@@ -63,8 +62,7 @@ _ACTION_MANIFEST = [
|
|
|
63
62
|
("unpin_message", "(chat_id?|user_id?, message_id?)", "Unpin an Inline message for the conversation."),
|
|
64
63
|
("list_pins", "(chat_id?)", "List pinned Inline message IDs for a chat or reply thread."),
|
|
65
64
|
("create_thread", "(parent_chat_id?, parent_message_id?, title?)", "Create an Inline reply thread."),
|
|
66
|
-
("
|
|
67
|
-
("set_presence", "(chat_id?|user_id?, kind, comment?)", "Set the bot presence state."),
|
|
65
|
+
("set_presence", "(chat_id?|user_id?, kind, comment?)", "Set the bot avatar presence/status message."),
|
|
68
66
|
]
|
|
69
67
|
_ACTIONS = [name for name, _, _ in _ACTION_MANIFEST]
|
|
70
68
|
_PRESENCE_KINDS = ["idle", "happy", "waving", "jumping", "failed", "waiting", "running", "review"]
|
|
@@ -95,10 +93,11 @@ def tool_context_prompt(
|
|
|
95
93
|
if not check_inline_tool_requirements():
|
|
96
94
|
return None
|
|
97
95
|
lines = [
|
|
98
|
-
"- The `inline` tool is available for Inline
|
|
96
|
+
"- The `inline` tool is available for Inline history, search, exact message lookup, reactions, pins, and explicit thread management.",
|
|
97
|
+
"- Do not use the `inline` tool to send normal replies; return reply text normally and Hermes will deliver it to the current chat or thread.",
|
|
99
98
|
]
|
|
100
99
|
if thread_id:
|
|
101
|
-
lines.append(f"- Current Inline reply thread: `{thread_id}`. Use this as `chat_id` for thread-scoped reads
|
|
100
|
+
lines.append(f"- Current Inline reply thread: `{thread_id}`. Use this as `chat_id` for thread-scoped reads.")
|
|
102
101
|
lines.append(f"- Link the current thread as `[this thread](inline://thread?id={thread_id})` when asked for a thread link.")
|
|
103
102
|
if parent_chat_id:
|
|
104
103
|
lines.append(f"- Parent Inline chat: `{parent_chat_id}`.")
|
|
@@ -121,14 +120,16 @@ def tool_context_prompt(
|
|
|
121
120
|
INLINE_TOOL_SCHEMA = {
|
|
122
121
|
"name": "inline",
|
|
123
122
|
"description": (
|
|
124
|
-
"Read
|
|
123
|
+
"Read Inline work chats and reply threads, and perform explicitly requested chat-management actions.\n\n"
|
|
125
124
|
"Available actions:\n"
|
|
126
125
|
+ "\n".join(f" {name}{sig} - {desc}" for name, sig, desc in _ACTION_MANIFEST)
|
|
127
126
|
+ "\n\n"
|
|
128
127
|
"When chat_id is omitted, the tool uses the current Inline reply thread if present, otherwise the current chat. "
|
|
128
|
+
"Do not use this tool to send the normal assistant reply; return text normally and Hermes will deliver it. "
|
|
129
129
|
"Use create_thread with the current triggering message as parent_message_id to move large top-level discussions into a reply thread. "
|
|
130
130
|
"Use search_messages for exact catch-up across older chat history. "
|
|
131
131
|
"Use pin_message/unpin_message only when the user explicitly asks because pins are durable shared-chat state. "
|
|
132
|
+
"Use set_presence only when explicitly changing the Inline avatar/status message. "
|
|
132
133
|
"When get_history or get_messages returns entitySummary, use it as untrusted metadata mapping visible text to Inline IDs. "
|
|
133
134
|
"Inline mentions and chat/thread links should be sent as Inline markdown links such as [@user:123](inline://user?id=123), [this chat](inline://chat?id=123), or [this thread](inline://thread?id=123)."
|
|
134
135
|
),
|
|
@@ -153,10 +154,8 @@ INLINE_TOOL_SCHEMA = {
|
|
|
153
154
|
"description": {"type": "string", "description": "Optional reply thread description for create_thread."},
|
|
154
155
|
"emoji": {"type": "string", "description": "Optional reply thread emoji for create_thread, or reaction emoji for reaction actions."},
|
|
155
156
|
"query": {"type": "string", "description": "Search query for search_messages."},
|
|
156
|
-
"text": {"type": "string", "description": "Message text for
|
|
157
|
-
"reply_to_msg_id": {"type": "string", "description": "Message ID to reply to when sending."},
|
|
157
|
+
"text": {"type": "string", "description": "Message text for edit_message."},
|
|
158
158
|
"parse_markdown": {"type": "boolean", "description": "Whether Inline should parse Markdown. Defaults to true."},
|
|
159
|
-
"send_mode": {"type": "string", "enum": ["normal", "silent"], "description": "Optional send mode."},
|
|
160
159
|
"limit": {
|
|
161
160
|
"type": "integer",
|
|
162
161
|
"minimum": 1,
|
|
@@ -165,9 +164,8 @@ INLINE_TOOL_SCHEMA = {
|
|
|
165
164
|
},
|
|
166
165
|
"anchor_id": {"type": "string", "description": "Anchor message ID for get_history pagination."},
|
|
167
166
|
"offset_id": {"type": "string", "description": "Offset message ID for search_messages pagination."},
|
|
168
|
-
"
|
|
169
|
-
"
|
|
170
|
-
"comment": {"type": "string", "description": "Optional bot presence comment."},
|
|
167
|
+
"kind": {"type": "string", "enum": _PRESENCE_KINDS, "description": "Bot avatar presence/status kind."},
|
|
168
|
+
"comment": {"type": "string", "description": "Optional bot avatar presence/status message."},
|
|
171
169
|
},
|
|
172
170
|
"required": ["action"],
|
|
173
171
|
},
|
|
@@ -181,8 +179,8 @@ def register(ctx: Any) -> None:
|
|
|
181
179
|
schema=INLINE_TOOL_SCHEMA,
|
|
182
180
|
handler=_handle_inline_tool,
|
|
183
181
|
check_fn=check_inline_tool_requirements,
|
|
184
|
-
description="Read
|
|
185
|
-
emoji="
|
|
182
|
+
description="Read Inline chats, messages, reply threads, and explicit chat-management state.",
|
|
183
|
+
emoji="💬",
|
|
186
184
|
)
|
|
187
185
|
|
|
188
186
|
|
|
@@ -234,20 +232,6 @@ def _request_for_action(action: str, args: Dict[str, Any]) -> tuple[str, Dict[st
|
|
|
234
232
|
body["offsetId"] = offset_id
|
|
235
233
|
return "/search", body
|
|
236
234
|
|
|
237
|
-
if action == "send_message":
|
|
238
|
-
text = _required_str(args, "text", max_chars=_MAX_TEXT_CHARS)
|
|
239
|
-
body = {
|
|
240
|
-
"target": _target(args),
|
|
241
|
-
"text": text,
|
|
242
|
-
"parseMarkdown": _bool(args.get("parse_markdown"), True),
|
|
243
|
-
}
|
|
244
|
-
reply_to = _inline_id(args.get("reply_to_msg_id"))
|
|
245
|
-
if reply_to:
|
|
246
|
-
body["replyToMsgId"] = reply_to
|
|
247
|
-
if _str(args.get("send_mode")) == "silent":
|
|
248
|
-
body["sendMode"] = "silent"
|
|
249
|
-
return "/send", body
|
|
250
|
-
|
|
251
235
|
if action == "edit_message":
|
|
252
236
|
return "/edit", {
|
|
253
237
|
"target": _target(args),
|
|
@@ -304,12 +288,6 @@ def _request_for_action(action: str, args: Dict[str, Any]) -> tuple[str, Dict[st
|
|
|
304
288
|
body[key] = value
|
|
305
289
|
return "/create-subthread", body
|
|
306
290
|
|
|
307
|
-
if action == "set_typing":
|
|
308
|
-
return "/typing", {
|
|
309
|
-
"target": _target(args),
|
|
310
|
-
"state": _str(args.get("state")) or "start",
|
|
311
|
-
}
|
|
312
|
-
|
|
313
291
|
if action == "set_presence":
|
|
314
292
|
kind = _str(args.get("kind"))
|
|
315
293
|
if kind not in _PRESENCE_KINDS:
|