@inline-chat/hermes-agent-adapter 0.0.12 → 0.0.14
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 +28 -3
- package/package.json +6 -5
- package/plugin/inline/adapter.py +92 -11
- package/plugin/inline/cli.py +48 -1
- package/plugin/inline/plugin.yaml +1 -1
- package/plugin/inline/sidecar/index.mjs +2751 -273
- package/plugin/inline/telemetry.py +222 -0
package/README.md
CHANGED
|
@@ -147,7 +147,7 @@ uv run ./hermes plugins list --plain --no-bundled
|
|
|
147
147
|
Expected local output includes:
|
|
148
148
|
|
|
149
149
|
```text
|
|
150
|
-
enabled user 0.0.
|
|
150
|
+
enabled user 0.0.14 inline-platform
|
|
151
151
|
```
|
|
152
152
|
|
|
153
153
|
## Update Or Reinstall
|
|
@@ -170,10 +170,11 @@ mismatch, rerun the same command after rebuilding or upgrading the package.
|
|
|
170
170
|
|
|
171
171
|
- Hermes Agent: requires the external user plugin registry and native platform
|
|
172
172
|
plugin loader available in Hermes Agent `0.17.x`. This package was validated
|
|
173
|
-
against Hermes Agent `0.
|
|
173
|
+
against Hermes Agent `0.21.0` from source commit `29112bef` (tag
|
|
174
|
+
`v2026.8.31`).
|
|
174
175
|
- Node.js: `>=20` is required for the bundled sidecar. Hermes-managed Node 22,
|
|
175
176
|
system Node, or an explicit `INLINE_NODE_BIN` path all work.
|
|
176
|
-
- Inline transport: the sidecar uses `@inline-chat/realtime-sdk@0.0.
|
|
177
|
+
- Inline transport: the sidecar uses `@inline-chat/realtime-sdk@0.0.17` and is
|
|
177
178
|
bundled into the npm package, so Hermes startup does not run `npm install`.
|
|
178
179
|
- Live sends require a valid Inline user or bot token in `INLINE_TOKEN`,
|
|
179
180
|
`INLINE_BOT_TOKEN`, `platforms.inline.token`, or `inline.token`.
|
|
@@ -313,6 +314,8 @@ Access control follows Hermes' native platform model:
|
|
|
313
314
|
| `INLINE_CONNECT_RETRY_MAX_MS` | Maximum sidecar retry delay after repeated realtime startup failures. Defaults to `15000`. |
|
|
314
315
|
| `INLINE_SIDECAR_PORT` | Fixed loopback port for the sidecar. Must be `1` through `65535`. Defaults to `8794`; `test-send` uses a random free port. |
|
|
315
316
|
| `INLINE_SIDECAR_BIND` | Sidecar bind host. Must be loopback: `127.0.0.1`, `localhost`, or `::1`. Defaults to `127.0.0.1`. |
|
|
317
|
+
| `INLINE_HERMES_SENTRY_DSN` | Explicitly enables adapter and sidecar error reporting to the configured collector. Reporting is disabled when unset. |
|
|
318
|
+
| `INLINE_PLUGIN_TELEMETRY` | Set to `off`, `0`, or `false` to disable explicitly configured plugin error reporting. `DO_NOT_TRACK=1` is also honored. |
|
|
316
319
|
| `platforms.inline.typing_indicator` | Hermes-native toggle for Inline typing/presence while a turn is running. Defaults to `true`; set to `false` to keep busy threads visually quiet. |
|
|
317
320
|
| `platforms.inline.gateway_restart_notification` | Hermes-native toggle for gateway online/restarted notices. Defaults to `true`. |
|
|
318
321
|
|
|
@@ -399,6 +402,28 @@ the normal reply.
|
|
|
399
402
|
The plugin id is `inline`, which is intentionally the same id an eventual
|
|
400
403
|
bundled Hermes adapter should use.
|
|
401
404
|
|
|
405
|
+
## Error Reporting And Privacy
|
|
406
|
+
|
|
407
|
+
The Inline adapter and its supervised sidecar keep error reporting disabled
|
|
408
|
+
unless the operator sets `INLINE_HERMES_SENTRY_DSN` to an explicit collector.
|
|
409
|
+
When enabled, reports include the raw exception type and message, traceback
|
|
410
|
+
paths, line/function locations, plugin release,
|
|
411
|
+
operation name, runtime, OS, and architecture so maintainers can diagnose
|
|
412
|
+
failures in subsequent releases.
|
|
413
|
+
|
|
414
|
+
Reports do not attach Inline or Hermes message events, request bodies,
|
|
415
|
+
user/chat/account identifiers, breadcrumbs, source context, or stack locals.
|
|
416
|
+
Known token, password, authorization, sidecar credential, and secret-shaped
|
|
417
|
+
values are redacted before upload. Inline's Sentry project also enables default
|
|
418
|
+
server-side data scrubbing and IP-address scrubbing. Because dependency
|
|
419
|
+
exception messages are preserved for diagnosis, they can still contain values
|
|
420
|
+
the dependency itself chose to place in an error.
|
|
421
|
+
|
|
422
|
+
Remove `INLINE_HERMES_SENTRY_DSN`, set `INLINE_PLUGIN_TELEMETRY=off`, or set
|
|
423
|
+
`DO_NOT_TRACK=1` to disable both adapter and sidecar reporting. Reporting is
|
|
424
|
+
best-effort, has a two-second network deadline, and never changes plugin success
|
|
425
|
+
or failure behavior.
|
|
426
|
+
|
|
402
427
|
## Troubleshooting
|
|
403
428
|
|
|
404
429
|
Run `inline-hermes doctor --json` first. It checks the installed plugin path,
|
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.14",
|
|
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",
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"plugin/inline/cli.py",
|
|
34
34
|
"plugin/inline/message_actions.py",
|
|
35
35
|
"plugin/inline/plugin.yaml",
|
|
36
|
+
"plugin/inline/telemetry.py",
|
|
36
37
|
"plugin/inline/tools.py",
|
|
37
38
|
"plugin/inline/sidecar/index.mjs"
|
|
38
39
|
],
|
|
@@ -50,7 +51,7 @@
|
|
|
50
51
|
},
|
|
51
52
|
"scripts": {
|
|
52
53
|
"build": "mkdir -p plugin/inline/sidecar && bun build ./src/install.ts --outdir dist --entry-naming '[name].js' --target=node --format=esm --packages=bundle && bun build ./src/sidecar/index.ts --outdir plugin/inline/sidecar --entry-naming 'index.mjs' --target=node --format=esm --packages=bundle && tsc -p tsconfig.json --emitDeclarationOnly",
|
|
53
|
-
"lint": "bunx oxlint --ignore-path ../.oxlintignore scripts/release-stage.mjs src/install.ts src/sidecar/contract.ts src/sidecar/index.ts src/sidecar/user-directory.ts src/sidecar/user-directory.test.ts tests/adapter-python.test.ts tests/install.test.ts tests/package-artifact.test.ts tests/sidecar-contract.test.ts tests/sidecar-runtime.test.ts vitest.config.ts",
|
|
54
|
+
"lint": "bunx oxlint --ignore-path ../.oxlintignore scripts/release-stage.mjs src/install.ts src/sidecar/contract.ts src/sidecar/index.ts src/sidecar/telemetry.ts src/sidecar/telemetry.test.ts src/sidecar/user-directory.ts src/sidecar/user-directory.test.ts tests/adapter-python.test.ts tests/install.test.ts tests/package-artifact.test.ts tests/sidecar-contract.test.ts tests/sidecar-runtime.test.ts tests/telemetry-python.test.ts vitest.config.ts",
|
|
54
55
|
"pretest": "bun run build",
|
|
55
56
|
"prepack": "bun run build",
|
|
56
57
|
"prepublishOnly": "bun run check",
|
|
@@ -61,7 +62,7 @@
|
|
|
61
62
|
"check": "bun run typecheck && bun run lint && bun run test"
|
|
62
63
|
},
|
|
63
64
|
"dependencies": {
|
|
64
|
-
"@inline-chat/realtime-sdk": "0.0.
|
|
65
|
+
"@inline-chat/realtime-sdk": "0.0.17",
|
|
65
66
|
"yaml": "2.9.0"
|
|
66
67
|
},
|
|
67
68
|
"devDependencies": {
|
|
@@ -81,7 +82,7 @@
|
|
|
81
82
|
},
|
|
82
83
|
"machineSetupProtocol": 1,
|
|
83
84
|
"minHermesVersion": "0.17.0",
|
|
84
|
-
"testedHermesVersion": "0.
|
|
85
|
-
"testedHermesCommit": "
|
|
85
|
+
"testedHermesVersion": "0.21.0",
|
|
86
|
+
"testedHermesCommit": "29112bef"
|
|
86
87
|
}
|
|
87
88
|
}
|
package/plugin/inline/adapter.py
CHANGED
|
@@ -55,6 +55,7 @@ from .message_actions import (
|
|
|
55
55
|
parse_inline_agent_action_reply_target,
|
|
56
56
|
resolve_inline_message_action_ownership,
|
|
57
57
|
)
|
|
58
|
+
from .telemetry import capture_plugin_error
|
|
58
59
|
|
|
59
60
|
logger = logging.getLogger(__name__)
|
|
60
61
|
|
|
@@ -600,6 +601,36 @@ def _inline_menu_commands(max_commands: int = _INLINE_COMMAND_LIMIT) -> tuple[Li
|
|
|
600
601
|
return commands, hidden_count + hidden_local + skipped
|
|
601
602
|
|
|
602
603
|
|
|
604
|
+
def _inline_skill_catalog() -> List[Dict[str, Any]]:
|
|
605
|
+
"""Return installed, enabled Hermes skills in the Bot API catalog shape."""
|
|
606
|
+
from tools.skills_tool import _find_all_skills, _sort_skills
|
|
607
|
+
|
|
608
|
+
published: List[Dict[str, Any]] = []
|
|
609
|
+
seen: set[str] = set()
|
|
610
|
+
for skill in _sort_skills(_find_all_skills(skip_disabled=False)):
|
|
611
|
+
if len(published) >= 250:
|
|
612
|
+
break
|
|
613
|
+
key = str(skill.get("name") or "").strip()
|
|
614
|
+
if not key or _utf16_length(key) > 256 or key in seen:
|
|
615
|
+
continue
|
|
616
|
+
# At most two UTF-16 code units per Python Unicode scalar keeps the
|
|
617
|
+
# 4,000-unit validation bound even for non-BMP descriptions.
|
|
618
|
+
description = str(skill.get("description") or "").strip()[:2000].rstrip()
|
|
619
|
+
seen.add(key)
|
|
620
|
+
published.append({
|
|
621
|
+
"key": key,
|
|
622
|
+
"name": key,
|
|
623
|
+
**({"description": description} if description else {}),
|
|
624
|
+
"sort_order": len(published),
|
|
625
|
+
})
|
|
626
|
+
return published
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
def _utf16_length(value: str) -> int:
|
|
630
|
+
"""Match JavaScript and Bot API string-length validation."""
|
|
631
|
+
return len(value.encode("utf-16-le", errors="surrogatepass")) // 2
|
|
632
|
+
|
|
633
|
+
|
|
603
634
|
def _token_value(raw: Any) -> str:
|
|
604
635
|
if raw is None:
|
|
605
636
|
return ""
|
|
@@ -971,6 +1002,18 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
971
1002
|
self._context_backfill_seen: "OrderedDict[str, float]" = OrderedDict()
|
|
972
1003
|
self._reply_thread_overrides = self._load_reply_thread_overrides()
|
|
973
1004
|
|
|
1005
|
+
def _report_error(self, operation: str, error: BaseException, *, handled: bool = True) -> None:
|
|
1006
|
+
try:
|
|
1007
|
+
capture_plugin_error(
|
|
1008
|
+
operation,
|
|
1009
|
+
error,
|
|
1010
|
+
handled=handled,
|
|
1011
|
+
secrets=(self._token, self._sidecar_token),
|
|
1012
|
+
)
|
|
1013
|
+
except Exception:
|
|
1014
|
+
# Observability must never alter adapter behavior.
|
|
1015
|
+
pass
|
|
1016
|
+
|
|
974
1017
|
@staticmethod
|
|
975
1018
|
def _parse_id_set(raw: Any) -> set[str]:
|
|
976
1019
|
if raw is None:
|
|
@@ -1014,6 +1057,7 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
1014
1057
|
except FileNotFoundError:
|
|
1015
1058
|
return {}
|
|
1016
1059
|
except Exception as exc:
|
|
1060
|
+
self._report_error("settings.load", exc)
|
|
1017
1061
|
logger.warning("[inline] failed to load Inline adapter settings: %s", exc)
|
|
1018
1062
|
return {}
|
|
1019
1063
|
raw = data.get("reply_threads") if isinstance(data, dict) else None
|
|
@@ -1045,6 +1089,7 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
1045
1089
|
tmp_path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
1046
1090
|
tmp_path.replace(self._settings_path)
|
|
1047
1091
|
except Exception as exc:
|
|
1092
|
+
self._report_error("settings.save", exc)
|
|
1048
1093
|
logger.warning("[inline] failed to save Inline adapter settings: %s", exc)
|
|
1049
1094
|
|
|
1050
1095
|
def _reply_thread_mode_for_chat(self, chat_id: str, parent_chat_id: Optional[str] = None) -> str:
|
|
@@ -1393,6 +1438,7 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
1393
1438
|
except asyncio.CancelledError:
|
|
1394
1439
|
raise
|
|
1395
1440
|
except Exception as exc:
|
|
1441
|
+
self._report_error("settings.answer", exc)
|
|
1396
1442
|
logger.warning("[inline] agent settings answer expired: %s", exc)
|
|
1397
1443
|
return False
|
|
1398
1444
|
|
|
@@ -1416,6 +1462,7 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
1416
1462
|
context = await self._bot_settings_context(event)
|
|
1417
1463
|
response = {"result": {"oneofKind": "document", "document": self._bot_settings_document(context)}}
|
|
1418
1464
|
except Exception as exc:
|
|
1465
|
+
self._report_error("settings.load", exc)
|
|
1419
1466
|
logger.warning("[inline] failed to load Hermes agent settings: %s", exc)
|
|
1420
1467
|
response = self._bot_settings_problem(_INLINE_BOT_SETTINGS_FAILED, "Hermes could not load settings.")
|
|
1421
1468
|
answered = await self._answer_bot_settings(request_id, response)
|
|
@@ -1530,6 +1577,7 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
1530
1577
|
except ValueError as exc:
|
|
1531
1578
|
response = self._bot_settings_problem(_INLINE_BOT_SETTINGS_INVALID_VALUE, str(exc).capitalize())
|
|
1532
1579
|
except Exception as exc:
|
|
1580
|
+
self._report_error("settings.update", exc)
|
|
1533
1581
|
logger.warning("[inline] failed to update Hermes agent settings: %s", exc)
|
|
1534
1582
|
response = self._bot_settings_problem(_INLINE_BOT_SETTINGS_FAILED, "Hermes could not update this setting.")
|
|
1535
1583
|
answered = await self._answer_bot_settings(request_id, response)
|
|
@@ -1776,6 +1824,7 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
1776
1824
|
try:
|
|
1777
1825
|
await self._sidecar_call("/follow-mode", {"target": target, "mode": mode})
|
|
1778
1826
|
except Exception as exc:
|
|
1827
|
+
self._report_error("follow_mode.update", exc)
|
|
1779
1828
|
logger.warning("[inline] /%s failed for chat %s: %s", command, chat_id, exc)
|
|
1780
1829
|
await self.send(
|
|
1781
1830
|
chat_id,
|
|
@@ -1814,6 +1863,7 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
1814
1863
|
try:
|
|
1815
1864
|
await self._start_sidecar()
|
|
1816
1865
|
except Exception as exc:
|
|
1866
|
+
self._report_error("sidecar.start", exc, handled=False)
|
|
1817
1867
|
self._set_fatal_error("SIDECAR_FAILED", f"failed to start Inline sidecar: {exc}", retryable=True)
|
|
1818
1868
|
await self._stop_sidecar()
|
|
1819
1869
|
await self._http_client.aclose()
|
|
@@ -1965,8 +2015,6 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
1965
2015
|
self._sidecar_supervisor_task = None
|
|
1966
2016
|
|
|
1967
2017
|
def _schedule_bot_command_sync(self) -> None:
|
|
1968
|
-
if not self._sync_commands:
|
|
1969
|
-
return
|
|
1970
2018
|
if self._command_sync_task is not None and not self._command_sync_task.done():
|
|
1971
2019
|
return
|
|
1972
2020
|
self._command_sync_task = asyncio.get_event_loop().create_task(self._run_bot_command_sync())
|
|
@@ -1974,9 +2022,11 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
1974
2022
|
async def _run_bot_command_sync(self) -> None:
|
|
1975
2023
|
try:
|
|
1976
2024
|
await self._sync_bot_commands()
|
|
2025
|
+
await self._sync_bot_skills()
|
|
1977
2026
|
except asyncio.CancelledError:
|
|
1978
2027
|
raise
|
|
1979
2028
|
except Exception as exc:
|
|
2029
|
+
self._report_error("commands_and_skills.sync", exc)
|
|
1980
2030
|
logger.warning("[inline] bot command sync failed: %s", exc)
|
|
1981
2031
|
finally:
|
|
1982
2032
|
if asyncio.current_task() is self._command_sync_task:
|
|
@@ -1996,8 +2046,20 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
1996
2046
|
hidden_suffix = f", {hidden_count} hidden" if hidden_count else ""
|
|
1997
2047
|
logger.info("[inline] bot commands synced (%d command%s%s)", len(synced), "" if len(synced) == 1 else "s", hidden_suffix)
|
|
1998
2048
|
except Exception as exc:
|
|
2049
|
+
self._report_error("commands.sync", exc)
|
|
1999
2050
|
logger.warning("[inline] bot command sync failed: %s", exc)
|
|
2000
2051
|
|
|
2052
|
+
async def _sync_bot_skills(self) -> None:
|
|
2053
|
+
if self._http_client is None:
|
|
2054
|
+
return
|
|
2055
|
+
try:
|
|
2056
|
+
skills = _inline_skill_catalog()
|
|
2057
|
+
await self._call_bot_api("setMySkills", {"skills": skills})
|
|
2058
|
+
logger.info("[inline] bot skills synced (%d skill%s)", len(skills), "" if len(skills) == 1 else "s")
|
|
2059
|
+
except Exception as exc:
|
|
2060
|
+
self._report_error("skills.sync", exc)
|
|
2061
|
+
logger.warning("[inline] bot skill sync failed: %s", exc)
|
|
2062
|
+
|
|
2001
2063
|
async def _set_bot_commands_with_retry(self, commands: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
2002
2064
|
try:
|
|
2003
2065
|
await self._call_bot_api("setMyCommands", {"commands": commands})
|
|
@@ -2087,6 +2149,7 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
2087
2149
|
try:
|
|
2088
2150
|
completed.result()
|
|
2089
2151
|
except Exception as exc:
|
|
2152
|
+
self._report_error("settings.task", exc)
|
|
2090
2153
|
logger.warning("[inline] agent settings task failed: %s", exc)
|
|
2091
2154
|
|
|
2092
2155
|
task.add_done_callback(finished)
|
|
@@ -2114,6 +2177,7 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
2114
2177
|
except Exception as exc:
|
|
2115
2178
|
if not self._inbound_running:
|
|
2116
2179
|
break
|
|
2180
|
+
self._report_error("inbound.stream", exc)
|
|
2117
2181
|
logger.warning("[inline] inbound stream dropped (%s); reconnecting in %.1fs", exc, backoff)
|
|
2118
2182
|
await asyncio.sleep(backoff)
|
|
2119
2183
|
backoff = min(backoff * 2, 30.0)
|
|
@@ -2367,8 +2431,13 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
2367
2431
|
agent_instructions = str(agent.get("instructions") or "").strip()
|
|
2368
2432
|
agent_skill = str(agent.get("skillKey") or agent.get("skill_key") or "").strip()
|
|
2369
2433
|
if agent_name:
|
|
2370
|
-
|
|
2371
|
-
|
|
2434
|
+
if agent_instructions:
|
|
2435
|
+
channel_prompt = self._merge_channel_prompt(channel_prompt, agent_instructions)
|
|
2436
|
+
elif not agent_skill:
|
|
2437
|
+
channel_prompt = self._merge_channel_prompt(
|
|
2438
|
+
channel_prompt,
|
|
2439
|
+
f'You are a specialized agent named "{agent_name}". Proceed with the user\'s request.',
|
|
2440
|
+
)
|
|
2372
2441
|
if agent_skill:
|
|
2373
2442
|
auto_skill = list(dict.fromkeys([*(auto_skill or []), agent_skill]))
|
|
2374
2443
|
except Exception as exc:
|
|
@@ -3749,7 +3818,8 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
3749
3818
|
return True
|
|
3750
3819
|
await self._finish_action(event, "Type your answer", "Type your answer:")
|
|
3751
3820
|
return True
|
|
3752
|
-
except Exception:
|
|
3821
|
+
except Exception as exc:
|
|
3822
|
+
self._report_error("action.clarify_other", exc)
|
|
3753
3823
|
logger.exception("[inline] clarify other failed")
|
|
3754
3824
|
return True
|
|
3755
3825
|
try:
|
|
@@ -3767,7 +3837,8 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
3767
3837
|
self._clarify_choices.pop(clarify_id, None)
|
|
3768
3838
|
await self._finish_action(event, "Prompt expired", "Clarification expired.")
|
|
3769
3839
|
return True
|
|
3770
|
-
except Exception:
|
|
3840
|
+
except Exception as exc:
|
|
3841
|
+
self._report_error("action.clarify", exc)
|
|
3771
3842
|
logger.exception("[inline] clarify action failed")
|
|
3772
3843
|
return True
|
|
3773
3844
|
|
|
@@ -3800,7 +3871,8 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
3800
3871
|
toast, text = labels[choice]
|
|
3801
3872
|
await self._finish_action(event, toast, text)
|
|
3802
3873
|
return True
|
|
3803
|
-
except Exception:
|
|
3874
|
+
except Exception as exc:
|
|
3875
|
+
self._report_error("action.approval", exc)
|
|
3804
3876
|
logger.exception("[inline] approval action failed")
|
|
3805
3877
|
return True
|
|
3806
3878
|
|
|
@@ -3823,7 +3895,8 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
3823
3895
|
text = str(result or ("Cancelled." if choice == "cancel" else "Recorded."))
|
|
3824
3896
|
await self._finish_action(event, "Recorded", text)
|
|
3825
3897
|
return True
|
|
3826
|
-
except Exception:
|
|
3898
|
+
except Exception as exc:
|
|
3899
|
+
self._report_error("action.slash_confirm", exc)
|
|
3827
3900
|
logger.exception("[inline] slash confirm action failed")
|
|
3828
3901
|
return True
|
|
3829
3902
|
|
|
@@ -3867,7 +3940,8 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
3867
3940
|
self._thread_action_sessions.pop(session_id, None)
|
|
3868
3941
|
await self._answer_action(interaction_id, f"Reply threads: {mode}")
|
|
3869
3942
|
return True
|
|
3870
|
-
except Exception:
|
|
3943
|
+
except Exception as exc:
|
|
3944
|
+
self._report_error("action.thread", exc)
|
|
3871
3945
|
logger.exception("[inline] thread action failed")
|
|
3872
3946
|
await self._answer_action(interaction_id, "Thread setting failed")
|
|
3873
3947
|
return True
|
|
@@ -3957,6 +4031,7 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
3957
4031
|
result_text = await result_text
|
|
3958
4032
|
result_text = str(result_text or "Selection applied.")
|
|
3959
4033
|
except Exception as exc:
|
|
4034
|
+
self._report_error("action.choice_picker", exc)
|
|
3960
4035
|
logger.error("[inline] choice picker selection failed (%s)", type(exc).__name__)
|
|
3961
4036
|
result_text = "Selection failed; try again."
|
|
3962
4037
|
failed = True
|
|
@@ -3995,7 +4070,8 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
3995
4070
|
tmp_path = response_path.with_suffix(".tmp")
|
|
3996
4071
|
tmp_path.write_text(answer, encoding="utf-8")
|
|
3997
4072
|
tmp_path.replace(response_path)
|
|
3998
|
-
except Exception:
|
|
4073
|
+
except Exception as exc:
|
|
4074
|
+
self._report_error("action.update_prompt", exc)
|
|
3999
4075
|
logger.exception("[inline] failed to write Hermes update response")
|
|
4000
4076
|
await self._answer_action(interaction_id, "Response failed; try again")
|
|
4001
4077
|
return True
|
|
@@ -4171,6 +4247,7 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
4171
4247
|
result_text = await result_text
|
|
4172
4248
|
result_text = str(result_text or "Model switched.")
|
|
4173
4249
|
except Exception as exc:
|
|
4250
|
+
self._report_error("action.model_picker", exc)
|
|
4174
4251
|
logger.error("[inline] model picker switch failed (%s)", type(exc).__name__)
|
|
4175
4252
|
result_text = "Model switch failed; try again."
|
|
4176
4253
|
failed = True
|
|
@@ -4600,6 +4677,7 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
4600
4677
|
result = data.get("result") or {}
|
|
4601
4678
|
return str(result.get("chatId") or "") or None
|
|
4602
4679
|
except Exception as exc:
|
|
4680
|
+
self._report_error("thread.create", exc)
|
|
4603
4681
|
logger.debug("[inline] create handoff thread failed: %s", exc)
|
|
4604
4682
|
return None
|
|
4605
4683
|
|
|
@@ -4779,7 +4857,7 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
4779
4857
|
]}]},
|
|
4780
4858
|
})
|
|
4781
4859
|
if not result.success:
|
|
4782
|
-
# Hermes
|
|
4860
|
+
# Hermes treats any non-raising hook call as delivered and
|
|
4783
4861
|
# otherwise suppresses its plaintext /approve and /deny fallback.
|
|
4784
4862
|
raise RuntimeError(result.error or "failed to send Inline update prompt")
|
|
4785
4863
|
self._remember(self._update_prompt_sessions, prompt_id, {
|
|
@@ -5011,6 +5089,8 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
5011
5089
|
raw_response=result,
|
|
5012
5090
|
)
|
|
5013
5091
|
except InlineSidecarError as exc:
|
|
5092
|
+
if exc.error_kind == "unknown":
|
|
5093
|
+
self._report_error("sidecar.send", exc)
|
|
5014
5094
|
return _send_result(
|
|
5015
5095
|
success=False,
|
|
5016
5096
|
error=str(exc),
|
|
@@ -5019,6 +5099,7 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
5019
5099
|
error_kind=exc.error_kind,
|
|
5020
5100
|
)
|
|
5021
5101
|
except Exception as exc:
|
|
5102
|
+
self._report_error("sidecar.send", exc)
|
|
5022
5103
|
return _send_result(success=False, error=str(exc), retryable=self._is_retryable_error(str(exc)))
|
|
5023
5104
|
|
|
5024
5105
|
async def _sidecar_call(self, path: str, body: Dict[str, Any]) -> Dict[str, Any]:
|
package/plugin/inline/cli.py
CHANGED
|
@@ -7,6 +7,7 @@ the plugin has already been discovered by Hermes.
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
9
|
import argparse
|
|
10
|
+
import hashlib
|
|
10
11
|
import json
|
|
11
12
|
import os
|
|
12
13
|
import re
|
|
@@ -26,7 +27,7 @@ _CLI_INSTALL_URL = "https://inline.chat/cli/install.sh"
|
|
|
26
27
|
_MAX_TOKEN_BYTES = 16 * 1024
|
|
27
28
|
_MAX_PROBE_RESPONSE_BYTES = 64 * 1024
|
|
28
29
|
_MACHINE_SETUP_PROTOCOL_VERSION = 1
|
|
29
|
-
_PROBE_USER_AGENT = "inline-hermes-agent-adapter/0.0.
|
|
30
|
+
_PROBE_USER_AGENT = "inline-hermes-agent-adapter/0.0.14"
|
|
30
31
|
_ENV_REFERENCE_RE = re.compile(r"^\$\{([A-Za-z_][A-Za-z0-9_]*)\}$")
|
|
31
32
|
|
|
32
33
|
|
|
@@ -463,6 +464,7 @@ def _status(args) -> int:
|
|
|
463
464
|
"sidecar": sidecar,
|
|
464
465
|
"node": node,
|
|
465
466
|
"probeRequested": probe_requested,
|
|
467
|
+
"gateway": _gateway_status(),
|
|
466
468
|
**({"probe": probe} if probe is not None else {}),
|
|
467
469
|
}
|
|
468
470
|
if getattr(args, "json", False):
|
|
@@ -480,6 +482,51 @@ def _status(args) -> int:
|
|
|
480
482
|
return 0 if runtime_usable and (not probe_requested or ready) else 1
|
|
481
483
|
|
|
482
484
|
|
|
485
|
+
def _gateway_status() -> dict:
|
|
486
|
+
"""Project local runtime facts without changing the credential-status contract."""
|
|
487
|
+
unavailable = {"supported": False, "ready": False, "reason": "runtime_status_unavailable"}
|
|
488
|
+
try:
|
|
489
|
+
from gateway.status import read_runtime_status, get_runtime_status_running_pid
|
|
490
|
+
from hermes_constants import get_hermes_home
|
|
491
|
+
|
|
492
|
+
runtime = read_runtime_status()
|
|
493
|
+
# Also validate this helper contract for an absent runtime: older hosts
|
|
494
|
+
# lack expected_home and must not claim support until after installation.
|
|
495
|
+
pid = get_runtime_status_running_pid(runtime if isinstance(runtime, dict) else {}, expected_home=get_hermes_home())
|
|
496
|
+
if not isinstance(runtime, dict):
|
|
497
|
+
return {"supported": True, "ready": False, "reason": "missing_status"}
|
|
498
|
+
# Return only an opaque generation, never process IDs or raw state.
|
|
499
|
+
start_time = runtime.get("start_time")
|
|
500
|
+
valid_identity = type(pid) is int and pid > 0 and type(start_time) is int and start_time >= 0
|
|
501
|
+
if pid and not valid_identity:
|
|
502
|
+
return {"supported": False, "ready": False, "reason": "runtime_identity_unavailable"}
|
|
503
|
+
generation = hashlib.sha256(f"inline-gateway:{pid}:{start_time}".encode()).hexdigest() if valid_identity else None
|
|
504
|
+
state = runtime.get("gateway_state")
|
|
505
|
+
platforms = runtime.get("platforms")
|
|
506
|
+
inline = platforms.get("inline") if isinstance(platforms, dict) else None
|
|
507
|
+
platform_state = inline.get("state") if isinstance(inline, dict) else None
|
|
508
|
+
state = state if state in ("starting", "running", "draining", "stopping", "stopped", "startup_failed") else "unknown"
|
|
509
|
+
platform_state = platform_state if platform_state in ("connected", "connecting", "retrying", "fatal", "disconnected") else "unknown"
|
|
510
|
+
if pid and isinstance(inline, dict) and ("writer_pid" not in inline or "writer_start_time" not in inline):
|
|
511
|
+
return {"supported": False, "ready": False, "reason": "runtime_writer_identity_unavailable"}
|
|
512
|
+
# Runtime status can retain platform entries from the prior process.
|
|
513
|
+
# Only the current process's own connected projection is readiness proof.
|
|
514
|
+
writer_matches = isinstance(inline, dict) and inline.get("writer_pid") == pid and inline.get("writer_start_time") == start_time
|
|
515
|
+
ready = generation is not None and writer_matches and state == "running" and platform_state == "connected"
|
|
516
|
+
return {
|
|
517
|
+
"supported": True,
|
|
518
|
+
"ready": ready,
|
|
519
|
+
**({"generation": generation} if generation is not None else {}),
|
|
520
|
+
"gatewayState": state,
|
|
521
|
+
"platformState": platform_state,
|
|
522
|
+
"reason": "ready" if ready else "stale_pid" if not pid else "gateway_not_running" if state != "running" else "platform_status_stale" if not writer_matches else "platform_not_connected",
|
|
523
|
+
}
|
|
524
|
+
except Exception:
|
|
525
|
+
# Older Hermes versions may not expose the helpers. Never return raw
|
|
526
|
+
# runtime files or exceptions: they can contain provider/credential data.
|
|
527
|
+
return unavailable
|
|
528
|
+
|
|
529
|
+
|
|
483
530
|
def _plugin_version() -> str:
|
|
484
531
|
"""Read the separately installed plugin version without package-manager state."""
|
|
485
532
|
try:
|