@animalabs/agent-framework 0.31.0 → 0.33.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 (298) hide show
  1. package/README.md +204 -14
  2. package/dist/src/admission/admit.d.ts +18 -0
  3. package/dist/src/admission/admit.d.ts.map +1 -1
  4. package/dist/src/admission/admit.js +16 -3
  5. package/dist/src/admission/admit.js.map +1 -1
  6. package/dist/src/agent.d.ts +185 -78
  7. package/dist/src/agent.d.ts.map +1 -1
  8. package/dist/src/agent.js +279 -102
  9. package/dist/src/agent.js.map +1 -1
  10. package/dist/src/api/mcp-server-core.d.ts.map +1 -1
  11. package/dist/src/api/mcp-server-core.js +50 -12
  12. package/dist/src/api/mcp-server-core.js.map +1 -1
  13. package/dist/src/api/server.d.ts.map +1 -1
  14. package/dist/src/api/server.js +4 -20
  15. package/dist/src/api/server.js.map +1 -1
  16. package/dist/src/code-execution/isolate/scripts.d.ts +12 -8
  17. package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -1
  18. package/dist/src/code-execution/isolate/scripts.js +24 -0
  19. package/dist/src/code-execution/isolate/scripts.js.map +1 -1
  20. package/dist/src/config-checks.d.ts +7 -0
  21. package/dist/src/config-checks.d.ts.map +1 -1
  22. package/dist/src/config-checks.js +14 -0
  23. package/dist/src/config-checks.js.map +1 -1
  24. package/dist/src/context-manager-errors.d.ts +25 -0
  25. package/dist/src/context-manager-errors.d.ts.map +1 -0
  26. package/dist/src/context-manager-errors.js +31 -0
  27. package/dist/src/context-manager-errors.js.map +1 -0
  28. package/dist/src/context-options.d.ts +67 -0
  29. package/dist/src/context-options.d.ts.map +1 -0
  30. package/dist/src/context-options.js +85 -0
  31. package/dist/src/context-options.js.map +1 -0
  32. package/dist/src/deferred-import.d.ts +93 -0
  33. package/dist/src/deferred-import.d.ts.map +1 -0
  34. package/dist/src/deferred-import.js +177 -0
  35. package/dist/src/deferred-import.js.map +1 -0
  36. package/dist/src/dendrite/audience.d.ts +223 -0
  37. package/dist/src/dendrite/audience.d.ts.map +1 -0
  38. package/dist/src/dendrite/audience.js +202 -0
  39. package/dist/src/dendrite/audience.js.map +1 -0
  40. package/dist/src/dendrite/index.d.ts +5 -3
  41. package/dist/src/dendrite/index.d.ts.map +1 -1
  42. package/dist/src/dendrite/index.js +2 -1
  43. package/dist/src/dendrite/index.js.map +1 -1
  44. package/dist/src/dendrite/presets.d.ts +10 -5
  45. package/dist/src/dendrite/presets.d.ts.map +1 -1
  46. package/dist/src/dendrite/presets.js +17 -5
  47. package/dist/src/dendrite/presets.js.map +1 -1
  48. package/dist/src/dendrite/registry.d.ts +62 -7
  49. package/dist/src/dendrite/registry.d.ts.map +1 -1
  50. package/dist/src/dendrite/registry.js +179 -10
  51. package/dist/src/dendrite/registry.js.map +1 -1
  52. package/dist/src/dendrite/types.d.ts +91 -8
  53. package/dist/src/dendrite/types.d.ts.map +1 -1
  54. package/dist/src/foreign-rounds.d.ts +69 -0
  55. package/dist/src/foreign-rounds.d.ts.map +1 -0
  56. package/dist/src/foreign-rounds.js +152 -0
  57. package/dist/src/foreign-rounds.js.map +1 -0
  58. package/dist/src/framework.d.ts +1977 -820
  59. package/dist/src/framework.d.ts.map +1 -1
  60. package/dist/src/framework.js +7571 -3457
  61. package/dist/src/framework.js.map +1 -1
  62. package/dist/src/gate/event-gate.d.ts +85 -15
  63. package/dist/src/gate/event-gate.d.ts.map +1 -1
  64. package/dist/src/gate/event-gate.js +421 -47
  65. package/dist/src/gate/event-gate.js.map +1 -1
  66. package/dist/src/gate/index.d.ts +1 -1
  67. package/dist/src/gate/index.d.ts.map +1 -1
  68. package/dist/src/gate/index.js.map +1 -1
  69. package/dist/src/gate/rule-scope.d.ts +30 -0
  70. package/dist/src/gate/rule-scope.d.ts.map +1 -0
  71. package/dist/src/gate/rule-scope.js +41 -0
  72. package/dist/src/gate/rule-scope.js.map +1 -0
  73. package/dist/src/gate/suppress-script-worker.d.ts +2 -0
  74. package/dist/src/gate/suppress-script-worker.d.ts.map +1 -0
  75. package/dist/src/gate/suppress-script-worker.js +103 -0
  76. package/dist/src/gate/suppress-script-worker.js.map +1 -0
  77. package/dist/src/gate/suppress-scripts.d.ts +105 -0
  78. package/dist/src/gate/suppress-scripts.d.ts.map +1 -0
  79. package/dist/src/gate/suppress-scripts.js +403 -0
  80. package/dist/src/gate/suppress-scripts.js.map +1 -0
  81. package/dist/src/gate/suppression-review.d.ts +143 -0
  82. package/dist/src/gate/suppression-review.d.ts.map +1 -0
  83. package/dist/src/gate/suppression-review.js +463 -0
  84. package/dist/src/gate/suppression-review.js.map +1 -0
  85. package/dist/src/gate/suppression-stats.d.ts +125 -0
  86. package/dist/src/gate/suppression-stats.d.ts.map +1 -0
  87. package/dist/src/gate/suppression-stats.js +214 -0
  88. package/dist/src/gate/suppression-stats.js.map +1 -0
  89. package/dist/src/gate/types.d.ts +135 -7
  90. package/dist/src/gate/types.d.ts.map +1 -1
  91. package/dist/src/gate/wake-onboarding.d.ts +119 -0
  92. package/dist/src/gate/wake-onboarding.d.ts.map +1 -0
  93. package/dist/src/gate/wake-onboarding.js +277 -0
  94. package/dist/src/gate/wake-onboarding.js.map +1 -0
  95. package/dist/src/held-writes.d.ts +31 -23
  96. package/dist/src/held-writes.d.ts.map +1 -1
  97. package/dist/src/held-writes.js +44 -59
  98. package/dist/src/held-writes.js.map +1 -1
  99. package/dist/src/index.d.ts +5 -2
  100. package/dist/src/index.d.ts.map +1 -1
  101. package/dist/src/index.js +3 -0
  102. package/dist/src/index.js.map +1 -1
  103. package/dist/src/jsonrpc-envelope.d.ts +69 -0
  104. package/dist/src/jsonrpc-envelope.d.ts.map +1 -0
  105. package/dist/src/jsonrpc-envelope.js +66 -0
  106. package/dist/src/jsonrpc-envelope.js.map +1 -0
  107. package/dist/src/mcpl/channel-registry.d.ts +131 -15
  108. package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
  109. package/dist/src/mcpl/channel-registry.js +290 -46
  110. package/dist/src/mcpl/channel-registry.js.map +1 -1
  111. package/dist/src/mcpl/checkpoint-manager.d.ts +2 -4
  112. package/dist/src/mcpl/checkpoint-manager.d.ts.map +1 -1
  113. package/dist/src/mcpl/checkpoint-manager.js.map +1 -1
  114. package/dist/src/mcpl/feature-set-manager.d.ts +2 -4
  115. package/dist/src/mcpl/feature-set-manager.d.ts.map +1 -1
  116. package/dist/src/mcpl/feature-set-manager.js.map +1 -1
  117. package/dist/src/mcpl/inbound-source.d.ts +2 -1
  118. package/dist/src/mcpl/inbound-source.d.ts.map +1 -1
  119. package/dist/src/mcpl/inbound-source.js +2 -1
  120. package/dist/src/mcpl/inbound-source.js.map +1 -1
  121. package/dist/src/mcpl/inference-router.d.ts +6 -6
  122. package/dist/src/mcpl/inference-router.d.ts.map +1 -1
  123. package/dist/src/mcpl/inference-router.js.map +1 -1
  124. package/dist/src/mcpl/push-coalescer.d.ts +132 -22
  125. package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
  126. package/dist/src/mcpl/push-coalescer.js +105 -15
  127. package/dist/src/mcpl/push-coalescer.js.map +1 -1
  128. package/dist/src/mcpl/push-handler.d.ts +9 -7
  129. package/dist/src/mcpl/push-handler.d.ts.map +1 -1
  130. package/dist/src/mcpl/push-handler.js.map +1 -1
  131. package/dist/src/mcpl/server-connection.d.ts +13 -10
  132. package/dist/src/mcpl/server-connection.d.ts.map +1 -1
  133. package/dist/src/mcpl/server-connection.js +134 -56
  134. package/dist/src/mcpl/server-connection.js.map +1 -1
  135. package/dist/src/mcpl/tool-classes.d.ts.map +1 -1
  136. package/dist/src/mcpl/tool-classes.js +3 -0
  137. package/dist/src/mcpl/tool-classes.js.map +1 -1
  138. package/dist/src/mcpl/types.d.ts +8 -2
  139. package/dist/src/mcpl/types.d.ts.map +1 -1
  140. package/dist/src/mcpl/types.js.map +1 -1
  141. package/dist/src/module-registry.d.ts +24 -1
  142. package/dist/src/module-registry.d.ts.map +1 -1
  143. package/dist/src/module-registry.js +68 -18
  144. package/dist/src/module-registry.js.map +1 -1
  145. package/dist/src/modules/discord/index.d.ts +45 -8
  146. package/dist/src/modules/discord/index.d.ts.map +1 -1
  147. package/dist/src/modules/discord/index.js +237 -86
  148. package/dist/src/modules/discord/index.js.map +1 -1
  149. package/dist/src/modules/history/index.d.ts +59 -0
  150. package/dist/src/modules/history/index.d.ts.map +1 -1
  151. package/dist/src/modules/history/index.js +432 -67
  152. package/dist/src/modules/history/index.js.map +1 -1
  153. package/dist/src/modules/history/semantic.d.ts +6 -5
  154. package/dist/src/modules/history/semantic.d.ts.map +1 -1
  155. package/dist/src/modules/history/semantic.js +15 -22
  156. package/dist/src/modules/history/semantic.js.map +1 -1
  157. package/dist/src/modules/workspace/disk-agreement.d.ts +1 -1
  158. package/dist/src/modules/workspace/disk-agreement.js +1 -1
  159. package/dist/src/modules/workspace/index.d.ts +28 -7
  160. package/dist/src/modules/workspace/index.d.ts.map +1 -1
  161. package/dist/src/modules/workspace/index.js +76 -22
  162. package/dist/src/modules/workspace/index.js.map +1 -1
  163. package/dist/src/notice-tags.d.ts +10 -1
  164. package/dist/src/notice-tags.d.ts.map +1 -1
  165. package/dist/src/notice-tags.js +10 -1
  166. package/dist/src/notice-tags.js.map +1 -1
  167. package/dist/src/operator-log.d.ts +36 -3
  168. package/dist/src/operator-log.d.ts.map +1 -1
  169. package/dist/src/operator-log.js +2 -2
  170. package/dist/src/operator-log.js.map +1 -1
  171. package/dist/src/presentation.d.ts +746 -0
  172. package/dist/src/presentation.d.ts.map +1 -0
  173. package/dist/src/presentation.js +1435 -0
  174. package/dist/src/presentation.js.map +1 -0
  175. package/dist/src/prose-drafts.d.ts +15 -11
  176. package/dist/src/prose-drafts.d.ts.map +1 -1
  177. package/dist/src/prose-drafts.js +16 -12
  178. package/dist/src/prose-drafts.js.map +1 -1
  179. package/dist/src/read-marks.d.ts +192 -0
  180. package/dist/src/read-marks.d.ts.map +1 -0
  181. package/dist/src/read-marks.js +242 -0
  182. package/dist/src/read-marks.js.map +1 -0
  183. package/dist/src/recovery/offline-branch.d.ts +91 -6
  184. package/dist/src/recovery/offline-branch.d.ts.map +1 -1
  185. package/dist/src/recovery/offline-branch.js +273 -29
  186. package/dist/src/recovery/offline-branch.js.map +1 -1
  187. package/dist/src/recovery/recover-cli.js +22 -3
  188. package/dist/src/recovery/recover-cli.js.map +1 -1
  189. package/dist/src/send-tools.d.ts +5 -9
  190. package/dist/src/send-tools.d.ts.map +1 -1
  191. package/dist/src/send-tools.js +40 -7
  192. package/dist/src/send-tools.js.map +1 -1
  193. package/dist/src/stored-injections.d.ts +2 -2
  194. package/dist/src/stored-injections.js +2 -2
  195. package/dist/src/stored-removal.d.ts +17 -4
  196. package/dist/src/stored-removal.d.ts.map +1 -1
  197. package/dist/src/stored-removal.js +29 -5
  198. package/dist/src/stored-removal.js.map +1 -1
  199. package/dist/src/tool-result-guard.d.ts +12 -1
  200. package/dist/src/tool-result-guard.d.ts.map +1 -1
  201. package/dist/src/tool-result-guard.js +42 -8
  202. package/dist/src/tool-result-guard.js.map +1 -1
  203. package/dist/src/tune-out/coordinator.d.ts +1159 -79
  204. package/dist/src/tune-out/coordinator.d.ts.map +1 -1
  205. package/dist/src/tune-out/coordinator.js +3709 -363
  206. package/dist/src/tune-out/coordinator.js.map +1 -1
  207. package/dist/src/tune-out/tools.d.ts +114 -17
  208. package/dist/src/tune-out/tools.d.ts.map +1 -1
  209. package/dist/src/tune-out/tools.js +87 -27
  210. package/dist/src/tune-out/tools.js.map +1 -1
  211. package/dist/src/tune-out/window-anchor.d.ts +143 -0
  212. package/dist/src/tune-out/window-anchor.d.ts.map +1 -0
  213. package/dist/src/tune-out/window-anchor.js +202 -0
  214. package/dist/src/tune-out/window-anchor.js.map +1 -0
  215. package/dist/src/types/agent.d.ts +86 -31
  216. package/dist/src/types/agent.d.ts.map +1 -1
  217. package/dist/src/types/events.d.ts +2 -0
  218. package/dist/src/types/events.d.ts.map +1 -1
  219. package/dist/src/types/framework.d.ts +43 -21
  220. package/dist/src/types/framework.d.ts.map +1 -1
  221. package/dist/src/types/module.d.ts +180 -69
  222. package/dist/src/types/module.d.ts.map +1 -1
  223. package/dist/src/types/trace.d.ts +510 -36
  224. package/dist/src/types/trace.d.ts.map +1 -1
  225. package/package.json +3 -3
  226. package/src/admission/admit.ts +32 -3
  227. package/src/agent.ts +323 -120
  228. package/src/api/mcp-server-core.ts +54 -17
  229. package/src/api/server.ts +4 -23
  230. package/src/code-execution/isolate/scripts.ts +28 -3
  231. package/src/config-checks.ts +14 -0
  232. package/src/context-manager-errors.ts +37 -0
  233. package/src/context-options.ts +96 -0
  234. package/src/deferred-import.ts +239 -0
  235. package/src/dendrite/audience.ts +315 -0
  236. package/src/dendrite/index.ts +14 -2
  237. package/src/dendrite/presets.ts +18 -6
  238. package/src/dendrite/registry.ts +209 -12
  239. package/src/dendrite/types.ts +92 -8
  240. package/src/foreign-rounds.ts +199 -0
  241. package/src/framework.ts +7814 -3585
  242. package/src/gate/event-gate.ts +428 -65
  243. package/src/gate/index.ts +3 -0
  244. package/src/gate/rule-scope.ts +45 -0
  245. package/src/gate/suppress-script-worker.ts +89 -0
  246. package/src/gate/suppress-scripts.ts +414 -0
  247. package/src/gate/suppression-review.ts +517 -0
  248. package/src/gate/suppression-stats.ts +314 -0
  249. package/src/gate/types.ts +141 -7
  250. package/src/gate/wake-onboarding.ts +309 -0
  251. package/src/held-writes.ts +49 -60
  252. package/src/index.ts +9 -2
  253. package/src/jsonrpc-envelope.ts +81 -0
  254. package/src/mcpl/channel-registry.ts +353 -85
  255. package/src/mcpl/checkpoint-manager.ts +3 -2
  256. package/src/mcpl/feature-set-manager.ts +2 -1
  257. package/src/mcpl/inbound-source.ts +2 -1
  258. package/src/mcpl/inference-router.ts +7 -4
  259. package/src/mcpl/push-coalescer.ts +193 -34
  260. package/src/mcpl/push-handler.ts +10 -5
  261. package/src/mcpl/server-connection.ts +141 -60
  262. package/src/mcpl/tool-classes.ts +3 -0
  263. package/src/mcpl/types.ts +8 -2
  264. package/src/module-registry.ts +73 -17
  265. package/src/modules/discord/index.ts +229 -81
  266. package/src/modules/history/index.ts +439 -70
  267. package/src/modules/history/semantic.ts +15 -21
  268. package/src/modules/workspace/disk-agreement.ts +1 -1
  269. package/src/modules/workspace/index.ts +85 -23
  270. package/src/notice-tags.ts +10 -1
  271. package/src/operator-log.ts +36 -3
  272. package/src/presentation.ts +1676 -0
  273. package/src/prose-drafts.ts +16 -12
  274. package/src/read-marks.ts +328 -0
  275. package/src/recovery/offline-branch.ts +346 -32
  276. package/src/recovery/recover-cli.ts +21 -3
  277. package/src/send-tools.ts +40 -7
  278. package/src/stored-injections.ts +2 -2
  279. package/src/stored-removal.ts +31 -7
  280. package/src/tool-result-guard.ts +41 -7
  281. package/src/tune-out/coordinator.ts +4089 -457
  282. package/src/tune-out/tools.ts +160 -43
  283. package/src/tune-out/window-anchor.ts +277 -0
  284. package/src/types/agent.ts +90 -31
  285. package/src/types/events.ts +2 -0
  286. package/src/types/framework.ts +43 -21
  287. package/src/types/module.ts +181 -68
  288. package/src/types/trace.ts +542 -40
  289. package/dist/src/reasoning-loss-notice.d.ts +0 -23
  290. package/dist/src/reasoning-loss-notice.d.ts.map +0 -1
  291. package/dist/src/reasoning-loss-notice.js +0 -52
  292. package/dist/src/reasoning-loss-notice.js.map +0 -1
  293. package/dist/src/reasoning-loss.d.ts +0 -24
  294. package/dist/src/reasoning-loss.d.ts.map +0 -1
  295. package/dist/src/reasoning-loss.js +0 -102
  296. package/dist/src/reasoning-loss.js.map +0 -1
  297. package/src/reasoning-loss-notice.ts +0 -64
  298. package/src/reasoning-loss.ts +0 -120
@@ -0,0 +1,309 @@
1
+ /**
2
+ * Wake onboarding: an MCPL server whose events the agent's wake rules never
3
+ * decided about.
4
+ *
5
+ * A server's events are often skipped by a rule written for something else —
6
+ * a catch-all with no `source` (a Discord-era `discord-ambient`) or the gate's
7
+ * default — so a game that waits on the agent's move, or a message addressed
8
+ * to it, stalls without anyone having chosen that. MCPL RFC-001 §5 lets a
9
+ * server suggest how its tagged events should be treated, and §5.2 forbids a
10
+ * host from applying suggestions it was not told to apply. This module is the
11
+ * missing step between the two: the host notices the first undecided event
12
+ * from a server, offers the agent that server's suggestions once, and records
13
+ * the agent's answer — accepted (all of them or some) or declined — keyed to
14
+ * the server's declared ontology, so it asks again only when the ontology
15
+ * changes.
16
+ *
17
+ * An event is *undecided* when the gate let it go by without waking
18
+ * (`defer`/`skip`) by its default, or by a rule that names neither the event's
19
+ * source nor its channel nor any of its tags. A rule that does — Linn's
20
+ * `touche-quiet`, a channel's debounce — is a decision, and this module leaves
21
+ * it alone. So does the gate's sleep, gate.js, and every rule that wakes later
22
+ * (debounce, judge, a rate limit or sampler).
23
+ *
24
+ * Pure functions here; the framework wires the notice, the wake and the tools.
25
+ */
26
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
27
+ import { dirname } from 'node:path';
28
+ import { stableHash } from '../stable-hash.js';
29
+ import { firstCatchAll, ruleDecides } from './rule-scope.js';
30
+ import type { GateBehavior, GateDecision, GatePolicy } from './types.js';
31
+
32
+ /**
33
+ * Tags that say an event is addressed to the agent or waiting on it (MCPL
34
+ * RFC-001 `chat:addressed`, RFC-014 `surface:turn`/`surface:alert`; not
35
+ * `surface:invite`, which only suggests). The first undecided event carrying
36
+ * one wakes the agent to decide; any other undecided event leaves its offer
37
+ * for the agent's next turn.
38
+ */
39
+ export const ATTENTION_TAGS: readonly string[] = ['chat:addressed', 'surface:turn', 'surface:alert'];
40
+
41
+ /** Accepted rules are named `suggested:<server>:<n>`: the name is the attribution. */
42
+ export const SUGGESTED_RULE_PREFIX = 'suggested:';
43
+ export const suggestedRuleName = (server: string, n: number): string => `${SUGGESTED_RULE_PREFIX}${server}:${n}`;
44
+ /** Exactly `suggested:<server>:<n>`: a server id that another extends (`games`, `games:beta`) never claims the other's rules. */
45
+ export const isSuggestedRuleOf = (name: string, server: string): boolean =>
46
+ name.startsWith(`${SUGGESTED_RULE_PREFIX}${server}:`) && /^\d+$/.test(name.slice(SUGGESTED_RULE_PREFIX.length + server.length + 1));
47
+
48
+ /** One of a server's suggestions as the host offers it: the server's words and the rule it becomes. */
49
+ export interface OfferedRule {
50
+ /** 1-based: the number `wake_accept_suggestions` takes it by. */
51
+ n: number;
52
+ /** The feature set that declared it; empty for the host's own fallback. */
53
+ featureSet: string;
54
+ match: { tagsAny?: string[]; tagsAll?: string[]; tagsNone?: string[] };
55
+ /** The gate behavior the suggestion becomes. */
56
+ behavior: GateBehavior;
57
+ /** The server's own word for it (`immediate`, `{ debounce: 60000 }`, …). */
58
+ said: unknown;
59
+ /** The server's account of what the rule is for and what declining costs. */
60
+ rationale?: string;
61
+ /** The host wrote it: the server suggested nothing it could use. */
62
+ fallback?: boolean;
63
+ }
64
+
65
+ export interface ServerOffer {
66
+ server: string;
67
+ /** What the decision is about: a digest of the server's declared ontologies ('none' without one). */
68
+ digest: string;
69
+ rules: OfferedRule[];
70
+ /** Descriptions the server gave of the tags its rules name. */
71
+ tagDescriptions: Record<string, string>;
72
+ /** Suggestions in the declaration the host could not translate, as given. */
73
+ untranslated: unknown[];
74
+ }
75
+
76
+ type Ontology = {
77
+ tags?: Record<string, unknown>;
78
+ suggestedTreatment?: unknown;
79
+ /** RFC-001 revision 1's name for suggestedTreatment; still accepted. */
80
+ defaultTreatment?: unknown;
81
+ };
82
+
83
+ const DEBOUNCE_MIN_MS = 100;
84
+ const DEBOUNCE_MAX_MS = 300_000;
85
+ const THROTTLE_DEFAULT_MS = 60_000;
86
+
87
+ /**
88
+ * A suggested behavior (RFC-001 §6's vocabulary, or the gate's own names) as a
89
+ * gate behavior; null for one with no faithful translation.
90
+ */
91
+ export function translateBehavior(said: unknown): GateBehavior | null {
92
+ if (said === 'immediate' || said === 'always') return 'always';
93
+ if (said === 'mute' || said === 'defer' || said === 'skip') return 'defer';
94
+ if (said === 'throttle') return { rate_limit: { tokens: 1, refillIntervalMs: THROTTLE_DEFAULT_MS } };
95
+ if (said && typeof said === 'object' && !Array.isArray(said)) {
96
+ const o = said as Record<string, unknown>;
97
+ if (typeof o.debounce === 'number' && Number.isFinite(o.debounce)) {
98
+ return { debounce: Math.min(DEBOUNCE_MAX_MS, Math.max(DEBOUNCE_MIN_MS, Math.round(o.debounce))) };
99
+ }
100
+ const perMs = (o.throttle as Record<string, unknown> | undefined)?.perMs;
101
+ if (typeof perMs === 'number' && perMs > 0) return { rate_limit: { tokens: 1, refillIntervalMs: Math.round(perMs) } };
102
+ const every = (o.sample as Record<string, unknown> | undefined)?.every;
103
+ if (typeof every === 'number' && Number.isInteger(every) && every > 0) return { passive_sample: { every } };
104
+ }
105
+ return null;
106
+ }
107
+
108
+ const tagList = (v: unknown): string[] | undefined => {
109
+ if (!Array.isArray(v)) return undefined;
110
+ const tags = v.filter((t): t is string => typeof t === 'string' && t.length > 0);
111
+ return tags.length > 0 ? tags : undefined;
112
+ };
113
+
114
+ /**
115
+ * A server's suggestions from its declared feature sets (null: none declared).
116
+ * Feature sets in name order; in each, its ordered rules as declared, then
117
+ * per-tag suggestions no ordered rule already makes, by tag. A server that suggests nothing the host can
118
+ * use gets one host-written rule: wake for its events tagged as addressed to
119
+ * the agent or waiting on it, nothing else.
120
+ */
121
+ export function offerFor(server: string, declared: Record<string, { tagOntology?: unknown }> | null): ServerOffer {
122
+ const rules: OfferedRule[] = [];
123
+ const tagDescriptions: Record<string, string> = {};
124
+ const untranslated: unknown[] = [];
125
+ const ontologies: Record<string, unknown> = {};
126
+ const seen = new Set<string>();
127
+ const add = (featureSet: string, match: OfferedRule['match'], said: unknown, rationale?: unknown) => {
128
+ const behavior = translateBehavior(said);
129
+ if (!behavior) { untranslated.push({ featureSet, ...match, behavior: said }); return; }
130
+ const key = stableHash({ match, behavior });
131
+ if (seen.has(key)) return;
132
+ seen.add(key);
133
+ rules.push({
134
+ n: rules.length + 1, featureSet, match, behavior, said,
135
+ ...(typeof rationale === 'string' && rationale.trim() ? { rationale: rationale.trim() } : {}),
136
+ });
137
+ };
138
+
139
+ for (const featureSet of Object.keys(declared ?? {}).sort()) {
140
+ const ontology = declared![featureSet]?.tagOntology as Ontology | undefined;
141
+ if (!ontology || typeof ontology !== 'object') continue;
142
+ ontologies[featureSet] = ontology;
143
+ const tags = ontology.tags && typeof ontology.tags === 'object' ? ontology.tags : {};
144
+ const ordered = Array.isArray(ontology.suggestedTreatment) ? ontology.suggestedTreatment
145
+ : Array.isArray(ontology.defaultTreatment) ? ontology.defaultTreatment : [];
146
+ for (const raw of ordered) {
147
+ if (!raw || typeof raw !== 'object') continue;
148
+ const r = raw as Record<string, unknown>;
149
+ const match = {
150
+ ...(tagList(r.tagsAny) ? { tagsAny: tagList(r.tagsAny) } : {}),
151
+ ...(tagList(r.tagsAll) ? { tagsAll: tagList(r.tagsAll) } : {}),
152
+ ...(tagList(r.tagsNone) ? { tagsNone: tagList(r.tagsNone) } : {}),
153
+ };
154
+ // A rule that names no tag would take every event the server sends: that is the agent's call to write, not an offer.
155
+ if (!match.tagsAny && !match.tagsAll) { untranslated.push({ featureSet, ...r }); continue; }
156
+ add(featureSet, match, r.behavior, r.rationale);
157
+ }
158
+ // Per-tag suggestions in tag order (UTF-8 bytes, as MCPL RFC-001 §5.2 and SPEC §16.5 order them):
159
+ // a JSON object's key order is no order at all.
160
+ const byTag = Object.keys(tags).sort((a, b) => Buffer.compare(Buffer.from(a), Buffer.from(b)));
161
+ for (const tag of byTag) {
162
+ const descriptor = tags[tag];
163
+ const d = descriptor as Record<string, unknown> | null;
164
+ if (!d || typeof d !== 'object') continue;
165
+ if (typeof d.desc === 'string') tagDescriptions[tag] = d.desc;
166
+ if (d.suggestedTreatment !== undefined) add(featureSet, { tagsAny: [tag] }, d.suggestedTreatment, d.rationale);
167
+ }
168
+ }
169
+
170
+ if (rules.length === 0) {
171
+ rules.push({
172
+ n: 1, featureSet: '', match: { tagsAny: [...ATTENTION_TAGS] }, behavior: 'always', said: 'immediate', fallback: true,
173
+ rationale: 'This server suggests nothing the host can use. This rule wakes you for its events marked as addressed to you or waiting on you, and leaves the rest as they are.',
174
+ });
175
+ }
176
+ const named = new Set(rules.flatMap((r) => [...(r.match.tagsAny ?? []), ...(r.match.tagsAll ?? [])]));
177
+ for (const tag of Object.keys(tagDescriptions)) if (!named.has(tag)) delete tagDescriptions[tag];
178
+ const digest = Object.keys(ontologies).length > 0 ? stableHash({ server, ontologies }) : 'none';
179
+ return { server, digest, rules, tagDescriptions, untranslated };
180
+ }
181
+
182
+ export { ruleDecides } from './rule-scope.js';
183
+
184
+ /**
185
+ * Whether the gate let an event go by without anyone having decided to: it did
186
+ * not wake and will not (`defer`/`skip`), by the default or by a rule that
187
+ * doesn't decide about it (ruleDecides). `policy` is the matched rule, as in
188
+ * force; undefined when the decision named none or it can't be found.
189
+ */
190
+ export function isUndecided(decision: GateDecision, policy: GatePolicy | undefined): boolean {
191
+ if (decision.trigger) return false;
192
+ if (decision.behavior !== 'skip' && decision.behavior !== 'defer') return false;
193
+ if (decision.policyName === null) return true;
194
+ if (decision.policyName === 'asleep' || decision.policyName === 'gate.js') return false;
195
+ return policy !== undefined && !ruleDecides(policy);
196
+ }
197
+
198
+ export const carriesAttention = (tags: readonly string[] | undefined): boolean => (tags ?? []).some((t) => ATTENTION_TAGS.includes(t));
199
+
200
+ /**
201
+ * Where an accepted rule goes: ahead of the first rule that would let the
202
+ * server's events by without deciding about them (a catch-all that defers or
203
+ * skips), so the rules the agent wrote for this server, or for a channel or
204
+ * tags, keep their place before it, and so do catch-alls that wake (a mention
205
+ * rule on metadata): a muted suggestion can't swallow an explicit mention.
206
+ * Null: there is none; the rule goes last, ahead of the default. (Accepted
207
+ * rules name their source, so they are never the catch-all.)
208
+ */
209
+ export const placementFor = (policies: readonly GatePolicy[]): string | null => firstCatchAll(policies, { lettingBy: true });
210
+
211
+ /** The gate rules accepting `rules` of `offer` writes. */
212
+ export function rulesToWrite(offer: ServerOffer, accepted: readonly OfferedRule[]): GatePolicy[] {
213
+ return accepted.map((r) => ({
214
+ name: suggestedRuleName(offer.server, r.n),
215
+ match: { source: offer.server, ...r.match },
216
+ behavior: r.behavior,
217
+ }));
218
+ }
219
+
220
+ // ---------------------------------------------------------------- decisions
221
+
222
+ /** The least time between two offers for one server (WakeDecisions.mayOffer). */
223
+ export const OFFER_FLOOR_MS = 24 * 60 * 60 * 1000;
224
+
225
+ export type DecisionState = 'offered' | 'accepted' | 'declined';
226
+ export interface DecisionRecord {
227
+ digest: string;
228
+ state: DecisionState;
229
+ /** The rule names an acceptance wrote (kept while a newer ontology is being asked about). */
230
+ rules?: string[];
231
+ /** The digest whose numbered list the agent was last shown, when newer than `digest` (a drift refusal re-showed it). */
232
+ shown?: string;
233
+ at: string;
234
+ }
235
+
236
+ /**
237
+ * The agent's decisions per server, beside gate.json (`wake-decisions.json`).
238
+ * Written whole, through a temporary file; one that can't be read is set aside
239
+ * and starts empty, so a damaged file costs a repeated offer, never a crash.
240
+ */
241
+ export class WakeDecisions {
242
+ private records: Record<string, DecisionRecord> = {};
243
+
244
+ constructor(private readonly path: string, private readonly log: (message: string) => void = () => undefined) {
245
+ if (!existsSync(path)) return;
246
+ try {
247
+ const parsed = JSON.parse(readFileSync(path, 'utf8')) as { servers?: Record<string, DecisionRecord> };
248
+ for (const [server, r] of Object.entries(parsed.servers ?? {})) {
249
+ if (r && typeof r.digest === 'string' && (r.state === 'offered' || r.state === 'accepted' || r.state === 'declined')) this.records[server] = r;
250
+ }
251
+ } catch (error) {
252
+ const aside = `${path}.unreadable-${Date.now()}`;
253
+ try { renameSync(path, aside); } catch { /* leave it */ }
254
+ this.log(`wake decisions: ${path} could not be read (${(error as Error).message}); set aside as ${aside}, starting empty`);
255
+ }
256
+ }
257
+
258
+ get(server: string): DecisionRecord | undefined { return this.records[server]; }
259
+
260
+ /**
261
+ * Whether an offer for `server` may go out now: never twice for the same
262
+ * digest, answered or not, and not within OFFER_FLOOR_MS of the server's
263
+ * last offer or decision even when its ontology changed — a server can't
264
+ * buy itself a stream of questions by editing what it declares.
265
+ */
266
+ /** Whether an offer for `server` could go out at all, before its digest is known: not within the floor of the last one. */
267
+ mayOfferSoon(server: string, now = Date.now()): boolean {
268
+ const r = this.records[server];
269
+ if (!r) return true;
270
+ const at = Date.parse(r.at);
271
+ return !Number.isFinite(at) || now - at >= OFFER_FLOOR_MS;
272
+ }
273
+
274
+ mayOffer(server: string, digest: string, now = Date.now()): boolean {
275
+ const r = this.records[server];
276
+ if (!r) return true;
277
+ if (r.digest === digest || r.shown === digest) return false;
278
+ const at = Date.parse(r.at);
279
+ return !Number.isFinite(at) || now - at >= OFFER_FLOOR_MS;
280
+ }
281
+
282
+ set(server: string, record: Omit<DecisionRecord, 'at'>, now = Date.now()): DecisionRecord {
283
+ const r: DecisionRecord = { ...record, at: new Date(now).toISOString() };
284
+ this.records[server] = r;
285
+ this.write();
286
+ return r;
287
+ }
288
+
289
+ /** Record that the agent was shown `digest`'s list, leaving the decision and its time as they are (the floor doesn't restart). */
290
+ markShown(server: string, digest: string): void {
291
+ const r = this.records[server];
292
+ if (!r) return;
293
+ this.records[server] = { ...r, shown: digest };
294
+ this.write();
295
+ }
296
+
297
+ all(): Readonly<Record<string, DecisionRecord>> { return this.records; }
298
+
299
+ private write() {
300
+ try {
301
+ mkdirSync(dirname(this.path), { recursive: true });
302
+ const tmp = `${this.path}.tmp-${process.pid}`;
303
+ writeFileSync(tmp, JSON.stringify({ servers: this.records }, null, 2) + '\n');
304
+ renameSync(tmp, this.path);
305
+ } catch (error) {
306
+ this.log(`wake decisions: could not write ${this.path}: ${(error as Error).message}`);
307
+ }
308
+ }
309
+ }
@@ -1,73 +1,62 @@
1
- import type { MessageId, MessageQuery, StoredMessage } from '@animalabs/context-manager';
1
+ import { suppressionRecordOf } from './gate/suppression-stats.js';
2
+ import type { ContextManager, MessageId, StoredMessage } from '@animalabs/context-manager';
2
3
 
3
4
  /**
4
- * The id a module holds for a write the framework holds. addMessage holds a
5
- * write while its agent's turn is alive or the host is quiesced, and stores
6
- * it at a later boundary. Until then, a module's context answers its id as a
7
- * handle: `held:` and the write's id in the deferred queue. The context's
8
- * getMessage, editMessage and removeMessage take the handle as they take a
9
- * stored id, as a process answer's edits and removals do, and once the write
10
- * lands, the handle names the stored row, which carries the write's id as
11
- * `metadata.deferredWriteId`.
5
+ * The id a release before #291 gave a module for a write it held while the
6
+ * write's agent's turn was alive: `held:` and the write's id in its deferred
7
+ * queue, which the stored row carries as `metadata.deferredWriteId` once it
8
+ * lands. This release stores every write at once and answers its stored id,
9
+ * unless a surgery's hold or a context manager refusing its work keeps the
10
+ * write in memory, where it answers '', so no write gets a handle any more.
11
+ * A module may have kept one across the upgrade: for one release the
12
+ * context's getMessage, editMessage and removeMessage still take it, and
13
+ * reach the row its write landed as (AgentFramework.landedRow), which the
14
+ * importer stores at boot for a write that release left queued.
12
15
  */
13
16
  const HELD = 'held:';
14
17
 
15
- export function heldHandle(writeId: string): MessageId {
16
- return `${HELD}${writeId}`;
17
- }
18
-
19
- /** The deferred write a handle names, or undefined for any other id. A value
20
- * that isn't a string, which only an untyped caller passes, is no handle,
21
- * and goes on to the store as it did before handles. */
18
+ /** The queued write a `held:` handle names, or undefined for any other id. A
19
+ * value that isn't a string, which only an untyped caller passes, is no
20
+ * handle, and goes on to the store as it did before handles. */
22
21
  export function heldWriteId(id: MessageId): string | undefined {
23
22
  return typeof id === 'string' && id.startsWith(HELD) ? id.slice(HELD.length) : undefined;
24
23
  }
25
24
 
26
- /**
27
- * Whether a row matches `filter` as context-manager's queryMessages matches a
28
- * stored one. That's its MessageStore's private matchesFilter, the same at
29
- * 0.14.1, 0.16.0, 0.17.1 and 0.18.0, which the package doesn't export, so the rule is copied
30
- * here, and a test compares the two on the same rows. A row matches the external source, one of the
31
- * external ids (an empty list filters nothing), the participant, and each
32
- * metadata path, dot-separated, by strict equality.
33
- */
34
- export function matchesMessageQuery(row: Pick<StoredMessage, 'participant' | 'metadata'>, filter: MessageQuery): boolean {
35
- const external = row.metadata?.external as { source?: unknown; id?: unknown } | undefined;
36
- if (filter.source !== undefined && external?.source !== filter.source) return false;
37
- if (
38
- filter.externalIds !== undefined &&
39
- filter.externalIds.length > 0 &&
40
- (!external?.id || !filter.externalIds.includes(external.id as string))
41
- ) {
42
- return false;
43
- }
44
- if (filter.participant !== undefined && row.participant !== filter.participant) return false;
45
- if (filter.metadata !== undefined) {
46
- for (const [path, value] of Object.entries(filter.metadata)) {
47
- if (valueAt(row.metadata, path) !== value) return false;
48
- }
49
- }
50
- return true;
51
- }
52
-
53
- function valueAt(value: unknown, path: string): unknown {
54
- for (const part of path.split('.')) {
55
- if (value === undefined || value === null || typeof value !== 'object') return undefined;
56
- value = (value as Record<string, unknown>)[part];
57
- }
58
- return value;
59
- }
25
+ /** What asModuleRow reads of the window of the agent a read is for. */
26
+ export type ModuleRowWindow = Pick<ContextManager, 'windowMode' | 'ordinalOf'>;
60
27
 
61
28
  /**
62
- * A row as a module's context answers it: marked `heldFromResidents` when it
63
- * carries the stamp a tune-out puts on a message it holds from the residents
64
- * (`metadata.tuneOut`). Every resident's viewFilter leaves a stamped row out
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
- * (ModuleMessage.heldFromResidents says where its text can still reach
69
- * one). Any other row comes back as it is.
29
+ * A row as a module's context answers it, for the agent whose store it was
30
+ * read from:
31
+ *
32
+ * - `heldFromResidents` when it carries the stamp a tune-out puts on a
33
+ * message it holds from the residents (`metadata.tuneOut`). Every
34
+ * resident's viewFilter leaves a stamped row out of its compiled view
35
+ * (framework.ts, createAgent), and a tool boundary never presents one to a
36
+ * resident, so a marked row is one the store keeps and no resident is
37
+ * shown (ModuleMessage.heldFromResidents says where its text can still
38
+ * reach one). So is a row a suppress rule hid (`metadata.suppressed`),
39
+ * which no view shows either, nor any window presents (one stored before
40
+ * audience stamps stays in a window written down from the record; the
41
+ * view still leaves it out).
42
+ * - `held` when that agent's window (`window`) presents rows and doesn't
43
+ * hold it (#291): stored and not yet presented to the agent, which its
44
+ * next presentation point does, or taken out of it as an undone turn's,
45
+ * or one it is never presented, as a held row is for a resident. Never in
46
+ * legacy mode, where the window is the record. A resident's window
47
+ * written down from the record holds the held rows an earlier release
48
+ * stored, unstamped, so those read without `held`: `heldFromResidents`
49
+ * marks every held row.
50
+ *
51
+ * Any other row comes back as it is.
70
52
  */
71
- export function asModuleRow<T extends Pick<StoredMessage, 'metadata'>>(row: T): T & { heldFromResidents?: true } {
72
- return (row.metadata as { tuneOut?: unknown } | undefined)?.tuneOut ? { ...row, heldFromResidents: true } : row;
53
+ export function asModuleRow<T extends Pick<StoredMessage, 'id' | 'metadata'>>(
54
+ row: T,
55
+ window?: ModuleRowWindow,
56
+ ): T & { heldFromResidents?: true; held?: true } {
57
+ const metadata = row.metadata as Record<string, unknown> | undefined;
58
+ const fromResidents = !!metadata?.tuneOut || suppressionRecordOf(metadata) !== undefined;
59
+ const held = window !== undefined && window.windowMode() === 'presented' && window.ordinalOf(row.id) === undefined;
60
+ if (!fromResidents && !held) return row;
61
+ return { ...row, ...(fromResidents ? { heldFromResidents: true as const } : {}), ...(held ? { held: true as const } : {}) };
73
62
  }
package/src/index.ts CHANGED
@@ -8,6 +8,13 @@ export { Agent } from './agent.js';
8
8
  export { FLAT_MINIMUM_MS, type InferenceBoundsOptions } from './inference-bounds.js';
9
9
  export { storedInjectionMarkOf, type StoredInjectionMark } from './stored-injections.js';
10
10
  export type { StartStreamResult } from './agent.js';
11
+ export type { PresentationOccasion } from './presentation.js';
12
+ export { covers, coverTest, describeReadMark, markAtOrAfter, type Cover, type MarkCm, type MarkedRow, type ReadMark } from './read-marks.js';
13
+ // How a resident's context and the subconscious's are opened (#291), for a host that opens one itself.
14
+ export {
15
+ residentContextOptions, residentViewFilter, subconsciousContextOptions, subconsciousViewFilter,
16
+ type ContextManagerOpenOptions,
17
+ } from './context-options.js';
11
18
  export { ProcessQueueImpl } from './queue.js';
12
19
  export { ModuleRegistry } from './module-registry.js';
13
20
  export { ShutdownTimeoutError, DEFAULT_SHUTDOWN_TIMEOUT_MS } from './shutdown.js';
@@ -73,9 +80,9 @@ export type { SessionUsage, AgentUsage, SessionUsageSnapshot, UsageUpdatedEvent
73
80
 
74
81
  // EventGate
75
82
  export { EventGate, MCPL_GATE_EVENT_TYPES, wakeLineText, type EventTypesFrom, type WakeProvenance, type WritePlacement } from './gate/index.js';
76
- export type { GateConfig, GateOptions, GatePolicy, GatePolicyMatch, GateBehavior, WakeLine } from './gate/index.js';
83
+ export type { GateConfig, GateOptions, GatePolicy, GatePolicyMatch, GateBehavior, WakeLine, SuppressConfig, SuppressionReviewOptions } from './gate/index.js';
77
84
  export { ReplyJudge, DEFAULT_JUDGE_MODEL, DEFAULT_JUDGE_MAX_INPUT_TOKENS, MIN_JUDGE_INPUT_TOKENS } from './gate/index.js';
78
- export type { JudgeConfig, JudgeEventSummary, JudgeFn, JudgeRequest, JudgeVerdict, ReplyJudgeOptions, ReplyJudgeDeps, UpcomingWake } from './gate/index.js';
85
+ export type { JudgeConfig, JudgeEventSummary, JudgeFn, JudgeRequest, JudgeVerdict, ReplyJudgeOptions, ReplyJudgeDeps, UpcomingWake, UpcomingWakeKind } from './gate/index.js';
79
86
  export { Heartbeat, DEFAULT_HEARTBEAT, HEARTBEAT_TOOL_PREFIX } from './heartbeat/heartbeat.js';
80
87
  export type { HeartbeatOptions, HeartbeatSettings, HeartbeatWake, HeartbeatDeliveryMode, HeartbeatScheduledWake, Reminder } from './heartbeat/heartbeat.js';
81
88
 
@@ -0,0 +1,81 @@
1
+ /**
2
+ * A JSON-RPC 2.0 message's envelope, read once where a message arrives, by
3
+ * each of the framework's endpoints that answers requests: the MCPL
4
+ * connection, for its servers' requests, and the MCP server
5
+ * (src/api/mcp-server-core.ts), for its client's.
6
+ *
7
+ * A message with an id member and no method is a response, whatever the
8
+ * id's value: a peer answers a request it couldn't read with id null (§5),
9
+ * and taking that answer for a request would answer it in turn. So is an
10
+ * error with no id, which MCP lets a peer send for a request it couldn't
11
+ * read (MCP 2025-11-25, Basic, Error Responses), but only from a message
12
+ * whose jsonrpc member is "2.0": a peer's own JSON log lines, which an MCPL
13
+ * server's stdout can carry, can have an error key. Any other object with
14
+ * neither a method nor an id isn't JSON-RPC: the MCP server answers it
15
+ * -32600 with id null, as it does a value that isn't an object, and the MCPL
16
+ * connection, whose server's stdout carries other output, ignores it. A
17
+ * message with a method is a request when it has an id member, whatever its
18
+ * value, and a notification when it has none (§4.1), so `"id": null` is a
19
+ * request. MCP says a request's id MUST NOT be null (2025-11-25, Basic); one
20
+ * is served anyway, as JSON-RPC allows, since its sender waits for the
21
+ * answer. A request's method must be a string and its id a string, a number
22
+ * or null (§4). A message that breaks either isn't a Request object, and so
23
+ * isn't a notification either, with or without an id: the endpoints answer
24
+ * it once with -32600, carrying its id where that's a string or a number and
25
+ * null otherwise (§5, and the spec's own example of an invalid Request
26
+ * object, which has no id), and don't dispatch it. The MCPL handshake, which
27
+ * answers only pings, ignores it, as it ignores every other request then.
28
+ * Apart from the error with no id, the `jsonrpc` member isn't checked: a
29
+ * peer that leaves it out is served, as before.
30
+ */
31
+
32
+ /** A JSON value's kind, as a log line or an error names what a peer sent
33
+ * without repeating it: "null", "an array", "an object", "a number". */
34
+ export function jsonKind(value: unknown): string {
35
+ if (value === null) return 'null';
36
+ const kind = Array.isArray(value) ? 'array' : typeof value;
37
+ return `${/^[aeiou]/.test(kind) ? 'an' : 'a'} ${kind}`;
38
+ }
39
+
40
+ export type JsonRpcEnvelope =
41
+ /** An id member and no method, or a JSON-RPC 2.0 error with neither: an
42
+ * answer, which the endpoint matches to one of its own requests by the
43
+ * id where it can. */
44
+ | { kind: 'response' }
45
+ /** Neither a method nor an id member, and not such an error: not a
46
+ * JSON-RPC message. */
47
+ | { kind: 'neither' }
48
+ | { kind: 'request'; method: string; id: string | number | null }
49
+ | { kind: 'notification'; method: string }
50
+ /** Not a Request object, with or without an id: answered with `error`,
51
+ * carrying `id`, and not dispatched. `method` is its method where that's
52
+ * a string, for a log line. */
53
+ | { kind: 'invalid'; id: string | number | null; error: { code: -32600; message: string }; method?: string; problems: string };
54
+
55
+ /** Read a parsed message's envelope. What to do with a value that isn't an
56
+ * object is the endpoint's own call. */
57
+ export function readEnvelope(msg: Record<string, unknown>): JsonRpcEnvelope {
58
+ if (!Object.hasOwn(msg, 'method')) {
59
+ const answers = Object.hasOwn(msg, 'id') || (msg.jsonrpc === '2.0' && Object.hasOwn(msg, 'error'));
60
+ return { kind: answers ? 'response' : 'neither' };
61
+ }
62
+ const isRequest = Object.hasOwn(msg, 'id');
63
+ const { method, id } = msg;
64
+ const problems = [
65
+ ...(typeof method === 'string' ? [] : [`the method must be a string, not ${jsonKind(method)}`]),
66
+ ...(!isRequest || typeof id === 'string' || typeof id === 'number' || id === null
67
+ ? [] : [`the id must be a string, a number or null, not ${jsonKind(id)}`]),
68
+ ].join('; ');
69
+ if (problems) {
70
+ return {
71
+ kind: 'invalid',
72
+ id: typeof id === 'string' || typeof id === 'number' ? id : null,
73
+ error: { code: -32600, message: `Invalid Request: ${problems}` },
74
+ ...(typeof method === 'string' ? { method } : {}),
75
+ problems,
76
+ };
77
+ }
78
+ return isRequest
79
+ ? { kind: 'request', method: method as string, id: id as string | number | null }
80
+ : { kind: 'notification', method: method as string };
81
+ }