@securecoms/agent-bridge 0.26.2 → 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
@@ -7597,6 +7597,15 @@ function systemPrompt(p, settings, agentName) {
7597
7597
  }
7598
7598
 
7599
7599
  // src/service-log.ts
7600
+ var ENUM_VALUE = /^[a-z][a-z_]{0,31}$/;
7601
+ var ServiceLogEnum = class _ServiceLogEnum {
7602
+ constructor(value) {
7603
+ this.value = value;
7604
+ }
7605
+ static of(value, allowed) {
7606
+ return new _ServiceLogEnum(allowed.includes(value) && ENUM_VALUE.test(value) ? value : "invalid");
7607
+ }
7608
+ };
7600
7609
  var TOKEN = /^[a-z][a-z0-9_.-]{0,63}$/;
7601
7610
  var CLASS = /^[A-Za-z_$][A-Za-z0-9_$]{0,63}$/;
7602
7611
  function errorClass(error) {
@@ -7626,6 +7635,7 @@ function formatServiceLog(level3, event, fields = {}, error) {
7626
7635
  if (!TOKEN.test(key)) continue;
7627
7636
  if (typeof value === "boolean") parts.push(`${key}=${value}`);
7628
7637
  else if (typeof value === "number" && Number.isFinite(value)) parts.push(`${key}=${value}`);
7638
+ else if (value instanceof ServiceLogEnum && ENUM_VALUE.test(value.value)) parts.push(`${key}=${value.value}`);
7629
7639
  }
7630
7640
  if (error !== void 0) parts.push(`error_class=${errorClass(error)}`);
7631
7641
  return parts.join(" ");
@@ -28502,11 +28512,26 @@ function clampContent(content, max = MAX_MESSAGE_CHARS) {
28502
28512
  const marker = "\u2026";
28503
28513
  return content.slice(0, Math.max(0, max - marker.length)) + marker;
28504
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
+ }
28505
28530
  function buildFastMessages(args2) {
28506
28531
  const cap = args2.charCap ?? FAST_RECENT_CHARS_CAP;
28507
28532
  const sp = systemPrompt(args2.persona, args2.settings, args2.agentName);
28508
28533
  const head = sp.slice(0, sp.length - CONTINUITY_CLAUSE.length).trimEnd();
28509
- const system = `${head} ${args2.guidance.trim()} ${CONTINUITY_CLAUSE}`;
28534
+ const system = `${head} ${args2.guidance.trim()} ${FAST_DEFER_RULE} ${CONTINUITY_CLAUSE}`;
28510
28535
  const recent = args2.memory.recent.map((t) => ({
28511
28536
  role: t.role === "agent" ? "assistant" : "user",
28512
28537
  content: t.text
@@ -29694,6 +29719,8 @@ var BridgeAgent = class _BridgeAgent {
29694
29719
  fastPathDeferred = 0;
29695
29720
  /** Of those, `full`-mode turns the deterministic pre-check deferred without calling the fast model. */
29696
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;
29697
29724
  /**
29698
29725
  * Task 8: count of live AUTHENTICATED /v1/ws sessions, so a later `status`
29699
29726
  * verb can tell "bridge process up" (the app's green dot, which only
@@ -31971,11 +31998,15 @@ var BridgeAgent = class _BridgeAgent {
31971
31998
  });
31972
31999
  const maxSentences = this.voiceSettings?.brevity === "one" ? 1 : 2;
31973
32000
  const raw = await this.latency.time("voice.fast_llm", () => fastLlm.complete(messages));
31974
- const { reply, defer } = splitFastReply(raw, 200, maxSentences);
31975
- 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;
31976
32006
  await this.sendSpoken(m.channelId, reply, fastReplyId(this.replyIdKey, this.cfg.identityId, m.channelId, m.messageId));
31977
32007
  this.fastPathSent++;
31978
32008
  if (defer) this.fastPathDeferred++;
32009
+ if (refusal) this.fastPathRefusalDeferred++;
31979
32010
  return defer ? "deferred" : "answered";
31980
32011
  } catch (e) {
31981
32012
  this.fastPathFailed++;
@@ -32344,6 +32375,7 @@ var BridgeAgent = class _BridgeAgent {
32344
32375
  if (ownerDirect || ownerRelayed) tool_grants = OWNER_DM_DEFAULT_READ_GRANTS;
32345
32376
  }
32346
32377
  }
32378
+ this.logAuthzDecision(channelId, senderId, relaySpeakerId, audience, tool_grants.length);
32347
32379
  return Object.freeze({
32348
32380
  version: 1,
32349
32381
  audience,
@@ -32351,6 +32383,54 @@ var BridgeAgent = class _BridgeAgent {
32351
32383
  tool_grants
32352
32384
  });
32353
32385
  }
32386
+ /**
32387
+ * One content-free INFO line per authorization decision (every call site of
32388
+ * authorizationFor stages exactly one runtime turn). It says WHY a turn got
32389
+ * the grants it got, without any identifier, name or text: every field is a
32390
+ * boolean, a count, or a closed-set label.
32391
+ *
32392
+ * owner_pin pinned | anomalous | missing (channelOwner cache)
32393
+ * owner_row_is_agent_owner the pinned owner's roster row; `absent` when
32394
+ * there is no pin or the row is not in the roster
32395
+ * sender_is_owner sender identity == pinned owner identity
32396
+ * relay sender's roster row has is_relay
32397
+ * relay_speaker_is_owner `na` unless a relay speaker was passed
32398
+ * kill_switch SECURECOMS_OWNER_DM_DEFAULT_GRANTS as read at
32399
+ * startup: `on` = default grants enabled
32400
+ * meta_fresh / roster_fresh whether the channel meta / roster caches were
32401
+ * fresh (either stale forces audience=unknown)
32402
+ */
32403
+ logAuthzDecision(channelId, senderId, relaySpeakerId, audience, grants) {
32404
+ const meta = this.freshMeta(channelId);
32405
+ const roster = this.freshRoster(channelId);
32406
+ const sender = roster?.get(senderId);
32407
+ const pinned = this.channelOwner.get(channelId);
32408
+ const ownerId = pinned && pinned !== "anomalous" ? pinned.identityId : void 0;
32409
+ const ownerRow = ownerId !== void 0 ? roster?.get(ownerId) : void 0;
32410
+ const ownerPin = pinned === void 0 ? "missing" : pinned === "anomalous" ? "anomalous" : "pinned";
32411
+ const ownerRowIsAgentOwner = ownerRow === void 0 ? "absent" : ownerRow.is_agent_owner ? "true" : "false";
32412
+ const speakerIsOwner = relaySpeakerId === void 0 ? "na" : ownerId !== void 0 && relaySpeakerId === ownerId ? "true" : "false";
32413
+ writeServiceLog("info", "authz.decision", {
32414
+ audience: ServiceLogEnum.of(audience, [
32415
+ "owner_dm",
32416
+ "other_human_dm",
32417
+ "team_room",
32418
+ "partner_room",
32419
+ "agent_to_agent",
32420
+ "unknown"
32421
+ ]),
32422
+ grants,
32423
+ owner_pin: ServiceLogEnum.of(ownerPin, ["pinned", "anomalous", "missing"]),
32424
+ owner_row_is_agent_owner: ServiceLogEnum.of(ownerRowIsAgentOwner, ["true", "false", "absent"]),
32425
+ sender_is_owner: ownerId !== void 0 && senderId === ownerId,
32426
+ relay: sender?.is_relay === true,
32427
+ relay_speaker_is_owner: ServiceLogEnum.of(speakerIsOwner, ["true", "false", "na"]),
32428
+ is_dm: meta?.is_dm === true,
32429
+ kill_switch: ServiceLogEnum.of(this.ownerDmDefaultGrants ? "on" : "off", ["on", "off"]),
32430
+ meta_fresh: meta !== void 0,
32431
+ roster_fresh: roster !== void 0
32432
+ });
32433
+ }
32354
32434
  /**
32355
32435
  * Task 8 (owner-attested Reset Secure Session): STRUCTURAL owner selection —
32356
32436
  * NEVER read the server's `role` field to pick the owner. In a DM the owner
@@ -33878,6 +33958,7 @@ var BridgeAgent = class _BridgeAgent {
33878
33958
  fast_path_skipped: this.fastPathSkipped,
33879
33959
  fast_path_deferred: this.fastPathDeferred,
33880
33960
  fast_path_intent_deferred: this.fastPathIntentDeferred,
33961
+ fast_path_refusal_deferred: this.fastPathRefusalDeferred,
33881
33962
  fast_path_mode: this.cfg.voiceFastPathMode ?? "full",
33882
33963
  voice_settings_version: this.voiceSettingsVersion,
33883
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
@@ -1967,6 +1969,24 @@ export declare class BridgeAgent {
1967
1969
  * Everything else gets the frozen empty list.
1968
1970
  */
1969
1971
  private authorizationFor;
1972
+ /**
1973
+ * One content-free INFO line per authorization decision (every call site of
1974
+ * authorizationFor stages exactly one runtime turn). It says WHY a turn got
1975
+ * the grants it got, without any identifier, name or text: every field is a
1976
+ * boolean, a count, or a closed-set label.
1977
+ *
1978
+ * owner_pin pinned | anomalous | missing (channelOwner cache)
1979
+ * owner_row_is_agent_owner the pinned owner's roster row; `absent` when
1980
+ * there is no pin or the row is not in the roster
1981
+ * sender_is_owner sender identity == pinned owner identity
1982
+ * relay sender's roster row has is_relay
1983
+ * relay_speaker_is_owner `na` unless a relay speaker was passed
1984
+ * kill_switch SECURECOMS_OWNER_DM_DEFAULT_GRANTS as read at
1985
+ * startup: `on` = default grants enabled
1986
+ * meta_fresh / roster_fresh whether the channel meta / roster caches were
1987
+ * fresh (either stale forces audience=unknown)
1988
+ */
1989
+ private logAuthzDecision;
1970
1990
  /**
1971
1991
  * Task 8 (owner-attested Reset Secure Session): STRUCTURAL owner selection —
1972
1992
  * NEVER read the server's `role` field to pick the owner. In a DM the owner
@@ -2326,6 +2346,8 @@ export declare class BridgeAgent {
2326
2346
  fast_path_deferred: number;
2327
2347
  /** `full`-mode turns deferred by the deterministic pre-check (no fast model call). */
2328
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;
2329
2351
  /** Credential `voice_fast_path_mode`: "full" (default), "ack_only" or "off". */
2330
2352
  fast_path_mode: "full" | "ack_only" | "off";
2331
2353
  /** Plan 2026-09-06 (agent voice settings): version of the loaded backend
package/dist/index.js CHANGED
@@ -27308,6 +27308,15 @@ var TokenBucketFloor = class {
27308
27308
  };
27309
27309
 
27310
27310
  // src/service-log.ts
27311
+ var ENUM_VALUE = /^[a-z][a-z_]{0,31}$/;
27312
+ var ServiceLogEnum = class _ServiceLogEnum {
27313
+ constructor(value) {
27314
+ this.value = value;
27315
+ }
27316
+ static of(value, allowed) {
27317
+ return new _ServiceLogEnum(allowed.includes(value) && ENUM_VALUE.test(value) ? value : "invalid");
27318
+ }
27319
+ };
27311
27320
  var TOKEN2 = /^[a-z][a-z0-9_.-]{0,63}$/;
27312
27321
  var CLASS2 = /^[A-Za-z_$][A-Za-z0-9_$]{0,63}$/;
27313
27322
  function errorClass2(error) {
@@ -27337,6 +27346,7 @@ function formatServiceLog2(level3, event, fields = {}, error) {
27337
27346
  if (!TOKEN2.test(key)) continue;
27338
27347
  if (typeof value === "boolean") parts.push(`${key}=${value}`);
27339
27348
  else if (typeof value === "number" && Number.isFinite(value)) parts.push(`${key}=${value}`);
27349
+ else if (value instanceof ServiceLogEnum && ENUM_VALUE.test(value.value)) parts.push(`${key}=${value.value}`);
27340
27350
  }
27341
27351
  if (error !== void 0) parts.push(`error_class=${errorClass2(error)}`);
27342
27352
  return parts.join(" ");
@@ -28278,11 +28288,26 @@ function clampContent(content, max = MAX_MESSAGE_CHARS) {
28278
28288
  const marker = "\u2026";
28279
28289
  return content.slice(0, Math.max(0, max - marker.length)) + marker;
28280
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
+ }
28281
28306
  function buildFastMessages(args2) {
28282
28307
  const cap = args2.charCap ?? FAST_RECENT_CHARS_CAP;
28283
28308
  const sp = systemPrompt(args2.persona, args2.settings, args2.agentName);
28284
28309
  const head = sp.slice(0, sp.length - CONTINUITY_CLAUSE.length).trimEnd();
28285
- const system = `${head} ${args2.guidance.trim()} ${CONTINUITY_CLAUSE}`;
28310
+ const system = `${head} ${args2.guidance.trim()} ${FAST_DEFER_RULE} ${CONTINUITY_CLAUSE}`;
28286
28311
  const recent = args2.memory.recent.map((t) => ({
28287
28312
  role: t.role === "agent" ? "assistant" : "user",
28288
28313
  content: t.text
@@ -29542,6 +29567,8 @@ var BridgeAgent = class _BridgeAgent {
29542
29567
  fastPathDeferred = 0;
29543
29568
  /** Of those, `full`-mode turns the deterministic pre-check deferred without calling the fast model. */
29544
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;
29545
29572
  /**
29546
29573
  * Task 8: count of live AUTHENTICATED /v1/ws sessions, so a later `status`
29547
29574
  * verb can tell "bridge process up" (the app's green dot, which only
@@ -31819,11 +31846,15 @@ var BridgeAgent = class _BridgeAgent {
31819
31846
  });
31820
31847
  const maxSentences = this.voiceSettings?.brevity === "one" ? 1 : 2;
31821
31848
  const raw = await this.latency.time("voice.fast_llm", () => fastLlm.complete(messages));
31822
- const { reply, defer } = splitFastReply(raw, 200, maxSentences);
31823
- 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;
31824
31854
  await this.sendSpoken(m.channelId, reply, fastReplyId(this.replyIdKey, this.cfg.identityId, m.channelId, m.messageId));
31825
31855
  this.fastPathSent++;
31826
31856
  if (defer) this.fastPathDeferred++;
31857
+ if (refusal) this.fastPathRefusalDeferred++;
31827
31858
  return defer ? "deferred" : "answered";
31828
31859
  } catch (e) {
31829
31860
  this.fastPathFailed++;
@@ -32192,6 +32223,7 @@ var BridgeAgent = class _BridgeAgent {
32192
32223
  if (ownerDirect || ownerRelayed) tool_grants = OWNER_DM_DEFAULT_READ_GRANTS;
32193
32224
  }
32194
32225
  }
32226
+ this.logAuthzDecision(channelId, senderId, relaySpeakerId, audience, tool_grants.length);
32195
32227
  return Object.freeze({
32196
32228
  version: 1,
32197
32229
  audience,
@@ -32199,6 +32231,54 @@ var BridgeAgent = class _BridgeAgent {
32199
32231
  tool_grants
32200
32232
  });
32201
32233
  }
32234
+ /**
32235
+ * One content-free INFO line per authorization decision (every call site of
32236
+ * authorizationFor stages exactly one runtime turn). It says WHY a turn got
32237
+ * the grants it got, without any identifier, name or text: every field is a
32238
+ * boolean, a count, or a closed-set label.
32239
+ *
32240
+ * owner_pin pinned | anomalous | missing (channelOwner cache)
32241
+ * owner_row_is_agent_owner the pinned owner's roster row; `absent` when
32242
+ * there is no pin or the row is not in the roster
32243
+ * sender_is_owner sender identity == pinned owner identity
32244
+ * relay sender's roster row has is_relay
32245
+ * relay_speaker_is_owner `na` unless a relay speaker was passed
32246
+ * kill_switch SECURECOMS_OWNER_DM_DEFAULT_GRANTS as read at
32247
+ * startup: `on` = default grants enabled
32248
+ * meta_fresh / roster_fresh whether the channel meta / roster caches were
32249
+ * fresh (either stale forces audience=unknown)
32250
+ */
32251
+ logAuthzDecision(channelId, senderId, relaySpeakerId, audience, grants) {
32252
+ const meta = this.freshMeta(channelId);
32253
+ const roster = this.freshRoster(channelId);
32254
+ const sender = roster?.get(senderId);
32255
+ const pinned = this.channelOwner.get(channelId);
32256
+ const ownerId = pinned && pinned !== "anomalous" ? pinned.identityId : void 0;
32257
+ const ownerRow = ownerId !== void 0 ? roster?.get(ownerId) : void 0;
32258
+ const ownerPin = pinned === void 0 ? "missing" : pinned === "anomalous" ? "anomalous" : "pinned";
32259
+ const ownerRowIsAgentOwner = ownerRow === void 0 ? "absent" : ownerRow.is_agent_owner ? "true" : "false";
32260
+ const speakerIsOwner = relaySpeakerId === void 0 ? "na" : ownerId !== void 0 && relaySpeakerId === ownerId ? "true" : "false";
32261
+ writeServiceLog2("info", "authz.decision", {
32262
+ audience: ServiceLogEnum.of(audience, [
32263
+ "owner_dm",
32264
+ "other_human_dm",
32265
+ "team_room",
32266
+ "partner_room",
32267
+ "agent_to_agent",
32268
+ "unknown"
32269
+ ]),
32270
+ grants,
32271
+ owner_pin: ServiceLogEnum.of(ownerPin, ["pinned", "anomalous", "missing"]),
32272
+ owner_row_is_agent_owner: ServiceLogEnum.of(ownerRowIsAgentOwner, ["true", "false", "absent"]),
32273
+ sender_is_owner: ownerId !== void 0 && senderId === ownerId,
32274
+ relay: sender?.is_relay === true,
32275
+ relay_speaker_is_owner: ServiceLogEnum.of(speakerIsOwner, ["true", "false", "na"]),
32276
+ is_dm: meta?.is_dm === true,
32277
+ kill_switch: ServiceLogEnum.of(this.ownerDmDefaultGrants ? "on" : "off", ["on", "off"]),
32278
+ meta_fresh: meta !== void 0,
32279
+ roster_fresh: roster !== void 0
32280
+ });
32281
+ }
32202
32282
  /**
32203
32283
  * Task 8 (owner-attested Reset Secure Session): STRUCTURAL owner selection —
32204
32284
  * NEVER read the server's `role` field to pick the owner. In a DM the owner
@@ -33726,6 +33806,7 @@ var BridgeAgent = class _BridgeAgent {
33726
33806
  fast_path_skipped: this.fastPathSkipped,
33727
33807
  fast_path_deferred: this.fastPathDeferred,
33728
33808
  fast_path_intent_deferred: this.fastPathIntentDeferred,
33809
+ fast_path_refusal_deferred: this.fastPathRefusalDeferred,
33729
33810
  fast_path_mode: this.cfg.voiceFastPathMode ?? "full",
33730
33811
  voice_settings_version: this.voiceSettingsVersion,
33731
33812
  voice_settings_source: this.voiceSettingsSource,
@@ -1,5 +1,18 @@
1
1
  export type ServiceLogLevel = "debug" | "info" | "warn" | "error";
2
- export type ServiceLogFields = Readonly<Record<string, number | boolean>>;
2
+ /**
3
+ * A closed-set label for a service-log field. The only way to build one is
4
+ * `ServiceLogEnum.of(value, allowed)`: a value outside `allowed`, or one that
5
+ * is not a short lowercase word, renders as `invalid`. Raw strings are still
6
+ * dropped by `formatServiceLog`, so an identifier, name or message text can
7
+ * never reach the log through a field. The privacy test requires `allowed` to
8
+ * be an inline array of string literals at every call site.
9
+ */
10
+ export declare class ServiceLogEnum {
11
+ readonly value: string;
12
+ private constructor();
13
+ static of<T extends string>(value: T, allowed: readonly T[]): ServiceLogEnum;
14
+ }
15
+ export type ServiceLogFields = Readonly<Record<string, number | boolean | ServiceLogEnum>>;
3
16
  export type ServiceLog = (level: ServiceLogLevel, event: string, fields?: ServiceLogFields, error?: unknown) => void;
4
17
  /** Reduce a thrown value without reading its message, stack, or string form. */
5
18
  export declare function errorClass(error: unknown): string;
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const BRIDGE_VERSION = "0.26.2";
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.2",
3
+ "version": "0.26.4",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=22.5.0"