@securecoms/agent-bridge 0.26.0 → 0.26.1
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/connectors/hermes/adapter.py +24 -0
- package/connectors/hermes/tests/test_adapter_wiring.py +29 -0
- package/connectors/openclaw/openclaw-entry.inbound.test.ts +16 -0
- package/connectors/openclaw/openclaw-entry.js +23 -0
- package/dist/bin.js +300 -72
- package/dist/bridge-agent.d.ts +89 -32
- package/dist/config.d.ts +8 -0
- package/dist/index.js +300 -72
- package/dist/version.d.ts +1 -1
- package/dist/voice/defer-intent.d.ts +22 -0
- package/dist/voice/fast-prompt.d.ts +14 -0
- package/dist/voice/prompt-compiler.d.ts +8 -0
- package/package.json +1 -1
|
@@ -80,6 +80,27 @@ _PLUGIN_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
|
80
80
|
_BINDING_PATH = os.path.join(_PLUGIN_DIR, "securecoms.binding.json")
|
|
81
81
|
_CONNECTIONS_DIR = os.path.expanduser("~/.securecoms/connections")
|
|
82
82
|
|
|
83
|
+
# Voice calls (owner report 2026-10-02): a turn the voice front desk handed to
|
|
84
|
+
# Hermes carries the daemon's compiled-in spoken-reply guidance. It rides as
|
|
85
|
+
# the event's per-message ephemeral system prompt (MessageEvent.channel_prompt)
|
|
86
|
+
# so the answer is fit to be read aloud. Byte-equal to
|
|
87
|
+
# @securecoms/agent-runtime's VOICE_TURN_PREFIX.
|
|
88
|
+
VOICE_TURN_PREFIX = (
|
|
89
|
+
"This message was spoken on a live voice call and your reply will be read aloud: "
|
|
90
|
+
"plain spoken sentences, no markdown, lists, links or code. "
|
|
91
|
+
"Use your tools as usual when the question needs them. "
|
|
92
|
+
)
|
|
93
|
+
_VOICE_GUIDANCE_MAX_CHARS = 1000
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def voice_prompt_of(frame: Dict[str, Any]) -> Optional[str]:
|
|
97
|
+
"""The voice system prompt for a `voice: true` frame, else None."""
|
|
98
|
+
guidance = frame.get("guidance")
|
|
99
|
+
if frame.get("voice") is not True or not isinstance(guidance, str):
|
|
100
|
+
return None
|
|
101
|
+
g = guidance.strip()[:_VOICE_GUIDANCE_MAX_CHARS]
|
|
102
|
+
return VOICE_TURN_PREFIX + g if g else None
|
|
103
|
+
|
|
83
104
|
|
|
84
105
|
def _error_class(error: object) -> str:
|
|
85
106
|
"""Return a bounded class name without reading exception prose."""
|
|
@@ -417,6 +438,9 @@ class SecureComsAdapter(BasePlatformAdapter):
|
|
|
417
438
|
message_id=str(frame.get("message_id") or int(time.time() * 1000)),
|
|
418
439
|
timestamp=datetime.now(),
|
|
419
440
|
)
|
|
441
|
+
voice_prompt = voice_prompt_of(frame)
|
|
442
|
+
if voice_prompt is not None:
|
|
443
|
+
event.channel_prompt = voice_prompt
|
|
420
444
|
await self.handle_message(event)
|
|
421
445
|
|
|
422
446
|
# ── Outbound ──────────────────────────────────────────────────────────
|
|
@@ -54,6 +54,7 @@ def _install_gateway_stub():
|
|
|
54
54
|
source: Any = None
|
|
55
55
|
message_id: Optional[str] = None
|
|
56
56
|
timestamp: Any = field(default_factory=datetime.now)
|
|
57
|
+
channel_prompt: Optional[str] = None
|
|
57
58
|
|
|
58
59
|
@dataclass
|
|
59
60
|
class SendResult:
|
|
@@ -324,6 +325,34 @@ class TestEchoGuardWiring:
|
|
|
324
325
|
assert ws.sent == [{"type": "ack", "message_id": "m2"}]
|
|
325
326
|
|
|
326
327
|
|
|
328
|
+
class TestVoiceGuidance:
|
|
329
|
+
@pytest.mark.asyncio
|
|
330
|
+
async def test_voice_turn_carries_guidance_as_channel_prompt_text_turn_none(self, monkeypatch):
|
|
331
|
+
a, _ = make_adapter(monkeypatch)
|
|
332
|
+
a._bound_identity = "A"
|
|
333
|
+
handled = []
|
|
334
|
+
|
|
335
|
+
async def handler(event):
|
|
336
|
+
handled.append(event)
|
|
337
|
+
|
|
338
|
+
a.set_message_handler(handler)
|
|
339
|
+
frames = [
|
|
340
|
+
{"type": "message", "sender_id": "B", "channel_id": "c", "text": "what's the weather?",
|
|
341
|
+
"message_id": "v1", "voice": True, "guidance": "One short spoken sentence."},
|
|
342
|
+
{"type": "message", "sender_id": "B", "channel_id": "c", "text": "hi",
|
|
343
|
+
"message_id": "t1", "guidance": "ignored without voice"},
|
|
344
|
+
]
|
|
345
|
+
ws = FakeWS([_FakeWSMsg(aiohttp.WSMsgType.TEXT, json.dumps(f)) for f in frames]
|
|
346
|
+
+ [_FakeWSMsg(aiohttp.WSMsgType.CLOSED)])
|
|
347
|
+
a._ws = ws
|
|
348
|
+
|
|
349
|
+
await a._pump()
|
|
350
|
+
|
|
351
|
+
assert [e.text for e in handled] == ["what's the weather?", "hi"]
|
|
352
|
+
assert handled[0].channel_prompt == adapter.VOICE_TURN_PREFIX + "One short spoken sentence."
|
|
353
|
+
assert handled[1].channel_prompt is None
|
|
354
|
+
|
|
355
|
+
|
|
327
356
|
# ---------------------------------------------------------------------------
|
|
328
357
|
# (2) send() success fires the status manager's on_reply_posted
|
|
329
358
|
# ---------------------------------------------------------------------------
|
|
@@ -255,6 +255,22 @@ describe("openclaw-entry — inbound sender, session, authority, dedup (P0 Task
|
|
|
255
255
|
expect(rt.recorded.map((r) => r.sessionKey)).toEqual([a.SessionKey, b.SessionKey]);
|
|
256
256
|
});
|
|
257
257
|
|
|
258
|
+
it("a voice turn carries the daemon's spoken-reply guidance as GroupSystemPrompt; a text turn carries none", async () => {
|
|
259
|
+
const rt = makeRuntime();
|
|
260
|
+
const { instances } = await start(rt.runtime);
|
|
261
|
+
const ws = instances[0];
|
|
262
|
+
|
|
263
|
+
deliver(ws, { channel_id: "chan-V", message_id: "m-V", sender_id: OWNER_ID, text: "what's the weather?", voice: true, guidance: "One short spoken sentence.", authorization: auth("owner_dm") });
|
|
264
|
+
deliver(ws, { channel_id: "chan-T", message_id: "m-T", sender_id: OWNER_ID, text: "hi", guidance: "ignored without voice", authorization: auth("owner_dm") });
|
|
265
|
+
await waitFor(() => acksOf(ws).length === 2);
|
|
266
|
+
|
|
267
|
+
const { VOICE_TURN_PREFIX } = await import("./openclaw-entry.js");
|
|
268
|
+
const [v, t] = rt.contexts;
|
|
269
|
+
expect(v.GroupSystemPrompt).toBe(`${VOICE_TURN_PREFIX}One short spoken sentence.`);
|
|
270
|
+
expect(v.RawBody).toBe("what's the weather?");
|
|
271
|
+
expect(t.GroupSystemPrompt).toBeUndefined();
|
|
272
|
+
});
|
|
273
|
+
|
|
258
274
|
it("two DMs from different humans do not share a session", async () => {
|
|
259
275
|
const rt = makeRuntime();
|
|
260
276
|
const { instances } = await start(rt.runtime);
|
|
@@ -85,6 +85,25 @@ const _channels = new Map();
|
|
|
85
85
|
// ---------------------------------------------------------------------------
|
|
86
86
|
|
|
87
87
|
export const MAX_INBOUND_ATTEMPTS = 3;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Voice calls (owner report 2026-10-02): prefixes the daemon's compiled-in
|
|
91
|
+
* spoken-reply guidance on a live voice turn, so a turn the voice front desk
|
|
92
|
+
* handed to OpenClaw is answered in a form fit to be read aloud. Byte-equal
|
|
93
|
+
* to @securecoms/agent-runtime's VOICE_TURN_PREFIX.
|
|
94
|
+
*/
|
|
95
|
+
export const VOICE_TURN_PREFIX =
|
|
96
|
+
"This message was spoken on a live voice call and your reply will be read aloud: plain spoken sentences, no markdown, lists, links or code. Use your tools as usual when the question needs them. ";
|
|
97
|
+
|
|
98
|
+
/** Cap on the daemon-supplied voice guidance carried into the prompt. */
|
|
99
|
+
const VOICE_GUIDANCE_MAX_CHARS = 1000;
|
|
100
|
+
|
|
101
|
+
/** The guidance of a `voice: true` frame (trimmed, capped), else undefined. */
|
|
102
|
+
export function voiceGuidanceOf(frame) {
|
|
103
|
+
if (frame?.voice !== true || typeof frame.guidance !== "string") return undefined;
|
|
104
|
+
const g = frame.guidance.trim().slice(0, VOICE_GUIDANCE_MAX_CHARS);
|
|
105
|
+
return g || undefined;
|
|
106
|
+
}
|
|
88
107
|
const SEEN_MAX = 5000;
|
|
89
108
|
|
|
90
109
|
/** Thrown by the inbound handler when register() has not injected a runtime. */
|
|
@@ -519,6 +538,7 @@ export class LoopbackChannel {
|
|
|
519
538
|
senderName: frame.sender_name,
|
|
520
539
|
authorization: frame.authorization,
|
|
521
540
|
timestamp: frame.ts,
|
|
541
|
+
voiceGuidance: voiceGuidanceOf(frame),
|
|
522
542
|
});
|
|
523
543
|
this._failures.delete(messageId);
|
|
524
544
|
this._failStreak = 0;
|
|
@@ -750,6 +770,9 @@ async function _handleInbound({ text, metadata, channel, account, cfg }) {
|
|
|
750
770
|
// authorization block (isOwnerAuthorization). MLS membership proves a
|
|
751
771
|
// sender is in the room, not that they own this agent.
|
|
752
772
|
CommandAuthorized: commandAuthorized,
|
|
773
|
+
// Voice turn: OpenClaw folds GroupSystemPrompt into this turn's extra
|
|
774
|
+
// system prompt (get-reply's extraSystemPromptParts) for any chat type.
|
|
775
|
+
...(metadata.voiceGuidance ? { GroupSystemPrompt: `${VOICE_TURN_PREFIX}${metadata.voiceGuidance}` } : {}),
|
|
753
776
|
});
|
|
754
777
|
|
|
755
778
|
await core.channel.session.recordInboundSession({
|