@inline-chat/hermes-agent-adapter 0.0.6 → 0.0.8-alpha.0
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 +24 -7
- package/dist/install.js +142 -56
- package/package.json +8 -4
- package/plugin/inline/adapter.py +147 -1
- package/plugin/inline/cli.py +295 -24
- package/plugin/inline/plugin.yaml +1 -1
- package/plugin/inline/sidecar/index.mjs +24614 -10512
- package/plugin/inline/tools.py +47 -10
package/README.md
CHANGED
|
@@ -81,7 +81,7 @@ Set up the Inline Hermes Agent adapter on this machine.
|
|
|
81
81
|
Constraints:
|
|
82
82
|
- Do not read, print, or edit .env files.
|
|
83
83
|
- Do not print Inline tokens or other secrets.
|
|
84
|
-
- Use an Inline token from INLINE_TOKEN or INLINE_BOT_TOKEN;
|
|
84
|
+
- Use an Inline token from INLINE_TOKEN or INLINE_BOT_TOKEN when already available; otherwise use Hermes's guided setup without exposing the token.
|
|
85
85
|
|
|
86
86
|
Tasks:
|
|
87
87
|
1. Verify Node.js is version 20 or newer and Hermes Agent is installed.
|
|
@@ -110,6 +110,23 @@ hermes inline status
|
|
|
110
110
|
inline-hermes --version
|
|
111
111
|
```
|
|
112
112
|
|
|
113
|
+
The Inline CLI can drive Hermes setup without putting the bot token in argv:
|
|
114
|
+
|
|
115
|
+
```sh
|
|
116
|
+
inline agents setup --target hermes --non-interactive --json
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
For other orchestrators, the plugin also exposes a token-free machine contract.
|
|
120
|
+
The token is accepted only on stdin and is saved through Hermes's credential
|
|
121
|
+
helper:
|
|
122
|
+
|
|
123
|
+
```sh
|
|
124
|
+
printf '%s\n' "$INLINE_BOT_TOKEN" | hermes inline setup \
|
|
125
|
+
--non-interactive --token-stdin --owner-user-id 123 \
|
|
126
|
+
--access owner --json
|
|
127
|
+
hermes inline status --json --probe
|
|
128
|
+
```
|
|
129
|
+
|
|
113
130
|
`doctor` verifies required plugin files, the Node executable used for the
|
|
114
131
|
sidecar, the installed sidecar bundle hash, and whether Hermes config enables
|
|
115
132
|
the external plugin with `plugins.enabled: [inline-platform]`. It detects
|
|
@@ -129,7 +146,7 @@ uv run ./hermes plugins list --plain --no-bundled
|
|
|
129
146
|
Expected local output includes:
|
|
130
147
|
|
|
131
148
|
```text
|
|
132
|
-
enabled user 0.0.
|
|
149
|
+
enabled user 0.0.8-alpha.0 inline-platform
|
|
133
150
|
```
|
|
134
151
|
|
|
135
152
|
## Update Or Reinstall
|
|
@@ -152,7 +169,7 @@ mismatch, rerun the same command after rebuilding or upgrading the package.
|
|
|
152
169
|
|
|
153
170
|
- Hermes Agent: requires the external user plugin registry and native platform
|
|
154
171
|
plugin loader available in Hermes Agent `0.17.x`. This package was validated
|
|
155
|
-
against Hermes Agent `0.
|
|
172
|
+
against Hermes Agent `0.20.0` from source commit `3c27eb6`.
|
|
156
173
|
- Node.js: `>=20` is required for the bundled sidecar. Hermes-managed Node 22,
|
|
157
174
|
system Node, or an explicit `INLINE_NODE_BIN` path all work.
|
|
158
175
|
- Inline transport: the sidecar uses `@inline-chat/realtime-sdk@0.0.14` and is
|
|
@@ -174,10 +191,10 @@ Before publishing, run:
|
|
|
174
191
|
bun run release:preflight
|
|
175
192
|
```
|
|
176
193
|
|
|
177
|
-
This
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
194
|
+
This creates an isolated registry-dependency stage, runs the full package
|
|
195
|
+
check, packs one read-only tarball, and runs `npm publish --dry-run` against
|
|
196
|
+
those exact bytes. It prints the artifact path, SHA-256, and file list so the
|
|
197
|
+
trusted-publishing workflow can hash-check and publish the same artifact.
|
|
181
198
|
|
|
182
199
|
Maintainers should also run the manual live-test and publish checklist in
|
|
183
200
|
[`hermes-agent/RELEASE.md`](https://github.com/inline-chat/inline/blob/main/hermes-agent/RELEASE.md).
|
package/dist/install.js
CHANGED
|
@@ -7047,7 +7047,7 @@ var minNodeMajor = 20;
|
|
|
7047
7047
|
async function main(argv = process.argv.slice(2)) {
|
|
7048
7048
|
const opts = parseArgs(argv);
|
|
7049
7049
|
if (opts.command === "help") {
|
|
7050
|
-
printHelp();
|
|
7050
|
+
printHelp(opts.helpTopic);
|
|
7051
7051
|
return 0;
|
|
7052
7052
|
}
|
|
7053
7053
|
const pkg = await readPackageInfo();
|
|
@@ -7064,7 +7064,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
7064
7064
|
} else {
|
|
7065
7065
|
printStatus(status);
|
|
7066
7066
|
}
|
|
7067
|
-
return status.ok ? 0 :
|
|
7067
|
+
return status.ok ? 0 : 1;
|
|
7068
7068
|
}
|
|
7069
7069
|
if (opts.command === "test-send") {
|
|
7070
7070
|
const result2 = await testSend({ source, opts, pkg });
|
|
@@ -7087,8 +7087,15 @@ async function installPlugin(params) {
|
|
|
7087
7087
|
const before = await inspectInstall(params);
|
|
7088
7088
|
if (!before.sourceValid)
|
|
7089
7089
|
return { ...before, action: "install", installed: false };
|
|
7090
|
-
if (params.opts.dryRun)
|
|
7091
|
-
return {
|
|
7090
|
+
if (params.opts.dryRun) {
|
|
7091
|
+
return {
|
|
7092
|
+
...before,
|
|
7093
|
+
ok: before.sourceReady,
|
|
7094
|
+
action: "install",
|
|
7095
|
+
installed: false,
|
|
7096
|
+
issues: before.sourceReady ? [] : before.issues
|
|
7097
|
+
};
|
|
7098
|
+
}
|
|
7092
7099
|
await mkdir(path.dirname(params.target), { recursive: true });
|
|
7093
7100
|
if (before.targetExists) {
|
|
7094
7101
|
if (!params.opts.force) {
|
|
@@ -7119,7 +7126,7 @@ async function installPlugin(params) {
|
|
|
7119
7126
|
return {
|
|
7120
7127
|
...after,
|
|
7121
7128
|
action: "install",
|
|
7122
|
-
installed: after.
|
|
7129
|
+
installed: after.installedReady
|
|
7123
7130
|
};
|
|
7124
7131
|
}
|
|
7125
7132
|
function shouldCopyPluginFile(src) {
|
|
@@ -7143,13 +7150,16 @@ async function inspectInstall(params) {
|
|
|
7143
7150
|
const activation = await inspectActivation(params.opts.hermesHome, params.target);
|
|
7144
7151
|
const issues = [];
|
|
7145
7152
|
const warnings = [];
|
|
7153
|
+
const sidecarMatches = Boolean(targetSidecar?.exists && sourceSidecar.sha256 && targetSidecar.sha256 && sourceSidecar.sha256 === targetSidecar.sha256);
|
|
7154
|
+
const sourceReady = sourceValid && sourceSidecar.exists && node.ok;
|
|
7155
|
+
const installedReady = targetExists && targetValid && sidecarMatches && node.ok;
|
|
7146
7156
|
if (!sourceValid) {
|
|
7147
7157
|
issues.push(`plugin source is incomplete: ${params.source}`);
|
|
7148
7158
|
}
|
|
7149
7159
|
if (targetExists && !targetValid) {
|
|
7150
7160
|
issues.push(`installed plugin is incomplete: ${params.target}`);
|
|
7151
7161
|
}
|
|
7152
|
-
if (params.opts.command === "doctor" && !targetExists) {
|
|
7162
|
+
if ((params.opts.command === "status" || params.opts.command === "doctor") && !targetExists) {
|
|
7153
7163
|
issues.push(`plugin is not installed: ${params.target}`);
|
|
7154
7164
|
}
|
|
7155
7165
|
if (targetSidecar?.exists && sourceSidecar.sha256 && targetSidecar.sha256 && sourceSidecar.sha256 !== targetSidecar.sha256) {
|
|
@@ -7171,7 +7181,7 @@ async function inspectInstall(params) {
|
|
|
7171
7181
|
warnings.push("Installed plugin ownership differs from the Hermes home owner; internal /inline_update may fail until inline-hermes install --force repairs it");
|
|
7172
7182
|
}
|
|
7173
7183
|
return {
|
|
7174
|
-
ok:
|
|
7184
|
+
ok: installedReady && issues.length === 0,
|
|
7175
7185
|
action: "status",
|
|
7176
7186
|
packageName: params.pkg.name,
|
|
7177
7187
|
packageVersion: params.pkg.version,
|
|
@@ -7180,14 +7190,17 @@ async function inspectInstall(params) {
|
|
|
7180
7190
|
source: params.source,
|
|
7181
7191
|
sourceReal,
|
|
7182
7192
|
sourceValid,
|
|
7193
|
+
sourceReady,
|
|
7183
7194
|
target: params.target,
|
|
7184
7195
|
targetReal,
|
|
7185
7196
|
targetExists,
|
|
7186
7197
|
targetValid,
|
|
7198
|
+
installedReady,
|
|
7187
7199
|
targetLinked,
|
|
7188
7200
|
sidecar: {
|
|
7189
7201
|
source: sourceSidecar,
|
|
7190
|
-
target: targetSidecar
|
|
7202
|
+
target: targetSidecar,
|
|
7203
|
+
matchesPackage: sidecarMatches
|
|
7191
7204
|
},
|
|
7192
7205
|
node,
|
|
7193
7206
|
activation,
|
|
@@ -7298,21 +7311,40 @@ async function testSend(params) {
|
|
|
7298
7311
|
}
|
|
7299
7312
|
}
|
|
7300
7313
|
function parseArgs(argv) {
|
|
7301
|
-
|
|
7302
|
-
|
|
7303
|
-
|
|
7304
|
-
|
|
7305
|
-
|
|
7306
|
-
|
|
7307
|
-
|
|
7308
|
-
|
|
7309
|
-
|
|
7310
|
-
|
|
7311
|
-
|
|
7312
|
-
};
|
|
7314
|
+
if (argv[0] === "help") {
|
|
7315
|
+
const helpTopic = argv[1] == null ? undefined : parseRuntimeCommand(argv[1]);
|
|
7316
|
+
if (argv.length > (helpTopic ? 2 : 1))
|
|
7317
|
+
throw new Error(`unknown argument: ${argv[helpTopic ? 2 : 1]}`);
|
|
7318
|
+
return defaultOptions("help", helpTopic);
|
|
7319
|
+
}
|
|
7320
|
+
if (argv[0] === "version") {
|
|
7321
|
+
if (argv.length !== 1)
|
|
7322
|
+
throw new Error(`unknown argument: ${argv[1]}`);
|
|
7323
|
+
return defaultOptions("version");
|
|
7324
|
+
}
|
|
7313
7325
|
const firstArgIsFlag = argv[0]?.startsWith("-") ?? false;
|
|
7314
|
-
|
|
7326
|
+
const selectedCommand = firstArgIsFlag || argv[0] == null ? "install" : parseRuntimeCommand(argv[0]);
|
|
7327
|
+
const opts = defaultOptions(selectedCommand);
|
|
7328
|
+
const startIndex = argv[0] == null || firstArgIsFlag ? 0 : 1;
|
|
7329
|
+
if (argv[startIndex] === "-v" || argv[startIndex] === "--version") {
|
|
7330
|
+
if (startIndex !== 0 || argv.length !== 1)
|
|
7331
|
+
throw new Error("--version is only valid as a top-level flag");
|
|
7332
|
+
opts.command = "version";
|
|
7333
|
+
return opts;
|
|
7334
|
+
}
|
|
7335
|
+
for (let i = startIndex;i < argv.length; i += 1) {
|
|
7315
7336
|
const arg = argv[i];
|
|
7337
|
+
if (arg == null)
|
|
7338
|
+
throw new Error("missing argument");
|
|
7339
|
+
if (arg === "-h" || arg === "--help") {
|
|
7340
|
+
if (argv.length !== i + 1)
|
|
7341
|
+
throw new Error(`${arg} must be the final argument`);
|
|
7342
|
+
opts.command = "help";
|
|
7343
|
+
if (startIndex !== 0)
|
|
7344
|
+
opts.helpTopic = selectedCommand;
|
|
7345
|
+
return opts;
|
|
7346
|
+
}
|
|
7347
|
+
assertOptionAllowed(selectedCommand, arg);
|
|
7316
7348
|
switch (arg) {
|
|
7317
7349
|
case "--hermes-home":
|
|
7318
7350
|
opts.hermesHome = requireValue(argv, ++i, arg);
|
|
@@ -7347,14 +7379,6 @@ function parseArgs(argv) {
|
|
|
7347
7379
|
case "--timeout-ms":
|
|
7348
7380
|
opts.timeoutMs = parsePositiveInt(requireValue(argv, ++i, arg), arg);
|
|
7349
7381
|
break;
|
|
7350
|
-
case "-h":
|
|
7351
|
-
case "--help":
|
|
7352
|
-
opts.command = "help";
|
|
7353
|
-
break;
|
|
7354
|
-
case "-v":
|
|
7355
|
-
case "--version":
|
|
7356
|
-
opts.command = "version";
|
|
7357
|
-
break;
|
|
7358
7382
|
default:
|
|
7359
7383
|
throw new Error(`unknown argument: ${arg}`);
|
|
7360
7384
|
}
|
|
@@ -7363,17 +7387,39 @@ function parseArgs(argv) {
|
|
|
7363
7387
|
validatePluginId(opts.pluginId);
|
|
7364
7388
|
return opts;
|
|
7365
7389
|
}
|
|
7366
|
-
function
|
|
7367
|
-
|
|
7368
|
-
|
|
7369
|
-
|
|
7390
|
+
function defaultOptions(command, helpTopic) {
|
|
7391
|
+
const opts = {
|
|
7392
|
+
command,
|
|
7393
|
+
hermesHome: process.env.HERMES_HOME || path.join(os.homedir(), ".hermes"),
|
|
7394
|
+
pluginId: defaultPluginId,
|
|
7395
|
+
link: false,
|
|
7396
|
+
dryRun: false,
|
|
7397
|
+
force: false,
|
|
7398
|
+
json: false,
|
|
7399
|
+
text: "Inline Hermes test",
|
|
7400
|
+
timeoutMs: 30000,
|
|
7401
|
+
...helpTopic ? { helpTopic } : {}
|
|
7402
|
+
};
|
|
7403
|
+
return opts;
|
|
7404
|
+
}
|
|
7405
|
+
function parseRuntimeCommand(raw) {
|
|
7406
|
+
if (raw === "install" || raw === "status" || raw === "doctor" || raw === "test-send")
|
|
7370
7407
|
return raw;
|
|
7371
|
-
if (raw === "-h" || raw === "--help")
|
|
7372
|
-
return "help";
|
|
7373
|
-
if (raw === "-v" || raw === "--version")
|
|
7374
|
-
return "version";
|
|
7375
7408
|
throw new Error(`unknown command: ${raw}`);
|
|
7376
7409
|
}
|
|
7410
|
+
var commandOptions = {
|
|
7411
|
+
install: new Set(["--hermes-home", "--plugin-id", "--link", "--dry-run", "--force", "--json"]),
|
|
7412
|
+
status: new Set(["--hermes-home", "--plugin-id", "--json"]),
|
|
7413
|
+
doctor: new Set(["--hermes-home", "--plugin-id", "--json"]),
|
|
7414
|
+
"test-send": new Set(["--hermes-home", "--dry-run", "--json", "--to", "--text", "--base-url", "--state-path", "--timeout-ms"])
|
|
7415
|
+
};
|
|
7416
|
+
function assertOptionAllowed(command, option) {
|
|
7417
|
+
if (!option.startsWith("-"))
|
|
7418
|
+
throw new Error(`unexpected positional argument for ${command}: ${option}`);
|
|
7419
|
+
if (!commandOptions[command].has(option)) {
|
|
7420
|
+
throw new Error(`${option} is not valid for inline-hermes ${command}`);
|
|
7421
|
+
}
|
|
7422
|
+
}
|
|
7377
7423
|
function requireValue(argv, index, flag) {
|
|
7378
7424
|
const value = argv[index];
|
|
7379
7425
|
if (value == null || value.startsWith("--")) {
|
|
@@ -7525,20 +7571,20 @@ async function inspectInstallOwnership(hermesHome, target) {
|
|
|
7525
7571
|
try {
|
|
7526
7572
|
const homeInfo = await stat(hermesHome);
|
|
7527
7573
|
const pluginsInfo = await stat(path.dirname(target));
|
|
7528
|
-
return pluginsInfo.uid === homeInfo.uid &&
|
|
7574
|
+
return pluginsInfo.uid === homeInfo.uid && await treeOwnershipMatches(target, homeInfo.uid);
|
|
7529
7575
|
} catch {
|
|
7530
7576
|
return false;
|
|
7531
7577
|
}
|
|
7532
7578
|
}
|
|
7533
|
-
async function treeOwnershipMatches(target, uid
|
|
7579
|
+
async function treeOwnershipMatches(target, uid) {
|
|
7534
7580
|
const info = await lstat(target);
|
|
7535
|
-
if (info.uid !== uid
|
|
7581
|
+
if (info.uid !== uid)
|
|
7536
7582
|
return false;
|
|
7537
7583
|
if (!info.isDirectory())
|
|
7538
7584
|
return true;
|
|
7539
7585
|
const entries = await readdir(target);
|
|
7540
7586
|
for (const entry of entries) {
|
|
7541
|
-
if (!await treeOwnershipMatches(path.join(target, entry), uid
|
|
7587
|
+
if (!await treeOwnershipMatches(path.join(target, entry), uid))
|
|
7542
7588
|
return false;
|
|
7543
7589
|
}
|
|
7544
7590
|
return true;
|
|
@@ -8001,28 +8047,68 @@ function formatSidecarInfo(info) {
|
|
|
8001
8047
|
const digest = info.sha256 ? info.sha256.slice(0, 12) : "unknown";
|
|
8002
8048
|
return `${info.path} (${info.size ?? 0} bytes, sha256 ${digest})`;
|
|
8003
8049
|
}
|
|
8004
|
-
function printHelp() {
|
|
8050
|
+
function printHelp(topic) {
|
|
8051
|
+
if (topic === "install") {
|
|
8052
|
+
console.log(`Usage: inline-hermes install [options]
|
|
8053
|
+
|
|
8054
|
+
Install the packaged Inline plugin into a Hermes home.
|
|
8055
|
+
|
|
8056
|
+
Options:
|
|
8057
|
+
--hermes-home <path> Hermes home directory. Defaults to HERMES_HOME or ~/.hermes.
|
|
8058
|
+
--plugin-id <id> Installed plugin directory name. Defaults to inline.
|
|
8059
|
+
--link Symlink the packaged plugin for local development.
|
|
8060
|
+
--force Replace an existing target plugin directory.
|
|
8061
|
+
--dry-run Validate packaged source and Node without writing files.
|
|
8062
|
+
--json Print machine-readable output.
|
|
8063
|
+
-h, --help Show this command help.
|
|
8064
|
+
`);
|
|
8065
|
+
return;
|
|
8066
|
+
}
|
|
8067
|
+
if (topic === "status" || topic === "doctor") {
|
|
8068
|
+
console.log(`Usage: inline-hermes ${topic} [options]
|
|
8069
|
+
|
|
8070
|
+
${topic === "doctor" ? "Diagnose" : "Inspect"} the installed Inline plugin, sidecar integrity, and Node runtime.
|
|
8071
|
+
|
|
8072
|
+
Options:
|
|
8073
|
+
--hermes-home <path> Hermes home directory. Defaults to HERMES_HOME or ~/.hermes.
|
|
8074
|
+
--plugin-id <id> Installed plugin directory name. Defaults to inline.
|
|
8075
|
+
--json Print machine-readable output.
|
|
8076
|
+
-h, --help Show this command help.
|
|
8077
|
+
`);
|
|
8078
|
+
return;
|
|
8079
|
+
}
|
|
8080
|
+
if (topic === "test-send") {
|
|
8081
|
+
console.log(`Usage: inline-hermes test-send --to chat:<id>|user:<id> [options]
|
|
8082
|
+
|
|
8083
|
+
Probe the packaged sidecar with a bounded Inline send, or validate it with --dry-run.
|
|
8084
|
+
|
|
8085
|
+
Options:
|
|
8086
|
+
--hermes-home <path> Hermes home used for token/config resolution.
|
|
8087
|
+
--to <target> Inline destination, for example chat:123 or user:42.
|
|
8088
|
+
--text <message> Message text. Defaults to "Inline Hermes test".
|
|
8089
|
+
--base-url <url> Inline API base URL. Defaults to https://api.inline.chat.
|
|
8090
|
+
--state-path <path> Inline SDK state file used by the probe sidecar.
|
|
8091
|
+
--timeout-ms <ms> Sidecar readiness timeout. Defaults to 30000.
|
|
8092
|
+
--dry-run Validate inputs without starting the sidecar or sending.
|
|
8093
|
+
--json Print machine-readable output.
|
|
8094
|
+
-h, --help Show this command help.
|
|
8095
|
+
`);
|
|
8096
|
+
return;
|
|
8097
|
+
}
|
|
8005
8098
|
console.log(`inline-hermes
|
|
8006
8099
|
|
|
8007
8100
|
Usage:
|
|
8008
|
-
inline-hermes install [
|
|
8101
|
+
inline-hermes install [options]
|
|
8009
8102
|
inline-hermes status [--hermes-home <path>] [--json]
|
|
8010
8103
|
inline-hermes doctor [--hermes-home <path>] [--json]
|
|
8011
|
-
inline-hermes test-send --to chat:<id>|user:<id> [
|
|
8104
|
+
inline-hermes test-send --to chat:<id>|user:<id> [options]
|
|
8105
|
+
inline-hermes help [install|status|doctor|test-send]
|
|
8012
8106
|
inline-hermes version
|
|
8013
8107
|
|
|
8014
|
-
|
|
8015
|
-
|
|
8016
|
-
--
|
|
8017
|
-
--
|
|
8018
|
-
--dry-run Print what would happen without writing files.
|
|
8019
|
-
--json Print machine-readable output.
|
|
8020
|
-
--to <target> Inline target for test-send, for example chat:123.
|
|
8021
|
-
--text <message> Message for test-send. Defaults to "Inline Hermes test".
|
|
8022
|
-
--base-url <url> Inline API base URL for test-send.
|
|
8023
|
-
--state-path <path> Inline SDK state file for test-send.
|
|
8024
|
-
--timeout-ms <ms> test-send sidecar readiness timeout. Defaults to 30000.
|
|
8025
|
-
-v, --version Print package version.
|
|
8108
|
+
Run \`inline-hermes <command> --help\` for command-scoped options.
|
|
8109
|
+
Top-level options:
|
|
8110
|
+
-h, --help Show this help.
|
|
8111
|
+
-v, --version Print package version.
|
|
8026
8112
|
|
|
8027
8113
|
After install, enable the external Hermes plugin with:
|
|
8028
8114
|
hermes plugins enable ${pluginEnableKey}
|
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.8-alpha.0",
|
|
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",
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"check": "bun run typecheck && bun run lint && bun run test"
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
|
-
"@inline-chat/realtime-sdk": "0.0.
|
|
63
|
+
"@inline-chat/realtime-sdk": "0.0.15-alpha.0",
|
|
64
64
|
"yaml": "2.9.0"
|
|
65
65
|
},
|
|
66
66
|
"devDependencies": {
|
|
@@ -75,8 +75,12 @@
|
|
|
75
75
|
"inlineHermes": {
|
|
76
76
|
"pluginId": "inline",
|
|
77
77
|
"pluginPath": "plugin/inline",
|
|
78
|
+
"install": {
|
|
79
|
+
"npmSpec": "@inline-chat/hermes-agent-adapter"
|
|
80
|
+
},
|
|
81
|
+
"machineSetupProtocol": 1,
|
|
78
82
|
"minHermesVersion": "0.17.0",
|
|
79
|
-
"testedHermesVersion": "0.
|
|
80
|
-
"testedHermesCommit": "
|
|
83
|
+
"testedHermesVersion": "0.20.0",
|
|
84
|
+
"testedHermesCommit": "3c27eb6"
|
|
81
85
|
}
|
|
82
86
|
}
|
package/plugin/inline/adapter.py
CHANGED
|
@@ -69,6 +69,8 @@ _DEFAULT_OBSERVED_CONTEXT_LIMIT = 20
|
|
|
69
69
|
_MAX_OBSERVED_CONTEXT_LIMIT = 100
|
|
70
70
|
_MAX_CONTEXT_HISTORY_LIMIT = 20
|
|
71
71
|
_MAX_CONTEXT_REQUEST_LIMIT = 100
|
|
72
|
+
_JOIN_MENTION_LOOKBACK_SECONDS = 60
|
|
73
|
+
_JOIN_HISTORY_PAGE_LIMIT = 100
|
|
72
74
|
_CONTEXT_MESSAGE_TEXT_LIMIT = 360
|
|
73
75
|
_OBSERVED_CONTEXT_CACHE_MAX_SIZE = 512
|
|
74
76
|
_STATE_DIR = Path.home() / ".hermes" / "inline"
|
|
@@ -855,6 +857,7 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
855
857
|
self._me_id: Optional[str] = None
|
|
856
858
|
self._me_username: Optional[str] = None
|
|
857
859
|
self._seen_messages: Dict[str, float] = {}
|
|
860
|
+
self._seen_message_instances: Dict[str, float] = {}
|
|
858
861
|
self._clarify_choices: "OrderedDict[str, List[str]]" = OrderedDict()
|
|
859
862
|
self._clarify_sessions: "OrderedDict[str, str]" = OrderedDict()
|
|
860
863
|
self._approval_sessions: "OrderedDict[str, str]" = OrderedDict()
|
|
@@ -2048,7 +2051,12 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
2048
2051
|
if self._system_events:
|
|
2049
2052
|
await self._dispatch_message(event, edit=True)
|
|
2050
2053
|
return
|
|
2051
|
-
if kind
|
|
2054
|
+
if kind == "chat.participant.add":
|
|
2055
|
+
if await self._recover_self_join_mentions(event):
|
|
2056
|
+
return
|
|
2057
|
+
await self._dispatch_system_event(event)
|
|
2058
|
+
return
|
|
2059
|
+
if kind in {"message.delete", "message.history.clear", "chat.participant.delete"}:
|
|
2052
2060
|
await self._dispatch_system_event(event)
|
|
2053
2061
|
return
|
|
2054
2062
|
if kind != "message.new":
|
|
@@ -2068,6 +2076,28 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
2068
2076
|
del self._seen_messages[stale]
|
|
2069
2077
|
return False
|
|
2070
2078
|
|
|
2079
|
+
def _is_duplicate_message_instance(self, chat_id: str, msg: Dict[str, Any], event: Dict[str, Any]) -> bool:
|
|
2080
|
+
message_date = str(msg.get("date") or event.get("date") or "").strip()
|
|
2081
|
+
content = json.dumps({
|
|
2082
|
+
"fromId": msg.get("fromId"),
|
|
2083
|
+
"message": msg.get("message"),
|
|
2084
|
+
"entities": msg.get("entities"),
|
|
2085
|
+
"media": msg.get("media"),
|
|
2086
|
+
}, sort_keys=True, default=str, separators=(",", ":"))
|
|
2087
|
+
fingerprint = hashlib.sha256(content.encode("utf-8")).hexdigest()[:16]
|
|
2088
|
+
key = f"{chat_id}:{str(msg.get('id') or '')}:{message_date}:{fingerprint}"
|
|
2089
|
+
now = time.time()
|
|
2090
|
+
old = self._seen_message_instances.get(key)
|
|
2091
|
+
if old is not None and now - old < _DEDUP_WINDOW_SECONDS:
|
|
2092
|
+
return True
|
|
2093
|
+
if key in self._seen_message_instances:
|
|
2094
|
+
del self._seen_message_instances[key]
|
|
2095
|
+
self._seen_message_instances[key] = now
|
|
2096
|
+
if len(self._seen_message_instances) > _DEDUP_MAX_SIZE:
|
|
2097
|
+
for stale in list(self._seen_message_instances.keys())[: len(self._seen_message_instances) - _DEDUP_MAX_SIZE]:
|
|
2098
|
+
del self._seen_message_instances[stale]
|
|
2099
|
+
return False
|
|
2100
|
+
|
|
2071
2101
|
async def _dispatch_message(self, event: Dict[str, Any], *, edit: bool = False) -> None:
|
|
2072
2102
|
msg = event.get("message") or {}
|
|
2073
2103
|
msg_id = str(msg.get("id") or "")
|
|
@@ -2089,6 +2119,8 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
2089
2119
|
dedup_key = f"new:{chat_id}:msg:{msg_id}"
|
|
2090
2120
|
if self._is_duplicate(dedup_key):
|
|
2091
2121
|
return
|
|
2122
|
+
if not edit and self._is_duplicate_message_instance(chat_id, msg, event):
|
|
2123
|
+
return
|
|
2092
2124
|
from_id = str(msg.get("fromId") or "")
|
|
2093
2125
|
if msg.get("out") or (self._me_id and from_id == self._me_id):
|
|
2094
2126
|
return
|
|
@@ -2209,6 +2241,21 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
2209
2241
|
parent_message_id = msg_id
|
|
2210
2242
|
|
|
2211
2243
|
channel_prompt, auto_skill = self._resolve_thread_bindings(chat_id, thread_id, parent_chat_id)
|
|
2244
|
+
mentioned_agent_id = self._mentioned_agent_id(msg)
|
|
2245
|
+
if mentioned_agent_id:
|
|
2246
|
+
try:
|
|
2247
|
+
resolved_agent = await self._sidecar_call("/get-agent", {"agentId": mentioned_agent_id})
|
|
2248
|
+
agent = resolved_agent.get("agent") if isinstance(resolved_agent.get("agent"), dict) else {}
|
|
2249
|
+
agent_name = str(agent.get("name") or "").strip()
|
|
2250
|
+
agent_instructions = str(agent.get("instructions") or "").strip()
|
|
2251
|
+
agent_skill = str(agent.get("skillKey") or agent.get("skill_key") or "").strip()
|
|
2252
|
+
if agent_name:
|
|
2253
|
+
specialization = agent_instructions or f'You are a specialized agent named "{agent_name}".'
|
|
2254
|
+
channel_prompt = self._merge_channel_prompt(channel_prompt, specialization)
|
|
2255
|
+
if agent_skill:
|
|
2256
|
+
auto_skill = [agent_skill]
|
|
2257
|
+
except Exception as exc:
|
|
2258
|
+
logger.warning("[inline] failed to resolve mentioned Agent %s: %s", mentioned_agent_id, exc)
|
|
2212
2259
|
entity_text = self._inline_entity_text(msg, str(msg.get("message") or ""))
|
|
2213
2260
|
parent_chat_info: Dict[str, Any] = {}
|
|
2214
2261
|
if parent_chat_id:
|
|
@@ -2298,6 +2345,92 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
2298
2345
|
timestamp=self._timestamp(event.get("date") or msg.get("date")),
|
|
2299
2346
|
))
|
|
2300
2347
|
|
|
2348
|
+
def _message_explicitly_mentions_me(self, msg: Dict[str, Any]) -> bool:
|
|
2349
|
+
if not self._me_id:
|
|
2350
|
+
return False
|
|
2351
|
+
if bool(msg.get("mentioned")):
|
|
2352
|
+
return True
|
|
2353
|
+
for entity in self._message_entities(msg):
|
|
2354
|
+
if self._entity_kind(entity) != "mention":
|
|
2355
|
+
continue
|
|
2356
|
+
payload = self._entity_payload(entity, "mention")
|
|
2357
|
+
if self._entity_id(payload, "userId") == self._me_id:
|
|
2358
|
+
return True
|
|
2359
|
+
username = str(self._me_username or "").strip().lstrip("@")
|
|
2360
|
+
text = str(msg.get("message") or "")
|
|
2361
|
+
if not username or not text:
|
|
2362
|
+
return False
|
|
2363
|
+
return bool(re.search(rf"(^|\s)@{re.escape(username)}(?=$|[\s,.:;!?])", text, re.IGNORECASE))
|
|
2364
|
+
|
|
2365
|
+
async def _recover_self_join_mentions(self, event: Dict[str, Any]) -> bool:
|
|
2366
|
+
participant = event.get("participant") if isinstance(event.get("participant"), dict) else {}
|
|
2367
|
+
if not self._me_id or str(participant.get("userId") or "").strip() != self._me_id:
|
|
2368
|
+
return False
|
|
2369
|
+
|
|
2370
|
+
chat_id = str(event.get("chatId") or "").strip()
|
|
2371
|
+
joined_at = _to_int(participant.get("date") or event.get("date"))
|
|
2372
|
+
if not chat_id or joined_at is None or joined_at <= 0:
|
|
2373
|
+
logger.warning("[inline] cannot recover join mentions without chat and participant date")
|
|
2374
|
+
return True
|
|
2375
|
+
|
|
2376
|
+
cutoff = joined_at - _JOIN_MENTION_LOOKBACK_SECONDS
|
|
2377
|
+
messages: Dict[str, Dict[str, Any]] = {}
|
|
2378
|
+
anchor_id: Optional[str] = None
|
|
2379
|
+
while True:
|
|
2380
|
+
body: Dict[str, Any] = {
|
|
2381
|
+
"target": _target_from_chat_id(chat_id),
|
|
2382
|
+
"limit": _JOIN_HISTORY_PAGE_LIMIT,
|
|
2383
|
+
}
|
|
2384
|
+
if anchor_id:
|
|
2385
|
+
body["anchorId"] = anchor_id
|
|
2386
|
+
try:
|
|
2387
|
+
data = await self._sidecar_call("/history", body)
|
|
2388
|
+
except Exception as exc:
|
|
2389
|
+
logger.warning("[inline] join mention history unavailable for chat %s: %s", chat_id, exc)
|
|
2390
|
+
return True
|
|
2391
|
+
page = (data.get("result") or {}).get("messages") or []
|
|
2392
|
+
page = [message for message in page if isinstance(message, dict)]
|
|
2393
|
+
if not page:
|
|
2394
|
+
break
|
|
2395
|
+
|
|
2396
|
+
oldest: Optional[Dict[str, Any]] = None
|
|
2397
|
+
added = 0
|
|
2398
|
+
for message in page:
|
|
2399
|
+
message_id = str(message.get("id") or "").strip()
|
|
2400
|
+
message_date = _to_int(message.get("date"))
|
|
2401
|
+
if not message_id or message_date is None:
|
|
2402
|
+
continue
|
|
2403
|
+
key = f"{message_id}:{message_date}"
|
|
2404
|
+
if key not in messages:
|
|
2405
|
+
messages[key] = message
|
|
2406
|
+
added += 1
|
|
2407
|
+
if oldest is None or message_date < int(oldest["date"]):
|
|
2408
|
+
oldest = {**message, "date": message_date}
|
|
2409
|
+
if oldest is None or int(oldest["date"]) < cutoff or len(page) < _JOIN_HISTORY_PAGE_LIMIT or added == 0:
|
|
2410
|
+
break
|
|
2411
|
+
anchor_id = str(oldest.get("id") or "").strip() or None
|
|
2412
|
+
if not anchor_id:
|
|
2413
|
+
break
|
|
2414
|
+
|
|
2415
|
+
candidates = []
|
|
2416
|
+
for message in messages.values():
|
|
2417
|
+
message_date = _to_int(message.get("date"))
|
|
2418
|
+
if message_date is None or message_date < cutoff or message_date > joined_at:
|
|
2419
|
+
continue
|
|
2420
|
+
if str(message.get("fromId") or "") == self._me_id:
|
|
2421
|
+
continue
|
|
2422
|
+
if self._message_explicitly_mentions_me(message):
|
|
2423
|
+
candidates.append(message)
|
|
2424
|
+
candidates.sort(key=lambda message: (_to_int(message.get("date")) or 0, str(message.get("id") or "")))
|
|
2425
|
+
|
|
2426
|
+
for message in candidates:
|
|
2427
|
+
await self._dispatch_message({
|
|
2428
|
+
"kind": "message.new",
|
|
2429
|
+
"chatId": chat_id,
|
|
2430
|
+
"message": {**message, "mentioned": True},
|
|
2431
|
+
})
|
|
2432
|
+
return True
|
|
2433
|
+
|
|
2301
2434
|
async def _dispatch_reaction(self, event: Dict[str, Any], *, added: bool) -> None:
|
|
2302
2435
|
reaction = event.get("reaction") if isinstance(event.get("reaction"), dict) else {}
|
|
2303
2436
|
chat_id = str(event.get("chatId") or reaction.get("chatId") or "")
|
|
@@ -2770,6 +2903,19 @@ class InlineAdapter(BasePlatformAdapter):
|
|
|
2770
2903
|
parts.append(f"+{len(entities) - _INLINE_ENTITY_LIMIT} more")
|
|
2771
2904
|
return " | ".join(parts) if parts else None
|
|
2772
2905
|
|
|
2906
|
+
def _mentioned_agent_id(self, msg: Dict[str, Any]) -> Optional[str]:
|
|
2907
|
+
for entity in self._message_entities(msg):
|
|
2908
|
+
if self._entity_kind(entity) != "mention":
|
|
2909
|
+
continue
|
|
2910
|
+
payload = self._entity_payload(entity, "mention")
|
|
2911
|
+
user_id = self._entity_id(payload, "userId")
|
|
2912
|
+
if self._me_id and user_id != self._me_id:
|
|
2913
|
+
continue
|
|
2914
|
+
agent_id = self._entity_id(payload, "agentId")
|
|
2915
|
+
if agent_id:
|
|
2916
|
+
return agent_id
|
|
2917
|
+
return None
|
|
2918
|
+
|
|
2773
2919
|
@staticmethod
|
|
2774
2920
|
def _merge_channel_prompt(*parts: Optional[str]) -> Optional[str]:
|
|
2775
2921
|
merged = [str(part).strip() for part in parts if str(part or "").strip()]
|