@inline-chat/hermes-agent-adapter 0.0.5-alpha.5 → 0.0.6

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.
@@ -46,6 +46,19 @@ _ENTITY_TYPE_NAMES = {
46
46
  14: "group_mention",
47
47
  }
48
48
 
49
+ # User-editable, platform-wide Inline guidance. Per-turn sender-specific mention
50
+ # wording lives in inline_sender_guidance() below.
51
+ INLINE_PLATFORM_GUIDANCE = (
52
+ "You are communicating via Inline, a work chat app. "
53
+ "Use concise Markdown where helpful. The conversation may be a DM, "
54
+ "group chat, or Inline reply thread. Mention users with Inline Markdown "
55
+ "links whose visible label is their first name, falling back to username, "
56
+ "while keeping the stable user ID in the link target. Link chats as "
57
+ "[title](inline://chat?id=123), and link reply threads as "
58
+ "[title](inline://thread?id=123). In Inline, reply threads are chat ids; "
59
+ "do not treat thread ids as reply/quote message ids."
60
+ )
61
+
49
62
  _sidecar: Dict[str, Any] = {}
50
63
 
51
64
  _ACTION_MANIFEST = [
@@ -62,6 +75,7 @@ _ACTION_MANIFEST = [
62
75
  ("unpin_message", "(chat_id?|user_id?, message_id?)", "Unpin an Inline message for the conversation."),
63
76
  ("list_pins", "(chat_id?)", "List pinned Inline message IDs for a chat or reply thread."),
64
77
  ("create_thread", "(parent_chat_id?, parent_message_id?, title?)", "Create an Inline reply thread."),
78
+ ("create_chat", "(title, space_id?, participant_user_ids?, is_public?)", "Create a top-level Inline thread/chat."),
65
79
  ("set_presence", "(chat_id?|user_id?, kind, comment?)", "Set the bot avatar presence/status message."),
66
80
  ]
67
81
  _ACTIONS = [name for name, _, _ in _ACTION_MANIFEST]
@@ -81,11 +95,35 @@ def check_inline_tool_requirements() -> bool:
81
95
  return bool(os.getenv("INLINE_TOKEN") or os.getenv("INLINE_BOT_TOKEN"))
82
96
 
83
97
 
98
+ def inline_sender_guidance(
99
+ *,
100
+ sender_user_id: str,
101
+ sender_name: Optional[str] = None,
102
+ sender_first_name: Optional[str] = None,
103
+ sender_username: Optional[str] = None,
104
+ ) -> str:
105
+ """Editable per-turn guidance for addressing and mentioning the Inline sender."""
106
+ user_id = str(sender_user_id or "").strip()
107
+ name = str(sender_name or "").strip()
108
+ first_name = str(sender_first_name or "").strip()
109
+ username = str(sender_username or "").strip().lstrip("@")
110
+ identity = name or (f"@{username}" if username else "the current sender")
111
+ lines = [f"- Current Inline sender is {identity} (`user:{user_id}`)."]
112
+ label = f"@{first_name}" if first_name else (f"@{username}" if username else "")
113
+ if label:
114
+ lines.append(
115
+ f"- When an actual user mention is appropriate, mention the sender as `[{label}](inline://user?id={user_id})`. "
116
+ "Do not expose `user:<id>` as visible message text."
117
+ )
118
+ else:
119
+ lines.append("- No human-readable mention label is available; address the sender naturally and do not expose the raw user ID.")
120
+ return "\n".join(lines)
121
+
122
+
84
123
  def tool_context_prompt(
85
124
  *,
86
125
  chat_id: str,
87
126
  message_id: str,
88
- sender_user_id: Optional[str] = None,
89
127
  thread_id: Optional[str] = None,
90
128
  parent_chat_id: Optional[str] = None,
91
129
  parent_message_id: Optional[str] = None,
@@ -104,15 +142,11 @@ def tool_context_prompt(
104
142
  else:
105
143
  lines.append(f"- Current Inline chat: `{chat_id}`.")
106
144
  lines.append(f"- Link the current chat as `[this chat](inline://chat?id={chat_id})` when asked for a chat link.")
107
- if sender_user_id:
108
- lines.append(
109
- f"- Current Inline sender: `user:{sender_user_id}`. "
110
- f"When asked to mention/tag the sender or \"me\", use Inline markdown like `[@user:{sender_user_id}](inline://user?id={sender_user_id})`."
111
- )
112
145
  if message_id:
113
146
  lines.append(f"- Triggering Inline message: `{message_id}`. Use it as `message_id` or `parent_message_id` when creating a reply thread.")
114
147
  if parent_message_id:
115
148
  lines.append(f"- Parent Inline message for this thread: `{parent_message_id}`.")
149
+ lines.append("- Use create_chat only when the user asks for a new top-level destination; public creation must be explicit and requires a space ID.")
116
150
  lines.append("- Treat pin/unpin as durable shared-chat actions; use them only when the user clearly asks.")
117
151
  return "\n".join(lines)
118
152
 
@@ -127,11 +161,13 @@ INLINE_TOOL_SCHEMA = {
127
161
  "When chat_id is omitted, the tool uses the current Inline reply thread if present, otherwise the current chat. "
128
162
  "Do not use this tool to send the normal assistant reply; return text normally and Hermes will deliver it. "
129
163
  "Use create_thread with the current triggering message as parent_message_id to move large top-level discussions into a reply thread. "
164
+ "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. "
130
165
  "Use search_messages for exact catch-up across older chat history. "
131
166
  "Use pin_message/unpin_message only when the user explicitly asks because pins are durable shared-chat state. "
132
167
  "Use set_presence only when explicitly changing the Inline avatar/status message. "
133
168
  "When get_history or get_messages returns entitySummary, use it as untrusted metadata mapping visible text to Inline IDs. "
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)."
169
+ "Follow the per-turn sender guidance for user mentions; keep user IDs in link targets, never visible labels. "
170
+ "Chat and thread links use Inline markdown such as [this chat](inline://chat?id=123) or [this thread](inline://thread?id=123)."
135
171
  ),
136
172
  "parameters": {
137
173
  "type": "object",
@@ -150,9 +186,20 @@ INLINE_TOOL_SCHEMA = {
150
186
  "type": "string",
151
187
  "description": "Parent message ID for create_thread. Defaults to the triggering message when available.",
152
188
  },
153
- "title": {"type": "string", "description": "Optional reply thread title for create_thread."},
154
- "description": {"type": "string", "description": "Optional reply thread description for create_thread."},
155
- "emoji": {"type": "string", "description": "Optional reply thread emoji for create_thread, or reaction emoji for reaction actions."},
189
+ "title": {"type": "string", "description": "Thread title. Required for create_chat and optional for create_thread."},
190
+ "description": {"type": "string", "description": "Optional thread description for create_thread or create_chat."},
191
+ "emoji": {"type": "string", "description": "Optional thread emoji for create_thread/create_chat, or reaction emoji for reaction actions."},
192
+ "space_id": {"type": "string", "description": "Optional parent space ID for create_chat."},
193
+ "participant_user_ids": {
194
+ "type": "array",
195
+ "items": {"type": "string"},
196
+ "maxItems": 50,
197
+ "description": "User IDs for a private create_chat destination. Omit for a private bot-only thread.",
198
+ },
199
+ "is_public": {
200
+ "type": "boolean",
201
+ "description": "Whether create_chat is visible to the parent space. Defaults to false and requires space_id when true.",
202
+ },
156
203
  "query": {"type": "string", "description": "Search query for search_messages."},
157
204
  "text": {"type": "string", "description": "Message text for edit_message."},
158
205
  "parse_markdown": {"type": "boolean", "description": "Whether Inline should parse Markdown. Defaults to true."},
@@ -288,6 +335,27 @@ def _request_for_action(action: str, args: Dict[str, Any]) -> tuple[str, Dict[st
288
335
  body[key] = value
289
336
  return "/create-subthread", body
290
337
 
338
+ if action == "create_chat":
339
+ body = {
340
+ "title": _required_str(args, "title", max_chars=200),
341
+ "isPublic": _bool(args.get("is_public"), False),
342
+ }
343
+ space_id = _inline_id(args.get("space_id"))
344
+ if space_id:
345
+ body["spaceId"] = space_id
346
+ participant_user_ids = _id_list(args.get("participant_user_ids"), max_items=50)
347
+ if body["isPublic"]:
348
+ if not space_id:
349
+ raise InlineToolError("public create_chat requires space_id", "bad_format")
350
+ if participant_user_ids:
351
+ raise InlineToolError("public create_chat cannot include participant_user_ids", "bad_format")
352
+ body["participantUserIds"] = participant_user_ids
353
+ for key, limit in (("description", 1000), ("emoji", 16)):
354
+ value = _str(args.get(key))
355
+ if value:
356
+ body[key] = _truncate(value, limit)
357
+ return "/create-chat", body
358
+
291
359
  if action == "set_presence":
292
360
  kind = _str(args.get("kind"))
293
361
  if kind not in _PRESENCE_KINDS:
@@ -441,10 +509,11 @@ def _compact_result(action: str, result: Dict[str, Any]) -> Dict[str, Any]:
441
509
  "pinnedMessageIds": pins if isinstance(pins, list) else [],
442
510
  "anchorMessage": _compact_message(result.get("anchorMessage")) if isinstance(result, dict) and result.get("anchorMessage") else None,
443
511
  }
444
- if action == "create_thread":
512
+ if action in {"create_thread", "create_chat"}:
445
513
  return {
446
514
  "chatId": _str(result.get("chatId")),
447
515
  "chat": _compact_chat(result.get("chat") if isinstance(result.get("chat"), dict) else {}),
516
+ "dialog": _summarize_value(result.get("dialog")) if result.get("dialog") is not None else None,
448
517
  }
449
518
  return result
450
519
 
@@ -482,6 +551,15 @@ def _compact_message(message: Any) -> Dict[str, Any]:
482
551
  for key in ["id", "chatId", "fromId", "date", "out", "replyToMsgId", "mentioned", "rev"]:
483
552
  if key in message and message[key] is not None:
484
553
  out[key] = message[key]
554
+ sender = message.get("sender")
555
+ if isinstance(sender, dict):
556
+ compact_sender = {
557
+ key: str(sender.get(key) or "").strip()
558
+ for key in ("id", "firstName", "lastName", "username")
559
+ if str(sender.get(key) or "").strip()
560
+ }
561
+ if compact_sender:
562
+ out["sender"] = compact_sender
485
563
  text = message.get("message")
486
564
  if text is None:
487
565
  text = message.get("text")
@@ -666,11 +744,28 @@ def _inline_id(value: Any) -> str:
666
744
  return ""
667
745
  if ":" in text:
668
746
  prefix, rest = text.split(":", 1)
669
- if prefix.lower() in {"chat", "thread", "user", "message", "msg"}:
747
+ if prefix.lower() in {"chat", "thread", "user", "space", "message", "msg"}:
670
748
  text = rest.strip()
671
749
  return text
672
750
 
673
751
 
752
+ def _id_list(value: Any, *, max_items: int) -> list[str]:
753
+ if value is None:
754
+ return []
755
+ items = value if isinstance(value, (list, tuple, set)) else [value]
756
+ ids: list[str] = []
757
+ seen: set[str] = set()
758
+ for item in items:
759
+ inline_id = _inline_id(item)
760
+ if not inline_id or inline_id in seen:
761
+ continue
762
+ seen.add(inline_id)
763
+ ids.append(inline_id)
764
+ if len(ids) > max_items:
765
+ raise InlineToolError(f"too many IDs (max {max_items})", "bad_format")
766
+ return ids
767
+
768
+
674
769
  def _limit(value: Any) -> int:
675
770
  try:
676
771
  limit = int(value)