@animalabs/agent-framework 0.28.0 → 0.29.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.
Files changed (240) hide show
  1. package/README.md +88 -25
  2. package/dist/src/agent.d.ts.map +1 -1
  3. package/dist/src/agent.js +4 -1
  4. package/dist/src/agent.js.map +1 -1
  5. package/dist/src/api/mcp-server-core.d.ts.map +1 -1
  6. package/dist/src/api/mcp-server-core.js +3 -12
  7. package/dist/src/api/mcp-server-core.js.map +1 -1
  8. package/dist/src/api/server.d.ts.map +1 -1
  9. package/dist/src/api/server.js +7 -3
  10. package/dist/src/api/server.js.map +1 -1
  11. package/dist/src/auto-continue.d.ts +2 -0
  12. package/dist/src/auto-continue.d.ts.map +1 -1
  13. package/dist/src/auto-continue.js +5 -2
  14. package/dist/src/auto-continue.js.map +1 -1
  15. package/dist/src/code-execution/isolate/runner.d.ts +7 -5
  16. package/dist/src/code-execution/isolate/runner.d.ts.map +1 -1
  17. package/dist/src/code-execution/isolate/runner.js +7 -5
  18. package/dist/src/code-execution/isolate/runner.js.map +1 -1
  19. package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -1
  20. package/dist/src/code-execution/isolate/scripts.js +3 -2
  21. package/dist/src/code-execution/isolate/scripts.js.map +1 -1
  22. package/dist/src/framework.d.ts +164 -36
  23. package/dist/src/framework.d.ts.map +1 -1
  24. package/dist/src/framework.js +1120 -479
  25. package/dist/src/framework.js.map +1 -1
  26. package/dist/src/gate/event-gate.d.ts +34 -10
  27. package/dist/src/gate/event-gate.d.ts.map +1 -1
  28. package/dist/src/gate/event-gate.js +118 -48
  29. package/dist/src/gate/event-gate.js.map +1 -1
  30. package/dist/src/gate/index.d.ts +1 -1
  31. package/dist/src/gate/index.d.ts.map +1 -1
  32. package/dist/src/gate/index.js +1 -1
  33. package/dist/src/gate/index.js.map +1 -1
  34. package/dist/src/gate/types.d.ts +12 -2
  35. package/dist/src/gate/types.d.ts.map +1 -1
  36. package/dist/src/heartbeat/heartbeat.d.ts +18 -0
  37. package/dist/src/heartbeat/heartbeat.d.ts.map +1 -1
  38. package/dist/src/heartbeat/heartbeat.js +34 -8
  39. package/dist/src/heartbeat/heartbeat.js.map +1 -1
  40. package/dist/src/held-writes.d.ts +3 -3
  41. package/dist/src/held-writes.js +3 -3
  42. package/dist/src/host-presentation.d.ts +170 -0
  43. package/dist/src/host-presentation.d.ts.map +1 -0
  44. package/dist/src/host-presentation.js +561 -0
  45. package/dist/src/host-presentation.js.map +1 -0
  46. package/dist/src/index.d.ts +2 -2
  47. package/dist/src/index.d.ts.map +1 -1
  48. package/dist/src/index.js +1 -1
  49. package/dist/src/index.js.map +1 -1
  50. package/dist/src/line-reader.d.ts +4 -0
  51. package/dist/src/line-reader.d.ts.map +1 -0
  52. package/dist/src/line-reader.js +59 -0
  53. package/dist/src/line-reader.js.map +1 -0
  54. package/dist/src/mcpl/channel-registry.d.ts +34 -6
  55. package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
  56. package/dist/src/mcpl/channel-registry.js +131 -53
  57. package/dist/src/mcpl/channel-registry.js.map +1 -1
  58. package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
  59. package/dist/src/mcpl/conversation-router.js +2 -1
  60. package/dist/src/mcpl/conversation-router.js.map +1 -1
  61. package/dist/src/mcpl/convert-block.d.ts +1 -0
  62. package/dist/src/mcpl/convert-block.d.ts.map +1 -1
  63. package/dist/src/mcpl/convert-block.js +16 -4
  64. package/dist/src/mcpl/convert-block.js.map +1 -1
  65. package/dist/src/mcpl/hook-orchestrator.d.ts.map +1 -1
  66. package/dist/src/mcpl/hook-orchestrator.js +4 -3
  67. package/dist/src/mcpl/hook-orchestrator.js.map +1 -1
  68. package/dist/src/mcpl/inbound-source.d.ts +3 -1
  69. package/dist/src/mcpl/inbound-source.d.ts.map +1 -1
  70. package/dist/src/mcpl/inbound-source.js +3 -1
  71. package/dist/src/mcpl/inbound-source.js.map +1 -1
  72. package/dist/src/mcpl/modern-connection.d.ts.map +1 -1
  73. package/dist/src/mcpl/modern-connection.js +11 -7
  74. package/dist/src/mcpl/modern-connection.js.map +1 -1
  75. package/dist/src/mcpl/protocol-family.d.ts +3 -2
  76. package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
  77. package/dist/src/mcpl/protocol-family.js +25 -3
  78. package/dist/src/mcpl/protocol-family.js.map +1 -1
  79. package/dist/src/mcpl/push-handler.js +2 -2
  80. package/dist/src/mcpl/push-handler.js.map +1 -1
  81. package/dist/src/mcpl/references.d.ts +12 -40
  82. package/dist/src/mcpl/references.d.ts.map +1 -1
  83. package/dist/src/mcpl/references.js +29 -23
  84. package/dist/src/mcpl/references.js.map +1 -1
  85. package/dist/src/mcpl/server-connection.d.ts.map +1 -1
  86. package/dist/src/mcpl/server-connection.js +20 -20
  87. package/dist/src/mcpl/server-connection.js.map +1 -1
  88. package/dist/src/mcpl/skip-reply-wake.d.ts +25 -0
  89. package/dist/src/mcpl/skip-reply-wake.d.ts.map +1 -1
  90. package/dist/src/mcpl/skip-reply-wake.js +25 -0
  91. package/dist/src/mcpl/skip-reply-wake.js.map +1 -1
  92. package/dist/src/mcpl/tool-presence.d.ts.map +1 -1
  93. package/dist/src/mcpl/tool-presence.js +18 -17
  94. package/dist/src/mcpl/tool-presence.js.map +1 -1
  95. package/dist/src/mcpl/tool-signature.d.ts.map +1 -1
  96. package/dist/src/mcpl/tool-signature.js +17 -16
  97. package/dist/src/mcpl/tool-signature.js.map +1 -1
  98. package/dist/src/mcpl/toolset-host.d.ts +8 -26
  99. package/dist/src/mcpl/toolset-host.d.ts.map +1 -1
  100. package/dist/src/mcpl/toolset-host.js +16 -6
  101. package/dist/src/mcpl/toolset-host.js.map +1 -1
  102. package/dist/src/mcpl/toolsets-module.d.ts +14 -25
  103. package/dist/src/mcpl/toolsets-module.d.ts.map +1 -1
  104. package/dist/src/mcpl/toolsets-module.js +180 -50
  105. package/dist/src/mcpl/toolsets-module.js.map +1 -1
  106. package/dist/src/mcpl/transport.d.ts +1 -1
  107. package/dist/src/mcpl/transport.d.ts.map +1 -1
  108. package/dist/src/mcpl/transport.js +16 -16
  109. package/dist/src/mcpl/transport.js.map +1 -1
  110. package/dist/src/mcpl/types.d.ts +28 -12
  111. package/dist/src/mcpl/types.d.ts.map +1 -1
  112. package/dist/src/mcpl/types.js.map +1 -1
  113. package/dist/src/module-registry.d.ts +6 -7
  114. package/dist/src/module-registry.d.ts.map +1 -1
  115. package/dist/src/module-registry.js +6 -7
  116. package/dist/src/module-registry.js.map +1 -1
  117. package/dist/src/modules/api/index.d.ts.map +1 -1
  118. package/dist/src/modules/api/index.js +4 -2
  119. package/dist/src/modules/api/index.js.map +1 -1
  120. package/dist/src/modules/discord/index.d.ts.map +1 -1
  121. package/dist/src/modules/discord/index.js +33 -25
  122. package/dist/src/modules/discord/index.js.map +1 -1
  123. package/dist/src/modules/history/index.js +8 -6
  124. package/dist/src/modules/history/index.js.map +1 -1
  125. package/dist/src/modules/sender-name.d.ts +7 -0
  126. package/dist/src/modules/sender-name.d.ts.map +1 -0
  127. package/dist/src/modules/sender-name.js +10 -0
  128. package/dist/src/modules/sender-name.js.map +1 -0
  129. package/dist/src/modules/workspace/answers.d.ts +5 -1
  130. package/dist/src/modules/workspace/answers.d.ts.map +1 -1
  131. package/dist/src/modules/workspace/answers.js +16 -1
  132. package/dist/src/modules/workspace/answers.js.map +1 -1
  133. package/dist/src/modules/workspace/direct-write.d.ts.map +1 -1
  134. package/dist/src/modules/workspace/direct-write.js +6 -4
  135. package/dist/src/modules/workspace/direct-write.js.map +1 -1
  136. package/dist/src/modules/workspace/effects.d.ts +4 -2
  137. package/dist/src/modules/workspace/effects.d.ts.map +1 -1
  138. package/dist/src/modules/workspace/effects.js +50 -40
  139. package/dist/src/modules/workspace/effects.js.map +1 -1
  140. package/dist/src/modules/workspace/index.d.ts +1 -0
  141. package/dist/src/modules/workspace/index.d.ts.map +1 -1
  142. package/dist/src/modules/workspace/index.js +66 -36
  143. package/dist/src/modules/workspace/index.js.map +1 -1
  144. package/dist/src/modules/workspace/observe.d.ts +51 -20
  145. package/dist/src/modules/workspace/observe.d.ts.map +1 -1
  146. package/dist/src/modules/workspace/observe.js +120 -82
  147. package/dist/src/modules/workspace/observe.js.map +1 -1
  148. package/dist/src/modules/workspace/reconcile.d.ts +7 -1
  149. package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
  150. package/dist/src/modules/workspace/reconcile.js +133 -37
  151. package/dist/src/modules/workspace/reconcile.js.map +1 -1
  152. package/dist/src/notice-tags.d.ts +79 -0
  153. package/dist/src/notice-tags.d.ts.map +1 -0
  154. package/dist/src/notice-tags.js +83 -0
  155. package/dist/src/notice-tags.js.map +1 -0
  156. package/dist/src/reasoning-loss-notice.d.ts.map +1 -1
  157. package/dist/src/reasoning-loss-notice.js +5 -3
  158. package/dist/src/reasoning-loss-notice.js.map +1 -1
  159. package/dist/src/silent-heartbeat.d.ts +1 -53
  160. package/dist/src/silent-heartbeat.d.ts.map +1 -1
  161. package/dist/src/silent-heartbeat.js +56 -3
  162. package/dist/src/silent-heartbeat.js.map +1 -1
  163. package/dist/src/supplied-text.d.ts +35 -0
  164. package/dist/src/supplied-text.d.ts.map +1 -0
  165. package/dist/src/supplied-text.js +141 -0
  166. package/dist/src/supplied-text.js.map +1 -0
  167. package/dist/src/tool-args.d.ts.map +1 -1
  168. package/dist/src/tool-args.js +9 -8
  169. package/dist/src/tool-args.js.map +1 -1
  170. package/dist/src/tool-image-ledger.d.ts.map +1 -1
  171. package/dist/src/tool-image-ledger.js +2 -1
  172. package/dist/src/tool-image-ledger.js.map +1 -1
  173. package/dist/src/tool-presentation.d.ts.map +1 -1
  174. package/dist/src/tool-presentation.js +5 -4
  175. package/dist/src/tool-presentation.js.map +1 -1
  176. package/dist/src/tune-out/coordinator.d.ts +3 -2
  177. package/dist/src/tune-out/coordinator.d.ts.map +1 -1
  178. package/dist/src/tune-out/coordinator.js +24 -26
  179. package/dist/src/tune-out/coordinator.js.map +1 -1
  180. package/dist/src/types/agent.d.ts +23 -9
  181. package/dist/src/types/agent.d.ts.map +1 -1
  182. package/dist/src/types/framework.d.ts +43 -0
  183. package/dist/src/types/framework.d.ts.map +1 -1
  184. package/dist/src/types/module.d.ts +22 -11
  185. package/dist/src/types/module.d.ts.map +1 -1
  186. package/package.json +2 -2
  187. package/src/agent.ts +4 -1
  188. package/src/api/mcp-server-core.ts +3 -14
  189. package/src/api/server.ts +7 -3
  190. package/src/auto-continue.ts +8 -2
  191. package/src/code-execution/isolate/runner.ts +7 -5
  192. package/src/code-execution/isolate/scripts.ts +3 -2
  193. package/src/framework.ts +1132 -492
  194. package/src/gate/event-gate.ts +128 -47
  195. package/src/gate/index.ts +1 -1
  196. package/src/gate/types.ts +12 -2
  197. package/src/heartbeat/heartbeat.ts +48 -9
  198. package/src/held-writes.ts +3 -3
  199. package/src/host-presentation.ts +605 -0
  200. package/src/index.ts +2 -2
  201. package/src/line-reader.ts +58 -0
  202. package/src/mcpl/channel-registry.ts +137 -52
  203. package/src/mcpl/conversation-router.ts +2 -1
  204. package/src/mcpl/convert-block.ts +17 -4
  205. package/src/mcpl/hook-orchestrator.ts +4 -3
  206. package/src/mcpl/inbound-source.ts +3 -1
  207. package/src/mcpl/modern-connection.ts +11 -7
  208. package/src/mcpl/protocol-family.ts +24 -3
  209. package/src/mcpl/push-handler.ts +2 -2
  210. package/src/mcpl/references.ts +31 -23
  211. package/src/mcpl/server-connection.ts +20 -18
  212. package/src/mcpl/skip-reply-wake.ts +35 -0
  213. package/src/mcpl/tool-presence.ts +18 -17
  214. package/src/mcpl/tool-signature.ts +17 -16
  215. package/src/mcpl/toolset-host.ts +18 -6
  216. package/src/mcpl/toolsets-module.ts +142 -50
  217. package/src/mcpl/transport.ts +18 -14
  218. package/src/mcpl/types.ts +29 -12
  219. package/src/module-registry.ts +6 -7
  220. package/src/modules/api/index.ts +4 -2
  221. package/src/modules/discord/index.ts +37 -25
  222. package/src/modules/history/index.ts +8 -6
  223. package/src/modules/sender-name.ts +9 -0
  224. package/src/modules/workspace/answers.ts +20 -2
  225. package/src/modules/workspace/direct-write.ts +4 -3
  226. package/src/modules/workspace/effects.ts +50 -40
  227. package/src/modules/workspace/index.ts +58 -34
  228. package/src/modules/workspace/observe.ts +139 -88
  229. package/src/modules/workspace/reconcile.ts +159 -36
  230. package/src/notice-tags.ts +84 -0
  231. package/src/reasoning-loss-notice.ts +5 -3
  232. package/src/silent-heartbeat.ts +4 -3
  233. package/src/supplied-text.ts +154 -0
  234. package/src/tool-args.ts +9 -8
  235. package/src/tool-image-ledger.ts +2 -1
  236. package/src/tool-presentation.ts +5 -4
  237. package/src/tune-out/coordinator.ts +25 -27
  238. package/src/types/agent.ts +23 -9
  239. package/src/types/framework.ts +44 -0
  240. package/src/types/module.ts +23 -11
@@ -11,6 +11,8 @@
11
11
  * inference tokens, not whether the agent should know about the event.
12
12
  */
13
13
 
14
+ import { NOTICE_TAG } from '../notice-tags.js';
15
+ import { cutToFit, suppliedValue } from '../supplied-text.js';
14
16
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
15
17
  import { basename, dirname, join } from 'node:path';
16
18
  import { GateScript } from './gate-script.js';
@@ -343,7 +345,7 @@ function referenceLine(count: number, channel: string, oldest: number, now: numb
343
345
  /** A handed batched-wake line's text (WakeLine), as it reads when written at
344
346
  * `now`: the gate's words, its message's age counted to then. */
345
347
  export function wakeLineText(line: WakeLine, now: number): string {
346
- return `[Gate: 1 event matched]\n\n${referenceLine(1, line.shown, line.at, now)}`;
348
+ return `[${NOTICE_TAG.gate}: 1 event matched]\n\n${referenceLine(1, line.shown, line.at, now)}`;
347
349
  }
348
350
 
349
351
  /** The event's own stored entry, as the framework is asked about it. */
@@ -1825,30 +1827,72 @@ function globAtLeastAsBroad(earlier: string | undefined, later: string | undefin
1825
1827
  return earlier === later;
1826
1828
  }
1827
1829
 
1830
+ /** The event types the gate can see from an MCPL server: its pushes and its
1831
+ * channels' messages. */
1832
+ export const MCPL_GATE_EVENT_TYPES: readonly string[] = ['mcpl:push-event', 'mcpl:channel-incoming'];
1833
+
1834
+ /** The event types a host knows a source sends (EventGate's
1835
+ * `eventTypesFrom`), or undefined when it could send any. */
1836
+ export type EventTypesFrom = (source: string) => readonly string[] | undefined;
1837
+
1838
+ /** The event types `rule` can match: its scope's, narrowed to those its
1839
+ * source is known to send; 'all' when neither says. */
1840
+ function typesSeenBy(rule: GatePolicy, eventTypesFrom?: EventTypesFrom): { types: string[] | 'all'; known: boolean } {
1841
+ const scope = rule.match.scope && rule.match.scope.length > 0 ? rule.match.scope : undefined;
1842
+ const source = rule.match.source;
1843
+ let sent: readonly string[] | undefined;
1844
+ if (eventTypesFrom && source !== undefined && !source.includes('*')) {
1845
+ try {
1846
+ sent = eventTypesFrom(source);
1847
+ } catch {
1848
+ sent = undefined;
1849
+ }
1850
+ }
1851
+ if (scope && sent) return { types: scope.filter((t) => sent!.includes(t)), known: true };
1852
+ if (scope) return { types: scope, known: true };
1853
+ if (sent) return { types: [...sent], known: true };
1854
+ return { types: 'all', known: false };
1855
+ }
1856
+
1828
1857
  /**
1829
- * Event types for which `earlier` provably matches EVERY event that `later`
1830
- * would match — i.e. the types for which `later` is unreachable when
1831
- * `earlier` sits before it and consumes its matches. Returns null when
1832
- * subsumption can't be proven (which is not proof of safety — the lint is
1833
- * deliberately one-sided so every warning it emits is true).
1858
+ * Whether `earlier` provably matches events `later` would match, so that
1859
+ * `later` doesn't see them when `earlier` sits before it and consumes its
1860
+ * matches: the event types concerned, whether that's every event `later`
1861
+ * would match, and whether `later` is known to see those types. Returns null
1862
+ * when it can't be proven (which is not proof of safety — the lint is
1863
+ * deliberately one-sided so every note it emits is true).
1834
1864
  */
1835
- function subsumedEventTypes(earlier: GatePolicy, later: GatePolicy): string[] | 'all' | null {
1865
+ function subsumedEventTypes(
1866
+ earlier: GatePolicy,
1867
+ later: GatePolicy,
1868
+ eventTypesFrom?: EventTypesFrom,
1869
+ ): Omit<ShadowWarning, 'earlier' | 'later'> | null {
1836
1870
  const e = earlier.match;
1837
1871
  const l = later.match;
1838
1872
 
1839
- // Scope: which of `later`'s event types does `earlier` cover?
1840
- let covered: string[] | 'all';
1841
- if (!e.scope || e.scope.length === 0) {
1842
- covered = l.scope && l.scope.length > 0 ? l.scope : 'all';
1843
- } else if (!l.scope || l.scope.length === 0) {
1844
- // `later` matches all types; `earlier` only consumes its own scope, so
1845
- // the shadow is partial — exactly those types.
1846
- covered = e.scope;
1873
+ // Event types: which of those `later` can match does `earlier` cover?
1874
+ const seen = typesSeenBy(later, eventTypesFrom);
1875
+ const earlierTypes = e.scope && e.scope.length > 0 ? e.scope : 'all';
1876
+ let eventTypes: string[] | 'all';
1877
+ let whole: boolean;
1878
+ let known = seen.known;
1879
+ if (earlierTypes === 'all') {
1880
+ eventTypes = seen.types;
1881
+ whole = true;
1882
+ } else if (seen.types === 'all') {
1883
+ // `later` matches every type; `earlier` only consumes its own scope, so
1884
+ // the overlap is partial — exactly those types, which may never reach
1885
+ // `later`.
1886
+ eventTypes = earlierTypes;
1887
+ whole = false;
1847
1888
  } else {
1848
- const overlap = l.scope.filter((t) => e.scope!.includes(t));
1889
+ const overlap = seen.types.filter((t) => earlierTypes.includes(t));
1849
1890
  if (overlap.length === 0) return null;
1850
- covered = overlap;
1891
+ eventTypes = overlap;
1892
+ whole = overlap.length === seen.types.length;
1893
+ known = true;
1851
1894
  }
1895
+ if (Array.isArray(eventTypes) && eventTypes.length === 0) return null;
1852
1896
 
1853
1897
  // Every remaining discriminator on `earlier` must be at least as broad as
1854
1898
  // `later`'s, otherwise some `later` events slip past `earlier`.
@@ -1876,38 +1920,55 @@ function subsumedEventTypes(earlier: GatePolicy, later: GatePolicy): string[] |
1876
1920
  if (e.tagsAll && !subsetOf(e.tagsAll, l.tagsAll ?? [])) return null;
1877
1921
  if (e.tagsNone && !subsetOf(e.tagsNone, l.tagsNone ?? [])) return null;
1878
1922
 
1879
- return covered;
1923
+ return { eventTypes, whole, known };
1880
1924
  }
1881
1925
 
1882
1926
  /**
1883
- * Find rules made unreachable by earlier, broader rules (first match wins).
1927
+ * Find rules an earlier, broader rule keeps from applying (first match wins),
1928
+ * in whole or for some event types.
1884
1929
  * The 2026-08-08 Mythos incident is the canonical catch: a prepended
1885
1930
  * `{scope:[mcpl:channel-incoming]}` sampler silently made the
1886
1931
  * direct-address and mention rules dead for the whole incoming lane.
1887
1932
  * `passthrough` observers shadow nothing — non-firing matches fall through.
1933
+ * `eventTypesFrom` leaves out types a rule's source is known never to send:
1934
+ * a rule for an MCPL server's events isn't told about another rule that
1935
+ * takes every `external-message`.
1888
1936
  */
1889
- export function findShadowedPolicies(policies: GatePolicy[]): ShadowWarning[] {
1937
+ export function findShadowedPolicies(policies: GatePolicy[], eventTypesFrom?: EventTypesFrom): ShadowWarning[] {
1890
1938
  const out: ShadowWarning[] = [];
1891
1939
  for (let i = 0; i < policies.length; i++) {
1892
1940
  const earlier = policies[i];
1893
1941
  if (earlier.passthrough) continue;
1894
1942
  for (let j = i + 1; j < policies.length; j++) {
1895
- const covered = subsumedEventTypes(earlier, policies[j]);
1896
- if (covered) {
1897
- out.push({ earlier: earlier.name, later: policies[j].name, eventTypes: covered });
1943
+ const found = subsumedEventTypes(earlier, policies[j], eventTypesFrom);
1944
+ if (found) {
1945
+ out.push({ earlier: earlier.name, later: policies[j].name, ...found });
1898
1946
  }
1899
1947
  }
1900
1948
  }
1901
1949
  return out;
1902
1950
  }
1903
1951
 
1904
- /** Human-readable one-liner for a ShadowWarning, used in tool results,
1905
- * traces, and gate_status alike so the same hazard reads the same way
1906
- * everywhere. */
1952
+ /** A ShadowWarning in words, used in tool results, traces, and gate_status
1953
+ * alike so the same note reads the same way everywhere. It says which of
1954
+ * three it is (the rule never applies; it misses some of its events; it
1955
+ * may not be meant for those events at all) and what to do. */
1907
1956
  export function formatShadowWarning(w: ShadowWarning): string {
1908
- const types = w.eventTypes === 'all' ? 'ALL event types' : w.eventTypes.join(', ');
1909
- return `rule "${w.earlier}" makes "${w.later}" unreachable for ${types} `
1910
- + `(first match wins — every event "${w.later}" would match is consumed by "${w.earlier}" first)`;
1957
+ const later = `"${w.later}"`;
1958
+ const earlier = `"${w.earlier}"`;
1959
+ const move = `move ${later} above ${earlier} (wake_add_rule with the whole rule and insertBefore: "${w.earlier}")`;
1960
+ if (w.whole) {
1961
+ return `${later} never applies: ${earlier} comes first and matches every event ${later} would. `
1962
+ + `If ${later} should apply, ${move}, or narrow ${earlier}.`;
1963
+ }
1964
+ const types = w.eventTypes === 'all' ? 'all' : w.eventTypes.join(', ');
1965
+ if (w.known) {
1966
+ return `${earlier} comes first and takes ${later}'s ${types} events, so ${later} applies only to its others. `
1967
+ + `If it should apply to those too, ${move}.`;
1968
+ }
1969
+ return `${earlier} comes first and takes every ${types} event, so ${later} sees none. `
1970
+ + `Nothing to do unless ${later} is meant for ${types} events; then ${move}. `
1971
+ + `A scope on ${later} naming the event types it's for clears this note.`;
1911
1972
  }
1912
1973
 
1913
1974
  /** The text of a thrown value, for a log line. It never throws itself, whatever was thrown. */
@@ -2035,10 +2096,12 @@ export class EventGate {
2035
2096
  // Per-policy stats
2036
2097
  private stats = new Map<string, PolicyStats>();
2037
2098
 
2038
- // Shadow-lint cache (see refreshShadowWarnings) — recomputed on every
2039
- // config change, fingerprinted so an unchanged hazard set doesn't re-trace.
2040
- private shadowWarningsCache: ShadowWarning[] = [];
2099
+ // The shadow lint's last findings (see refreshShadowWarnings), recomputed
2100
+ // on every config change and fingerprinted so an unchanged set doesn't
2101
+ // re-trace.
2041
2102
  private shadowWarningsFingerprint = '[]';
2103
+ /** See the constructor's `eventTypesFrom`. */
2104
+ private eventTypesFromFn: EventTypesFrom | undefined;
2042
2105
 
2043
2106
  // Observability counters for events that didn't match any policy
2044
2107
  private totalEvaluations = 0;
@@ -2153,6 +2216,11 @@ export class EventGate {
2153
2216
  * (WakeProvenance.line), for a fresh start of the wake's turn to decide
2154
2217
  * (WakeLine). Absent: every batched wake writes its line, as before. */
2155
2218
  isNewestEntry?: (entry: GateEntryRef) => boolean;
2219
+ /** The event types the gate can see from a source, when the host knows
2220
+ * them (an MCPL server's id: MCPL_GATE_EVENT_TYPES), else undefined.
2221
+ * The rule-order notes (findShadowedPolicies) leave out types a rule's
2222
+ * source never sends. Absent: any source can send any type. */
2223
+ eventTypesFrom?: EventTypesFrom;
2156
2224
  }) {
2157
2225
  this.configPath = opts.configPath;
2158
2226
  this.privilegedUsersPath = opts.privilegedUsersPath;
@@ -2163,6 +2231,7 @@ export class EventGate {
2163
2231
  this.resolveRouteChannelFn = opts.resolveRouteChannel ?? defaultRouteChannel;
2164
2232
  this.channelShownFn = opts.channelShown;
2165
2233
  this.isNewestEntryFn = opts.isNewestEntry;
2234
+ this.eventTypesFromFn = opts.eventTypesFrom;
2166
2235
  this.now = opts.now ?? (() => Date.now());
2167
2236
  this.judgeFn = opts.judge;
2168
2237
  this.changesJudgedContextFn = opts.changesJudgedContext;
@@ -3448,22 +3517,32 @@ export class EventGate {
3448
3517
  /** Current shadow-lint findings for the live policy order. Fresh compute —
3449
3518
  * cheap (policy lists are tens of entries at most). */
3450
3519
  lintShadows(): ShadowWarning[] {
3451
- return findShadowedPolicies(this.config.policies);
3520
+ return findShadowedPolicies(this.config.policies, this.eventTypesFromFn);
3521
+ }
3522
+
3523
+ /**
3524
+ * The sources the host knows changed (eventTypesFrom answers differently),
3525
+ * as when an MCPL server connects or is removed: the rule-order notes are
3526
+ * made again, and traced if they changed, as a change to the rules would.
3527
+ */
3528
+ sourcesChanged(): void {
3529
+ this.refreshShadowWarnings('sources-changed');
3452
3530
  }
3453
3531
 
3454
3532
  /**
3455
- * Recompute shadow warnings and, when the set changed and is non-empty,
3456
- * emit a `gate:shadow-warnings` trace so the hazard lands in ops
3457
- * observability at the moment the order changed — not at the next missed
3458
- * DM. Called from writeConfig and reloadIfChanged.
3533
+ * Recompute shadow warnings and, when the set changed, emit a
3534
+ * `gate:shadow-warnings` trace so the hazard lands in ops observability at
3535
+ * the moment the order changed — not at the next missed DM — and, with an
3536
+ * empty list, the moment the last of them went. Called from writeConfig,
3537
+ * reloadIfChanged and sourcesChanged.
3459
3538
  */
3460
3539
  private refreshShadowWarnings(reason: string): void {
3461
3540
  const warnings = this.lintShadows();
3462
3541
  const fingerprint = JSON.stringify(warnings);
3463
3542
  if (fingerprint === this.shadowWarningsFingerprint) return;
3543
+ const hadSome = this.shadowWarningsFingerprint !== '[]';
3464
3544
  this.shadowWarningsFingerprint = fingerprint;
3465
- this.shadowWarningsCache = warnings;
3466
- if (warnings.length > 0) {
3545
+ if (warnings.length > 0 || hadSome) {
3467
3546
  this.emitTrace({
3468
3547
  type: 'gate:shadow-warnings',
3469
3548
  reason,
@@ -3621,7 +3700,7 @@ export class EventGate {
3621
3700
  const quoted = events.filter(e => !e.inContext);
3622
3701
  const referenced = events.filter(e => e.inContext);
3623
3702
 
3624
- const lines: string[] = quoted.map(e => `- [${e.policyName}] (${e.eventType}): ${e.content}`);
3703
+ const lines: string[] = quoted.map(e => `- [${e.policyName}] (${e.eventType}): ${suppliedValue(e.content)}`);
3625
3704
  // How each referenced channel is named in the line, in order.
3626
3705
  const shown: string[] = [];
3627
3706
  if (referenced.length > 0) {
@@ -3640,7 +3719,7 @@ export class EventGate {
3640
3719
  ? '(unknown channel)'
3641
3720
  : e.routeChannelId !== undefined && e.serverId !== undefined && this.channelShownFn
3642
3721
  ? this.channelShownFn({ serverId: e.serverId, channelId: e.routeChannelId })
3643
- : id;
3722
+ : suppliedValue(id);
3644
3723
  const key = JSON.stringify([e.serverId ?? null, id ?? null]);
3645
3724
  const entry = byChannel.get(key) ?? { name, count: 0, oldest: e.timestamp, label: e.channelLabel };
3646
3725
  entry.count += 1;
@@ -3653,7 +3732,7 @@ export class EventGate {
3653
3732
  // agent can still address tools that want the composite id. A name
3654
3733
  // may carry its own `#`, so one is added only where the name lacks
3655
3734
  // it, as at the other places that add a `#`.
3656
- const channel = label ? `${label.startsWith('#') ? label : `#${label}`} (${name})` : name;
3735
+ const channel = label ? `${label.startsWith('#') ? '' : '#'}${suppliedValue(label)} (${name})` : name;
3657
3736
  shown.push(channel);
3658
3737
  lines.push(referenceLine(count, channel, oldest, Date.now()));
3659
3738
  }
@@ -3666,7 +3745,7 @@ export class EventGate {
3666
3745
  // gate hands the line to the wake (WakeProvenance.line) and a fresh start
3667
3746
  // of the wake's turn decides it (WakeLine). Several messages, one with
3668
3747
  // anything after it, or content the line quotes, keep the line now.
3669
- const text = `[Gate: ${events.length} event${events.length > 1 ? 's' : ''} matched]\n\n${lines.join('\n')}`;
3748
+ const text = `[${NOTICE_TAG.gate}: ${events.length} event${events.length > 1 ? 's' : ''} matched]\n\n${lines.join('\n')}`;
3670
3749
  const only = events.length === 1 && events[0].inContext ? events[0] : undefined;
3671
3750
  let line: WakeLine | undefined;
3672
3751
  if (only && this.newestEntry(only)) {
@@ -3911,7 +3990,7 @@ export class EventGate {
3911
3990
  record.last = {
3912
3991
  at: Date.now(),
3913
3992
  verdict: outcome,
3914
- reason: safeSlice(reason, 0, 300),
3993
+ reason: cutToFit(suppliedValue(reason), 300),
3915
3994
  eventCount: state.events.length,
3916
3995
  durationMs,
3917
3996
  ...(!failed && typeof verdict.inputTokens === 'number' ? { inputTokens: verdict.inputTokens } : {}),
@@ -3919,7 +3998,7 @@ export class EventGate {
3919
3998
  if (this.config.policies.some((p) => p.name === policyName)) this.judgeRecords.set(policyName, record);
3920
3999
  if (failed) {
3921
4000
  this.judgeFailStreak++;
3922
- this.judgeLastFailure = safeSlice(reason, 0, 200);
4001
+ this.judgeLastFailure = cutToFit(suppliedValue(reason), 200);
3923
4002
  } else {
3924
4003
  this.judgeFailStreak = 0;
3925
4004
  }
@@ -4246,7 +4325,7 @@ export class EventGate {
4246
4325
  const placement: WritePlacement = {};
4247
4326
  this.addMessageFn('user', [{
4248
4327
  type: 'text',
4249
- text: `[self-wake] your ${source} timer (${Math.round(ms / 1000)}s) elapsed — now ${nowIso}`,
4328
+ text: `[${NOTICE_TAG.selfWake}] your ${source} timer (${Math.round(ms / 1000)}s) elapsed — now ${nowIso}`,
4250
4329
  }], { source: 'gate:self-wake' }, agentName, placement);
4251
4330
  this.requestInferenceFn(
4252
4331
  agentName,
@@ -4497,7 +4576,9 @@ export class EventGate {
4497
4576
  this.judgeUnwiredNotice(), this.judgeFailingNotice(),
4498
4577
  ]
4499
4578
  .filter((e): e is string => e !== null),
4500
- shadowWarnings: this.shadowWarningsCache.map(formatShadowWarning),
4579
+ // Fresh, not the cache: the sources the host knows (eventTypesFrom)
4580
+ // change as servers connect, with no change to the rules.
4581
+ shadowWarnings: this.lintShadows().map(formatShadowWarning),
4501
4582
  totalEvaluations: this.totalEvaluations,
4502
4583
  defaultDecisions: {
4503
4584
  triggered: this.defaultTriggered,
package/src/gate/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { EventGate, wakeProvenance, wakeLineText, type WakeProvenance, type WritePlacement } from './event-gate.js';
1
+ export { EventGate, MCPL_GATE_EVENT_TYPES, wakeProvenance, wakeLineText, type EventTypesFrom, type WakeProvenance, type WritePlacement } from './event-gate.js';
2
2
  export type {
3
3
  GateConfig,
4
4
  GateOptions,
package/src/gate/types.ts CHANGED
@@ -359,10 +359,20 @@ export interface GateDecision {
359
359
  export interface ShadowWarning {
360
360
  /** Name of the earlier (consuming) policy. */
361
361
  earlier: string;
362
- /** Name of the later policy it makes unreachable. */
362
+ /** Name of the later policy whose events it takes. */
363
363
  later: string;
364
- /** Event types affected — 'all' when both rules are unscoped. */
364
+ /** Event types affected: those of `later`'s (its scope's, narrowed to the
365
+ * ones its source is known to send) that `earlier` covers. 'all' only when
366
+ * neither rule has a scope and `later`'s source could send any type. */
365
367
  eventTypes: string[] | 'all';
368
+ /** Whether `earlier` takes every event `later` would match, so `later`
369
+ * never applies. */
370
+ whole: boolean;
371
+ /** Whether `later` is known to see these types: its scope names them, or
372
+ * its source is one whose event types the host knows (an MCPL server's).
373
+ * False when `later` names no scope and its source could send anything,
374
+ * so the types may never reach it. */
375
+ known: boolean;
366
376
  }
367
377
 
368
378
  /** One row of the dry-run probe table (see EventGate.probeTable). */
@@ -28,6 +28,7 @@
28
28
  * missed.
29
29
  */
30
30
 
31
+ import { NOTICE_TAG } from '../notice-tags.js';
31
32
  import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
32
33
  import { dirname } from 'node:path';
33
34
  import { checkToolInput, shownValue } from '../gate/event-gate.js';
@@ -81,7 +82,7 @@ export const DEFAULT_HEARTBEAT: Readonly<HeartbeatSettings> = {
81
82
  intervalSeconds: 4 * 60 * 60,
82
83
  paused: false,
83
84
  deliveryMode: 'message',
84
- message: '[heartbeat] Periodic self-check-in. Review anything pending — reminders, schedules, follow-ups — and act if needed; otherwise a brief note to yourself is fine.',
85
+ message: `[${NOTICE_TAG.heartbeat}] Periodic self-check-in. Review anything pending — reminders, schedules, follow-ups — and act if needed; otherwise a brief note to yourself is fine.`,
85
86
  };
86
87
 
87
88
  /** A scheduled reminder. `everySeconds` null: one-off, dropped once it fires. */
@@ -112,6 +113,17 @@ export interface HeartbeatOptions {
112
113
  importFrom?: { configPath?: string; remindersPath?: string };
113
114
  }
114
115
 
116
+ /** One of the heartbeat's scheduled wakes: its next tick or a pending reminder. */
117
+ export interface HeartbeatScheduledWake extends UpcomingWake {
118
+ kind: 'heartbeat' | 'reminder';
119
+ at: number;
120
+ /** Whether it's a silent tick, delivered as one (HeartbeatWake's `silent`). */
121
+ silent: boolean;
122
+ /** The text it brings, as of its due time: so the gate can be asked about
123
+ * the event it will judge. */
124
+ text: string;
125
+ }
126
+
115
127
  /** A wake the heartbeat asks the framework to deliver. */
116
128
  export type HeartbeatWake =
117
129
  | { kind: 'heartbeat'; reason: 'schedule' | 'manual'; silent: boolean; text: string; eventId: string }
@@ -223,11 +235,16 @@ function callAt(dueAt: number, now: () => number, onDue: () => void): () => void
223
235
  return () => clearTimeout(timer);
224
236
  }
225
237
 
238
+ /** The text a tick brings: the time, then its message. */
239
+ function tickText(now: number, message: string, timeZone: string): string {
240
+ return `[${NOTICE_TAG.currentTime}: ${formatHeartbeatTime(now, timeZone)}] ${message}`;
241
+ }
242
+
226
243
  /** The text a reminder brings: a header naming it, then its message. */
227
244
  export function reminderText(rem: Reminder, now: number, timeZone: string): string {
228
245
  let schedule = rem.everySeconds !== null ? `, repeats every ${rem.everySeconds}s` : `, set ${formatHeartbeatTime(rem.createdAt, timeZone)}`;
229
246
  if (now - rem.fireAt >= LATE_MS) schedule += `, due ${formatHeartbeatTime(rem.fireAt, timeZone)}`;
230
- return `[reminder ${rem.id}${schedule} · current time: ${formatHeartbeatTime(now, timeZone)}] ${rem.message}`;
247
+ return `[${NOTICE_TAG.reminder} ${rem.id}${schedule} · current time: ${formatHeartbeatTime(now, timeZone)}] ${rem.message}`;
231
248
  }
232
249
 
233
250
  /** A length of time as the agent is shown it. */
@@ -318,24 +335,46 @@ export class Heartbeat {
318
335
 
319
336
  /** What the heartbeat will wake the agent with, soonest first, for the reply judge (UpcomingWake). */
320
337
  upcomingWakes(): UpcomingWake[] {
321
- const wakes: UpcomingWake[] = [];
338
+ return this.scheduledWakes().map(({ at, certain, what }) => ({ at, certain, what }));
339
+ }
340
+
341
+ /**
342
+ * The heartbeat's next tick and its pending reminders, soonest first, each
343
+ * with its kind and the event it will bring, so the gate can be asked about
344
+ * it. `whose` words each for the reply judge ("its heartbeat", as
345
+ * upcomingWakes) or for the agent ("your heartbeat", as skip_reply's
346
+ * confirmation names one due before its self-wake).
347
+ */
348
+ scheduledWakes(whose: 'its' | 'your' = 'its'): HeartbeatScheduledWake[] {
349
+ const wakes: HeartbeatScheduledWake[] = [];
322
350
  const s = this.state;
323
351
  if (!s.settings.paused && s.nextFireAt !== null) {
352
+ const at = Math.max(s.nextFireAt, this.notBefore);
353
+ const silent = s.settings.deliveryMode === 'silent';
324
354
  wakes.push({
325
- at: Math.max(s.nextFireAt, this.notBefore),
355
+ kind: 'heartbeat',
356
+ at,
326
357
  certain: true,
327
- what: `its heartbeat (${s.settings.deliveryMode === 'silent' ? 'a silent tick' : 'a check-in prompt'}, every ${shownLength(s.settings.intervalSeconds)})`,
358
+ what: `${whose} heartbeat (${silent ? 'a silent tick' : 'a check-in prompt'}, every ${shownLength(s.settings.intervalSeconds)})`,
359
+ silent,
360
+ // A host's interval can put the tick past the last time a date
361
+ // shows (readSettings has no ceiling), and a time can't be shown there.
362
+ text: canHoldTime(at) ? tickText(at, s.settings.message, this.timeZone) : s.settings.message,
328
363
  });
329
364
  }
330
365
  for (const r of s.reminders) {
331
366
  const message = r.message.length > 80 ? `${r.message.slice(0, 77)}...` : r.message;
367
+ const at = Math.max(dueAt(r), this.notBefore);
332
368
  wakes.push({
333
- at: Math.max(dueAt(r), this.notBefore),
369
+ kind: 'reminder',
370
+ at,
334
371
  certain: true,
335
- what: `its reminder ${JSON.stringify(message)}${r.everySeconds !== null ? ` (repeats every ${shownLength(r.everySeconds)})` : ''}`,
372
+ what: `${whose} reminder ${JSON.stringify(message)}${r.everySeconds !== null ? ` (repeats every ${shownLength(r.everySeconds)})` : ''}`,
373
+ silent: false,
374
+ text: reminderText(r, at, this.timeZone),
336
375
  });
337
376
  }
338
- return wakes.sort((a, b) => (a.at ?? 0) - (b.at ?? 0));
377
+ return wakes.sort((a, b) => a.at - b.at);
339
378
  }
340
379
 
341
380
  /**
@@ -549,7 +588,7 @@ export class Heartbeat {
549
588
  kind: 'heartbeat',
550
589
  reason,
551
590
  silent,
552
- text: `[current time: ${formatHeartbeatTime(now, this.timeZone)}] ${s.message}`,
591
+ text: tickText(now, s.message, this.timeZone),
553
592
  eventId: `heartbeat_${now}_${Math.random().toString(36).slice(2, 8)}`,
554
593
  });
555
594
  log(`fired heartbeat (${reason}${silent ? ', silent' : ''})`);
@@ -62,9 +62,9 @@ function valueAt(value: unknown, path: string): unknown {
62
62
  * A row as a module's context answers it: marked `heldFromResidents` when it
63
63
  * carries the stamp a tune-out puts on a message it holds from the residents
64
64
  * (`metadata.tuneOut`). Every resident's viewFilter leaves a stamped row out
65
- * of its compiled view (framework.ts, createAgent), and a tool boundary
66
- * stores a stamped write without injecting it into the live turn, so a
67
- * marked row is one the store keeps and no resident is shown
65
+ * of its compiled view (framework.ts, createAgent), and a resident's tool
66
+ * boundary stores a stamped write without injecting it into the live turn, so
67
+ * a marked row is one the store keeps and no resident is shown
68
68
  * (ModuleMessage.heldFromResidents says where its text can still reach
69
69
  * one). Any other row comes back as it is.
70
70
  */