@inline-chat/hermes-agent-adapter 0.0.8 → 0.0.10

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.
@@ -8,6 +8,8 @@ import urllib.error
8
8
  import urllib.request
9
9
  from typing import Any, Dict, Iterable, Optional
10
10
 
11
+ from .message_actions import build_inline_agent_action_id
12
+
11
13
  try:
12
14
  from tools.registry import tool_error, tool_result
13
15
  except Exception: # pragma: no cover - used by lightweight package tests
@@ -27,6 +29,11 @@ _MAX_MESSAGE_IDS = 100
27
29
  _MAX_INLINE_ID = 9_223_372_036_854_775_807
28
30
  _DEFAULT_HISTORY_LIMIT = 20
29
31
  _MAX_TEXT_CHARS = 4000
32
+ _MAX_ACTION_ROWS = 8
33
+ _MAX_ACTIONS_PER_ROW = 8
34
+ _MAX_ACTION_LABEL_CHARS = 64
35
+ _MAX_ACTION_CALLBACK_BYTES = 1024
36
+ _MAX_ACTION_COPY_TEXT_CHARS = 4096
30
37
  _MAX_QUERY_CHARS = 500
31
38
  _MAX_RESULT_TEXT_CHARS = 1600
32
39
  _MAX_MESSAGE_ENTITIES = 12
@@ -58,7 +65,11 @@ INLINE_PLATFORM_GUIDANCE = (
58
65
  "while keeping the stable user ID in the link target. Link chats as "
59
66
  "[title](inline://chat?id=123), and link reply threads as "
60
67
  "[title](inline://thread?id=123). In Inline, reply threads are chat ids; "
61
- "do not treat thread ids as reply/quote message ids. Use ordinary Markdown "
68
+ "do not treat thread ids as reply/quote message ids. Supported Markdown is "
69
+ "emphasis, inline or fenced code, links, headings, lists or checklists, "
70
+ "quotes, tables, separators, and HTTP(S) images. Do not rely on "
71
+ "strikethrough, footnotes, or arbitrary HTML; unsupported or ambiguous "
72
+ "incomplete syntax remains visible text. Use ordinary Markdown "
62
73
  "tables for tabular data, never fenced-code tables. For collapsible work, "
63
74
  "use <details open>, then <summary>Title</summary>, body Markdown, and "
64
75
  "</details>; put kind=\"progress\" on summary only while work is in progress. "
@@ -68,11 +79,12 @@ INLINE_PLATFORM_GUIDANCE = (
68
79
  _sidecar: Dict[str, Any] = {}
69
80
 
70
81
  _ACTION_MANIFEST = [
82
+ ("send_message", "(chat_id?|user_id?, text, buttons?)", "Send a message, including optional agent-owned buttons."),
71
83
  ("get_chat", "(chat_id?)", "Get metadata for a chat or reply thread."),
72
84
  ("get_messages", "(chat_id?, message_ids)", "Fetch specific Inline messages by ID."),
73
85
  ("get_history", "(chat_id?, limit?, anchor_id?)", "Fetch recent or anchored Inline history."),
74
86
  ("search_messages", "(chat_id?|user_id?, query, limit?, offset_id?)", "Search Inline message text in a chat, thread, or DM."),
75
- ("edit_message", "(chat_id?|user_id?, message_id, text)", "Edit a bot-owned message."),
87
+ ("edit_message", "(chat_id?|user_id?, message_id, text, buttons?)", "Edit a bot-owned message and optionally replace or clear its buttons."),
76
88
  ("delete_message", "(chat_id?|user_id?, message_id)", "Delete a bot-owned message."),
77
89
  ("add_reaction", "(chat_id?|user_id?, message_id?, emoji)", "Add a reaction to an Inline message."),
78
90
  ("remove_reaction", "(chat_id?|user_id?, message_id?, emoji)", "Remove the bot's reaction from an Inline message."),
@@ -83,6 +95,10 @@ _ACTION_MANIFEST = [
83
95
  ("create_thread", "(parent_chat_id?, parent_message_id?, title?)", "Create an Inline reply thread."),
84
96
  ("create_chat", "(title, space_id?, participant_user_ids?, is_public?)", "Create a top-level Inline thread/chat."),
85
97
  ("create_agent", "(name, skill_key?, instructions?)", "Create a named Inline Agent backed by this bot."),
98
+ ("get_agent", "(agent_id)", "Inspect one Agent backed by this bot."),
99
+ ("list_agents", "()", "List Agents backed by this bot."),
100
+ ("update_agent", "(agent_id, fields...)", "Update a named Inline Agent; empty optional fields clear them."),
101
+ ("delete_agent", "(agent_id)", "Delete an Agent backed by this bot."),
86
102
  ("set_presence", "(chat_id?|user_id?, kind, comment?)", "Set the bot avatar presence/status message."),
87
103
  ]
88
104
  _ACTIONS = [name for name, _, _ in _ACTION_MANIFEST]
@@ -139,7 +155,9 @@ def tool_context_prompt(
139
155
  return None
140
156
  lines = [
141
157
  "- The `inline` tool is available for Inline history, search, exact message lookup, reactions, pins, and explicit thread management.",
142
- "- 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.",
158
+ "- Return normal replies as text. Use inline send_message only to create a message with action buttons or to send into a different destination.",
159
+ "- An Inline action-button turn names its source message. A normal response replaces that message; omitted buttons clear the old buttons. Use edit_message with buttons to replace them explicitly.",
160
+ "- If edit_message already finalized the source action card during this turn, return NO_REPLY so the automatic final response does not overwrite that edit.",
143
161
  ]
144
162
  if thread_id:
145
163
  lines.append(f"- Current Inline reply thread: `{thread_id}`. Use this as `chat_id` for thread-scoped reads.")
@@ -166,7 +184,8 @@ INLINE_TOOL_SCHEMA = {
166
184
  + "\n".join(f" {name}{sig} - {desc}" for name, sig, desc in _ACTION_MANIFEST)
167
185
  + "\n\n"
168
186
  "When chat_id is omitted, the tool uses the current Inline reply thread if present, otherwise the current chat. "
169
- "Do not use this tool to send the normal assistant reply; return text normally and Hermes will deliver it. "
187
+ "Return normal assistant replies as text. Use send_message only for a button card or a different destination. "
188
+ "Agent-owned button presses return as ordinary turns, and the normal response replaces the source card while clearing omitted buttons. "
170
189
  "Use create_thread with the current triggering message as parent_message_id to move large top-level discussions into a reply thread. "
171
190
  "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. "
172
191
  "Use search_messages for exact catch-up across older chat history. "
@@ -196,11 +215,12 @@ INLINE_TOOL_SCHEMA = {
196
215
  },
197
216
  "title": {"type": "string", "description": "Thread title. Required for create_chat and optional for create_thread."},
198
217
  "name": {"type": "string", "description": "Agent name for create_agent."},
218
+ "agent_id": {"type": "string", "description": "Globally unique Inline Agent ID for get_agent, update_agent, or delete_agent."},
199
219
  "handle": {"type": "string", "description": "Optional Agent handle."},
200
- "skill_key": {"type": "string", "description": "Optional harness skill key for create_agent."},
201
- "instructions": {"type": "string", "description": "Optional specialized Agent instructions."},
202
- "description": {"type": "string", "description": "Optional thread description for create_thread or create_chat."},
203
- "emoji": {"type": "string", "description": "Optional thread emoji for create_thread/create_chat, or reaction emoji for reaction actions."},
220
+ "skill_key": {"type": "string", "description": "Optional harness skill key for create_agent or update_agent."},
221
+ "instructions": {"type": "string", "description": "Optional specialized Agent instructions for create_agent or update_agent."},
222
+ "description": {"type": "string", "description": "Optional thread or Agent description."},
223
+ "emoji": {"type": "string", "description": "Optional thread or Agent emoji, or reaction emoji for reaction actions."},
204
224
  "space_id": {"type": "string", "description": "Optional parent space ID for create_chat."},
205
225
  "participant_user_ids": {
206
226
  "type": "array",
@@ -213,8 +233,26 @@ INLINE_TOOL_SCHEMA = {
213
233
  "description": "Whether create_chat is visible to the parent space. Defaults to false and requires space_id when true.",
214
234
  },
215
235
  "query": {"type": "string", "description": "Search query for search_messages."},
216
- "text": {"type": "string", "description": "Message text for edit_message."},
217
- "parse_markdown": {"type": "boolean", "description": "Whether Inline should parse Markdown. Defaults to true."},
236
+ "text": {"type": "string", "description": "Message text for send_message or edit_message."},
237
+ "buttons": {
238
+ "type": "array",
239
+ "maxItems": _MAX_ACTION_ROWS,
240
+ "description": "Button rows. Omit to preserve buttons on edit; pass [] to clear them.",
241
+ "items": {
242
+ "type": "array",
243
+ "maxItems": _MAX_ACTIONS_PER_ROW,
244
+ "items": {
245
+ "type": "object",
246
+ "properties": {
247
+ "text": {"type": "string", "description": "Visible button label."},
248
+ "callback_data": {"type": "string", "description": "Opaque data returned in an agent turn when pressed."},
249
+ "copy_text": {"type": "string", "description": "Text copied locally instead of invoking the agent."},
250
+ },
251
+ "required": ["text"],
252
+ },
253
+ },
254
+ },
255
+ "parse_markdown": {"type": "boolean", "description": "Whether Inline should parse its supported Markdown surface. Defaults to true; false preserves the supplied syntax literally."},
218
256
  "limit": {
219
257
  "type": "integer",
220
258
  "minimum": 1,
@@ -261,6 +299,17 @@ def _handle_inline_tool(args: Dict[str, Any], **_: Any) -> str:
261
299
 
262
300
 
263
301
  def _request_for_action(action: str, args: Dict[str, Any]) -> tuple[str, Dict[str, Any]]:
302
+ if action == "send_message":
303
+ body = {
304
+ "target": _target(args),
305
+ "text": _required_str(args, "text", max_chars=_MAX_TEXT_CHARS),
306
+ "parseMarkdown": _bool(args.get("parse_markdown"), True),
307
+ }
308
+ actions = _message_actions(args)
309
+ if actions is not None:
310
+ body["actions"] = actions
311
+ return "/send", body
312
+
264
313
  if action == "get_chat":
265
314
  return "/chat", {"target": _target(args)}
266
315
 
@@ -292,12 +341,16 @@ def _request_for_action(action: str, args: Dict[str, Any]) -> tuple[str, Dict[st
292
341
  return "/search", body
293
342
 
294
343
  if action == "edit_message":
295
- return "/edit", {
344
+ body = {
296
345
  "target": _target(args),
297
346
  "messageId": _required_id(args, "message_id"),
298
347
  "text": _required_str(args, "text", max_chars=_MAX_TEXT_CHARS),
299
348
  "parseMarkdown": _bool(args.get("parse_markdown"), True),
300
349
  }
350
+ actions = _message_actions(args)
351
+ if actions is not None:
352
+ body["actions"] = actions
353
+ return "/edit", body
301
354
 
302
355
  if action == "delete_message":
303
356
  return "/delete", {
@@ -376,6 +429,26 @@ def _request_for_action(action: str, args: Dict[str, Any]) -> tuple[str, Dict[st
376
429
  body[key] = value
377
430
  return "/create-agent", body
378
431
 
432
+ if action == "get_agent":
433
+ return "/get-agent", {"agentId": _required_id(args, "agent_id")}
434
+
435
+ if action == "list_agents":
436
+ return "/list-agents", {}
437
+
438
+ if action == "update_agent":
439
+ body = {"agentId": _required_id(args, "agent_id")}
440
+ if "name" in args:
441
+ body["name"] = _required_str(args, "name", max_chars=256)
442
+ for key in ("handle", "emoji", "description", "skill_key", "instructions"):
443
+ if key in args and args.get(key) is not None:
444
+ body[key] = _str(args.get(key))
445
+ if len(body) == 1:
446
+ raise InlineToolError("update_agent requires at least one field", "bad_format")
447
+ return "/update-agent", body
448
+
449
+ if action == "delete_agent":
450
+ return "/delete-agent", {"agentId": _required_id(args, "agent_id")}
451
+
379
452
  if action == "set_presence":
380
453
  kind = _str(args.get("kind"))
381
454
  if kind not in _PRESENCE_KINDS:
@@ -504,7 +577,75 @@ def _message_id_or_current(args: Dict[str, Any]) -> str:
504
577
  return message_id
505
578
 
506
579
 
580
+ def _message_actions(args: Dict[str, Any]) -> Optional[Dict[str, Any]]:
581
+ if "buttons" not in args:
582
+ return None
583
+ raw_rows = args.get("buttons")
584
+ if not isinstance(raw_rows, list):
585
+ raise InlineToolError("buttons must be an array of button rows", "bad_format")
586
+ if len(raw_rows) > _MAX_ACTION_ROWS:
587
+ raise InlineToolError(f"buttons supports at most {_MAX_ACTION_ROWS} rows", "bad_format")
588
+
589
+ rows: list[Dict[str, Any]] = []
590
+ for row_index, raw_row in enumerate(raw_rows):
591
+ if not isinstance(raw_row, list) or not raw_row:
592
+ raise InlineToolError("each buttons row must be a non-empty array", "bad_format")
593
+ if len(raw_row) > _MAX_ACTIONS_PER_ROW:
594
+ raise InlineToolError(
595
+ f"each buttons row supports at most {_MAX_ACTIONS_PER_ROW} buttons",
596
+ "bad_format",
597
+ )
598
+ actions: list[Dict[str, str]] = []
599
+ for action_index, raw_button in enumerate(raw_row):
600
+ if not isinstance(raw_button, dict):
601
+ raise InlineToolError("each button must be an object", "bad_format")
602
+ label = _str(raw_button.get("text"))
603
+ if not label:
604
+ raise InlineToolError("button text is required", "bad_format")
605
+ if len(label) > _MAX_ACTION_LABEL_CHARS:
606
+ raise InlineToolError(
607
+ f"button text supports at most {_MAX_ACTION_LABEL_CHARS} characters",
608
+ "bad_format",
609
+ )
610
+ has_callback = "callback_data" in raw_button
611
+ has_copy = "copy_text" in raw_button
612
+ if has_callback == has_copy:
613
+ raise InlineToolError(
614
+ "each button requires exactly one of callback_data or copy_text",
615
+ "bad_format",
616
+ )
617
+ action: Dict[str, str] = {
618
+ "id": build_inline_agent_action_id(row_index, action_index),
619
+ "text": label,
620
+ }
621
+ if has_callback:
622
+ callback_data = raw_button.get("callback_data")
623
+ if not isinstance(callback_data, str):
624
+ raise InlineToolError("callback_data must be a string", "bad_format")
625
+ if len(callback_data.encode("utf-8")) > _MAX_ACTION_CALLBACK_BYTES:
626
+ raise InlineToolError(
627
+ f"callback_data supports at most {_MAX_ACTION_CALLBACK_BYTES} UTF-8 bytes",
628
+ "bad_format",
629
+ )
630
+ action["callback"] = callback_data
631
+ else:
632
+ copy_text = raw_button.get("copy_text")
633
+ if not isinstance(copy_text, str) or not copy_text:
634
+ raise InlineToolError("copy_text must be a non-empty string", "bad_format")
635
+ if len(copy_text) > _MAX_ACTION_COPY_TEXT_CHARS:
636
+ raise InlineToolError(
637
+ f"copy_text supports at most {_MAX_ACTION_COPY_TEXT_CHARS} characters",
638
+ "bad_format",
639
+ )
640
+ action["copyText"] = copy_text
641
+ actions.append(action)
642
+ rows.append({"actions": actions})
643
+ return {"rows": rows}
644
+
645
+
507
646
  def _compact_result(action: str, result: Dict[str, Any]) -> Dict[str, Any]:
647
+ if action in {"send_message", "edit_message"}:
648
+ return {"messageId": _str(result.get("messageId"))}
508
649
  if action == "get_chat":
509
650
  return _compact_chat(result)
510
651
  if action in {"get_messages", "get_history", "search_messages"}:
@@ -545,8 +686,17 @@ def _compact_result(action: str, result: Dict[str, Any]) -> Dict[str, Any]:
545
686
  "chat": _compact_chat(result.get("chat") if isinstance(result.get("chat"), dict) else {}),
546
687
  "dialog": _summarize_value(result.get("dialog")) if result.get("dialog") is not None else None,
547
688
  }
548
- if action == "create_agent":
689
+ if action in {"create_agent", "update_agent"}:
549
690
  return {"agent": _summarize_value(result.get("agent"))}
691
+ if action == "get_agent":
692
+ return {
693
+ "bot": _summarize_value(result.get("bot")),
694
+ "agent": _summarize_value(result.get("agent")),
695
+ }
696
+ if action == "list_agents":
697
+ return {"agents": _summarize_value(result.get("agents"))}
698
+ if action == "delete_agent":
699
+ return {"agentId": _str(result.get("agentId") or result.get("agent_id"))}
550
700
  return result
551
701
 
552
702