Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions extensions/connector-hermes/plugin/cotal/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,9 @@ async def send(
# The shared chat names no peer: the reply goes to where the answered message came from.
# Progress and notices name no message and go to the origin of the latest turn.
target = self._origins.get(answered) if answered else None
if target is None and self._running and "channel" in self._running and not self._channel_replies:
# An unanchored send in a channel turn (progress, a notice) falls back to the channel.
return SendResult(success=True, message_id=uuid.uuid4().hex)
target = target or self._running
if target is None:
logger.warning("cotal: a send to the shared chat names no delivered message (%r) and no turn has run; it is not sent", reply_to)
Expand Down
5 changes: 5 additions & 0 deletions extensions/connector-hermes/smoke/channel-replies.probe.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,11 @@ async def main() -> None:
# So does a channel send anchored to a DM, not a channel message.
await adapter.send("channel:general", "anchored to a DM", reply_to="m-dm")
out["sent"] = adapter._client.replies
# A send in the channel turn that names no message (progress, a turn-error notice).
adapter._client.replies = []
await adapter._inject(dict(CH, id="m-ch2"))
await adapter.send(adapter.handled[-1].source["chat_id"], "unanchored")
out["unanchored_sent"] = adapter._client.replies
print("RESULT " + json.dumps(out))


Expand Down
18 changes: 16 additions & 2 deletions extensions/connector-hermes/smoke/channel-replies.smoke.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ function hard(condition: unknown, name: string): asserts condition {
throw new Error(name);
}

const EXPECTED_CELLS = 17;
const EXPECTED_CELLS = 20;
if (process.platform === "win32") skipSuite("the Hermes connector is Unix-only");

const pkgDir = fileURLToPath(new URL("..", import.meta.url));
Expand All @@ -54,7 +54,13 @@ if (!python) skipSuite("Python is unavailable for the Hermes adapter probe");
const pythonBin = python;

type Sent = { target: { peerId?: string; channel?: string }; text: string };
type ProbeResult = { channel_note: boolean; dm_note: boolean; channel_reply_ok: boolean; sent: Sent[] };
type ProbeResult = {
channel_note: boolean;
dm_note: boolean;
channel_reply_ok: boolean;
sent: Sent[];
unanchored_sent: Sent[];
};

function run(mode: "subject" | "control" | "shared"): ProbeResult {
const res = spawnSync(pythonBin, [probe, `${pkgDir}plugin`, mode], { encoding: "utf8", timeout: 60_000 });
Expand All @@ -77,6 +83,9 @@ for (const mode of ["subject", "shared"] as const) {
assert.equal(r.channel_reply_ok, true, `${mode}: a dropped channel reply reports success, so the gateway does not retry`);
assert.equal(r.channel_note, true, `${mode}: a channel message tells the model its reply is not posted`);
assert.equal(r.dm_note, false, `${mode}: a DM carries no channel note`);
// Per-peer, an unanchored send names its chat like an explicit send (cron), so it still posts.
const unanchored = mode === "shared" ? [] : [{ target: { channel: "ops" }, text: "unanchored" }];
assert.deepEqual(r.unanchored_sent, unanchored, `${mode}: an unanchored send follows the chat it names`);
}

const control = run("control");
Expand All @@ -91,6 +100,11 @@ assert.deepEqual(
);
assert.equal(control.channel_note, false, "control: by default a channel message carries no note");
assert.equal(control.dm_note, false, "control: a DM carries no note");
assert.deepEqual(
control.unanchored_sent,
[{ target: { channel: "ops" }, text: "unanchored" }],
"control: by default an unanchored send in a channel turn is posted",
);

console.log("hermes channel replies: channel posts are tool-only when channel_replies is false");
if (cells !== EXPECTED_CELLS) {
Expand Down
Loading