@securecoms/agent-bridge 0.26.3 → 0.26.4

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/dist/bin.js CHANGED
@@ -28512,11 +28512,26 @@ function clampContent(content, max = MAX_MESSAGE_CHARS) {
28512
28512
  const marker = "\u2026";
28513
28513
  return content.slice(0, Math.max(0, max - marker.length)) + marker;
28514
28514
  }
28515
+ var FAST_DEFER_RULE = `Whenever you cannot answer completely from what is shown here, never say you can't, that you lack a tool, access or real-time data: reply with one short sentence like "Let me check that." and end with the exact token [[DEFER]] so the full agent answers next.`;
28516
+ var REFUSAL_PATTERNS = [
28517
+ // "I can't / cannot / am unable to / am not able to <check|access|...>", optional adverb.
28518
+ /\b(?:can't|cannot|can\s+not|unable\s+to|not\s+able\s+to)\s+(?:\w+ly\s+)?(?:check|access|look|see|browse|search|get|provide|give\s+you|find|retrieve|fetch|pull|know|help\s+with\s+(?:that|this)|do\s+that)\b/,
28519
+ // "I don't / do not / doesn't have (a|any|the)? <tool|access|ability|real-time|live ...>".
28520
+ /\b(?:don't|do\s+not|doesn't|does\s+not|lack)\s+(?:have\s+)?(?:a\s+|any\s+|the\s+)?(?:tool|tools|access|ability|capability|real[-\s]?time|live|current|internet|web|way\s+to)\b/,
28521
+ /\bno\s+(?:access|tools?|internet|web\s+access|real[-\s]?time|live\s+data|way\s+to\s+(?:check|access|know|see))\b/,
28522
+ /\bas\s+an\s+(?:ai|a\.i\.|language\s+model)\b/,
28523
+ /\bbeyond\s+my\s+(?:capabilities|abilities|capability)\b/,
28524
+ /\b(?:knowledge\s+cutoff|training\s+data)\b/
28525
+ ];
28526
+ function isRefusal(reply) {
28527
+ const t = reply.toLowerCase().replace(/[‘’]/g, "'");
28528
+ return REFUSAL_PATTERNS.some((re) => re.test(t));
28529
+ }
28515
28530
  function buildFastMessages(args2) {
28516
28531
  const cap = args2.charCap ?? FAST_RECENT_CHARS_CAP;
28517
28532
  const sp = systemPrompt(args2.persona, args2.settings, args2.agentName);
28518
28533
  const head = sp.slice(0, sp.length - CONTINUITY_CLAUSE.length).trimEnd();
28519
- const system = `${head} ${args2.guidance.trim()} ${CONTINUITY_CLAUSE}`;
28534
+ const system = `${head} ${args2.guidance.trim()} ${FAST_DEFER_RULE} ${CONTINUITY_CLAUSE}`;
28520
28535
  const recent = args2.memory.recent.map((t) => ({
28521
28536
  role: t.role === "agent" ? "assistant" : "user",
28522
28537
  content: t.text
@@ -29704,6 +29719,8 @@ var BridgeAgent = class _BridgeAgent {
29704
29719
  fastPathDeferred = 0;
29705
29720
  /** Of those, `full`-mode turns the deterministic pre-check deferred without calling the fast model. */
29706
29721
  fastPathIntentDeferred = 0;
29722
+ /** Of those, fast-model replies that voiced inability ("I can't check...") — never spoken; the ack is spoken instead. */
29723
+ fastPathRefusalDeferred = 0;
29707
29724
  /**
29708
29725
  * Task 8: count of live AUTHENTICATED /v1/ws sessions, so a later `status`
29709
29726
  * verb can tell "bridge process up" (the app's green dot, which only
@@ -31981,11 +31998,15 @@ var BridgeAgent = class _BridgeAgent {
31981
31998
  });
31982
31999
  const maxSentences = this.voiceSettings?.brevity === "one" ? 1 : 2;
31983
32000
  const raw = await this.latency.time("voice.fast_llm", () => fastLlm.complete(messages));
31984
- const { reply, defer } = splitFastReply(raw, 200, maxSentences);
31985
- if (!reply) return "failed";
32001
+ const split3 = splitFastReply(raw, 200, maxSentences);
32002
+ if (!split3.reply) return "failed";
32003
+ const refusal = !isGreeting && isRefusal(split3.reply);
32004
+ const defer = split3.defer || refusal;
32005
+ const reply = refusal ? deferAck(m.messageId) : split3.reply;
31986
32006
  await this.sendSpoken(m.channelId, reply, fastReplyId(this.replyIdKey, this.cfg.identityId, m.channelId, m.messageId));
31987
32007
  this.fastPathSent++;
31988
32008
  if (defer) this.fastPathDeferred++;
32009
+ if (refusal) this.fastPathRefusalDeferred++;
31989
32010
  return defer ? "deferred" : "answered";
31990
32011
  } catch (e) {
31991
32012
  this.fastPathFailed++;
@@ -33937,6 +33958,7 @@ var BridgeAgent = class _BridgeAgent {
33937
33958
  fast_path_skipped: this.fastPathSkipped,
33938
33959
  fast_path_deferred: this.fastPathDeferred,
33939
33960
  fast_path_intent_deferred: this.fastPathIntentDeferred,
33961
+ fast_path_refusal_deferred: this.fastPathRefusalDeferred,
33940
33962
  fast_path_mode: this.cfg.voiceFastPathMode ?? "full",
33941
33963
  voice_settings_version: this.voiceSettingsVersion,
33942
33964
  voice_settings_source: this.voiceSettingsSource,
@@ -800,6 +800,8 @@ export declare class BridgeAgent {
800
800
  private fastPathDeferred;
801
801
  /** Of those, `full`-mode turns the deterministic pre-check deferred without calling the fast model. */
802
802
  private fastPathIntentDeferred;
803
+ /** Of those, fast-model replies that voiced inability ("I can't check...") — never spoken; the ack is spoken instead. */
804
+ private fastPathRefusalDeferred;
803
805
  /**
804
806
  * Task 8: count of live AUTHENTICATED /v1/ws sessions, so a later `status`
805
807
  * verb can tell "bridge process up" (the app's green dot, which only
@@ -2344,6 +2346,8 @@ export declare class BridgeAgent {
2344
2346
  fast_path_deferred: number;
2345
2347
  /** `full`-mode turns deferred by the deterministic pre-check (no fast model call). */
2346
2348
  fast_path_intent_deferred: number;
2349
+ /** Fast-model replies that voiced inability/lack of tools: never spoken, deferred behind the canned ack instead. */
2350
+ fast_path_refusal_deferred: number;
2347
2351
  /** Credential `voice_fast_path_mode`: "full" (default), "ack_only" or "off". */
2348
2352
  fast_path_mode: "full" | "ack_only" | "off";
2349
2353
  /** Plan 2026-09-06 (agent voice settings): version of the loaded backend
package/dist/index.js CHANGED
@@ -28288,11 +28288,26 @@ function clampContent(content, max = MAX_MESSAGE_CHARS) {
28288
28288
  const marker = "\u2026";
28289
28289
  return content.slice(0, Math.max(0, max - marker.length)) + marker;
28290
28290
  }
28291
+ var FAST_DEFER_RULE = `Whenever you cannot answer completely from what is shown here, never say you can't, that you lack a tool, access or real-time data: reply with one short sentence like "Let me check that." and end with the exact token [[DEFER]] so the full agent answers next.`;
28292
+ var REFUSAL_PATTERNS = [
28293
+ // "I can't / cannot / am unable to / am not able to <check|access|...>", optional adverb.
28294
+ /\b(?:can't|cannot|can\s+not|unable\s+to|not\s+able\s+to)\s+(?:\w+ly\s+)?(?:check|access|look|see|browse|search|get|provide|give\s+you|find|retrieve|fetch|pull|know|help\s+with\s+(?:that|this)|do\s+that)\b/,
28295
+ // "I don't / do not / doesn't have (a|any|the)? <tool|access|ability|real-time|live ...>".
28296
+ /\b(?:don't|do\s+not|doesn't|does\s+not|lack)\s+(?:have\s+)?(?:a\s+|any\s+|the\s+)?(?:tool|tools|access|ability|capability|real[-\s]?time|live|current|internet|web|way\s+to)\b/,
28297
+ /\bno\s+(?:access|tools?|internet|web\s+access|real[-\s]?time|live\s+data|way\s+to\s+(?:check|access|know|see))\b/,
28298
+ /\bas\s+an\s+(?:ai|a\.i\.|language\s+model)\b/,
28299
+ /\bbeyond\s+my\s+(?:capabilities|abilities|capability)\b/,
28300
+ /\b(?:knowledge\s+cutoff|training\s+data)\b/
28301
+ ];
28302
+ function isRefusal(reply) {
28303
+ const t = reply.toLowerCase().replace(/[‘’]/g, "'");
28304
+ return REFUSAL_PATTERNS.some((re) => re.test(t));
28305
+ }
28291
28306
  function buildFastMessages(args2) {
28292
28307
  const cap = args2.charCap ?? FAST_RECENT_CHARS_CAP;
28293
28308
  const sp = systemPrompt(args2.persona, args2.settings, args2.agentName);
28294
28309
  const head = sp.slice(0, sp.length - CONTINUITY_CLAUSE.length).trimEnd();
28295
- const system = `${head} ${args2.guidance.trim()} ${CONTINUITY_CLAUSE}`;
28310
+ const system = `${head} ${args2.guidance.trim()} ${FAST_DEFER_RULE} ${CONTINUITY_CLAUSE}`;
28296
28311
  const recent = args2.memory.recent.map((t) => ({
28297
28312
  role: t.role === "agent" ? "assistant" : "user",
28298
28313
  content: t.text
@@ -29552,6 +29567,8 @@ var BridgeAgent = class _BridgeAgent {
29552
29567
  fastPathDeferred = 0;
29553
29568
  /** Of those, `full`-mode turns the deterministic pre-check deferred without calling the fast model. */
29554
29569
  fastPathIntentDeferred = 0;
29570
+ /** Of those, fast-model replies that voiced inability ("I can't check...") — never spoken; the ack is spoken instead. */
29571
+ fastPathRefusalDeferred = 0;
29555
29572
  /**
29556
29573
  * Task 8: count of live AUTHENTICATED /v1/ws sessions, so a later `status`
29557
29574
  * verb can tell "bridge process up" (the app's green dot, which only
@@ -31829,11 +31846,15 @@ var BridgeAgent = class _BridgeAgent {
31829
31846
  });
31830
31847
  const maxSentences = this.voiceSettings?.brevity === "one" ? 1 : 2;
31831
31848
  const raw = await this.latency.time("voice.fast_llm", () => fastLlm.complete(messages));
31832
- const { reply, defer } = splitFastReply(raw, 200, maxSentences);
31833
- if (!reply) return "failed";
31849
+ const split3 = splitFastReply(raw, 200, maxSentences);
31850
+ if (!split3.reply) return "failed";
31851
+ const refusal = !isGreeting && isRefusal(split3.reply);
31852
+ const defer = split3.defer || refusal;
31853
+ const reply = refusal ? deferAck(m.messageId) : split3.reply;
31834
31854
  await this.sendSpoken(m.channelId, reply, fastReplyId(this.replyIdKey, this.cfg.identityId, m.channelId, m.messageId));
31835
31855
  this.fastPathSent++;
31836
31856
  if (defer) this.fastPathDeferred++;
31857
+ if (refusal) this.fastPathRefusalDeferred++;
31837
31858
  return defer ? "deferred" : "answered";
31838
31859
  } catch (e) {
31839
31860
  this.fastPathFailed++;
@@ -33785,6 +33806,7 @@ var BridgeAgent = class _BridgeAgent {
33785
33806
  fast_path_skipped: this.fastPathSkipped,
33786
33807
  fast_path_deferred: this.fastPathDeferred,
33787
33808
  fast_path_intent_deferred: this.fastPathIntentDeferred,
33809
+ fast_path_refusal_deferred: this.fastPathRefusalDeferred,
33788
33810
  fast_path_mode: this.cfg.voiceFastPathMode ?? "full",
33789
33811
  voice_settings_version: this.voiceSettingsVersion,
33790
33812
  voice_settings_source: this.voiceSettingsSource,
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const BRIDGE_VERSION = "0.26.3";
1
+ export declare const BRIDGE_VERSION = "0.26.4";
@@ -29,6 +29,15 @@ export declare const MAX_MESSAGE_CHARS = 8000;
29
29
  export declare const FAST_PATH_MEMORY_TURNS = 10;
30
30
  export declare const FAST_RECENT_CHARS_CAP = 1500;
31
31
  export declare const FAST_FACTS_CHARS_CAP = 2000;
32
+ /**
33
+ * Live bug 2026-10-02 (bridge 0.26.3): asked for Sydney's weather, the fast
34
+ * model said "I can't check the weather right now since I don't have a
35
+ * tool..." with no [[DEFER]], so the runtime (which has web_search) never got
36
+ * the turn. Placed last before the continuity clause so it is the final rule
37
+ * the model reads. Compiled in, never interpolated with peer data.
38
+ */
39
+ export declare const FAST_DEFER_RULE: string;
40
+ export declare function isRefusal(reply: string): boolean;
32
41
  export declare function buildFastMessages(args: {
33
42
  persona: VoicePersona;
34
43
  guidance: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@securecoms/agent-bridge",
3
- "version": "0.26.3",
3
+ "version": "0.26.4",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=22.5.0"