@animalabs/agent-framework 0.26.1 → 0.27.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 (276) hide show
  1. package/README.md +57 -7
  2. package/dist/src/agent.d.ts +263 -22
  3. package/dist/src/agent.d.ts.map +1 -1
  4. package/dist/src/agent.js +531 -54
  5. package/dist/src/agent.js.map +1 -1
  6. package/dist/src/api/server.d.ts.map +1 -1
  7. package/dist/src/api/server.js +1 -2
  8. package/dist/src/api/server.js.map +1 -1
  9. package/dist/src/auto-continue.d.ts +33 -0
  10. package/dist/src/auto-continue.d.ts.map +1 -0
  11. package/dist/src/auto-continue.js +35 -0
  12. package/dist/src/auto-continue.js.map +1 -0
  13. package/dist/src/code-execution/isolate/scripts.d.ts +11 -0
  14. package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -1
  15. package/dist/src/code-execution/isolate/scripts.js +5 -1
  16. package/dist/src/code-execution/isolate/scripts.js.map +1 -1
  17. package/dist/src/framework.d.ts +829 -156
  18. package/dist/src/framework.d.ts.map +1 -1
  19. package/dist/src/framework.js +3906 -1008
  20. package/dist/src/framework.js.map +1 -1
  21. package/dist/src/gate/event-gate.d.ts +309 -46
  22. package/dist/src/gate/event-gate.d.ts.map +1 -1
  23. package/dist/src/gate/event-gate.js +2117 -395
  24. package/dist/src/gate/event-gate.js.map +1 -1
  25. package/dist/src/gate/index.d.ts +2 -2
  26. package/dist/src/gate/index.d.ts.map +1 -1
  27. package/dist/src/gate/index.js +1 -1
  28. package/dist/src/gate/index.js.map +1 -1
  29. package/dist/src/gate/recipe-reconcile.d.ts +103 -0
  30. package/dist/src/gate/recipe-reconcile.d.ts.map +1 -0
  31. package/dist/src/gate/recipe-reconcile.js +325 -0
  32. package/dist/src/gate/recipe-reconcile.js.map +1 -0
  33. package/dist/src/gate/types.d.ts +68 -8
  34. package/dist/src/gate/types.d.ts.map +1 -1
  35. package/dist/src/index.d.ts +6 -4
  36. package/dist/src/index.d.ts.map +1 -1
  37. package/dist/src/index.js +5 -2
  38. package/dist/src/index.js.map +1 -1
  39. package/dist/src/mcpl/channel-registry.d.ts +433 -113
  40. package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
  41. package/dist/src/mcpl/channel-registry.js +1152 -434
  42. package/dist/src/mcpl/channel-registry.js.map +1 -1
  43. package/dist/src/mcpl/conversation-router.d.ts +16 -8
  44. package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
  45. package/dist/src/mcpl/conversation-router.js +29 -15
  46. package/dist/src/mcpl/conversation-router.js.map +1 -1
  47. package/dist/src/mcpl/convert-block.d.ts.map +1 -1
  48. package/dist/src/mcpl/convert-block.js +14 -2
  49. package/dist/src/mcpl/convert-block.js.map +1 -1
  50. package/dist/src/mcpl/hook-orchestrator.d.ts +7 -1
  51. package/dist/src/mcpl/hook-orchestrator.d.ts.map +1 -1
  52. package/dist/src/mcpl/hook-orchestrator.js +32 -7
  53. package/dist/src/mcpl/hook-orchestrator.js.map +1 -1
  54. package/dist/src/mcpl/index.d.ts +5 -1
  55. package/dist/src/mcpl/index.d.ts.map +1 -1
  56. package/dist/src/mcpl/index.js +4 -0
  57. package/dist/src/mcpl/index.js.map +1 -1
  58. package/dist/src/mcpl/inference-router.d.ts +14 -2
  59. package/dist/src/mcpl/inference-router.d.ts.map +1 -1
  60. package/dist/src/mcpl/inference-router.js +73 -28
  61. package/dist/src/mcpl/inference-router.js.map +1 -1
  62. package/dist/src/mcpl/prose-grammar.d.ts +4 -0
  63. package/dist/src/mcpl/prose-grammar.d.ts.map +1 -1
  64. package/dist/src/mcpl/prose-grammar.js +6 -0
  65. package/dist/src/mcpl/prose-grammar.js.map +1 -1
  66. package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
  67. package/dist/src/mcpl/protocol-family.js +3 -0
  68. package/dist/src/mcpl/protocol-family.js.map +1 -1
  69. package/dist/src/mcpl/push-coalescer.d.ts +20 -2
  70. package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
  71. package/dist/src/mcpl/push-coalescer.js +33 -11
  72. package/dist/src/mcpl/push-coalescer.js.map +1 -1
  73. package/dist/src/mcpl/reference-fetcher.d.ts +63 -42
  74. package/dist/src/mcpl/reference-fetcher.d.ts.map +1 -1
  75. package/dist/src/mcpl/reference-fetcher.js +177 -145
  76. package/dist/src/mcpl/reference-fetcher.js.map +1 -1
  77. package/dist/src/mcpl/references.d.ts +380 -46
  78. package/dist/src/mcpl/references.d.ts.map +1 -1
  79. package/dist/src/mcpl/references.js +737 -78
  80. package/dist/src/mcpl/references.js.map +1 -1
  81. package/dist/src/mcpl/server-connection.d.ts +82 -3
  82. package/dist/src/mcpl/server-connection.d.ts.map +1 -1
  83. package/dist/src/mcpl/server-connection.js +132 -7
  84. package/dist/src/mcpl/server-connection.js.map +1 -1
  85. package/dist/src/mcpl/tool-lifecycle.d.ts +11 -0
  86. package/dist/src/mcpl/tool-lifecycle.d.ts.map +1 -1
  87. package/dist/src/mcpl/tool-lifecycle.js +21 -0
  88. package/dist/src/mcpl/tool-lifecycle.js.map +1 -1
  89. package/dist/src/mcpl/tool-presence.d.ts +259 -0
  90. package/dist/src/mcpl/tool-presence.d.ts.map +1 -0
  91. package/dist/src/mcpl/tool-presence.js +628 -0
  92. package/dist/src/mcpl/tool-presence.js.map +1 -0
  93. package/dist/src/mcpl/tool-result-normalize.d.ts +9 -3
  94. package/dist/src/mcpl/tool-result-normalize.d.ts.map +1 -1
  95. package/dist/src/mcpl/tool-result-normalize.js +14 -6
  96. package/dist/src/mcpl/tool-result-normalize.js.map +1 -1
  97. package/dist/src/mcpl/tool-signature.d.ts +39 -0
  98. package/dist/src/mcpl/tool-signature.d.ts.map +1 -0
  99. package/dist/src/mcpl/tool-signature.js +292 -0
  100. package/dist/src/mcpl/tool-signature.js.map +1 -0
  101. package/dist/src/mcpl/toolset-host.d.ts +200 -0
  102. package/dist/src/mcpl/toolset-host.d.ts.map +1 -0
  103. package/dist/src/mcpl/toolset-host.js +621 -0
  104. package/dist/src/mcpl/toolset-host.js.map +1 -0
  105. package/dist/src/mcpl/toolsets-module.d.ts +80 -0
  106. package/dist/src/mcpl/toolsets-module.d.ts.map +1 -0
  107. package/dist/src/mcpl/toolsets-module.js +431 -0
  108. package/dist/src/mcpl/toolsets-module.js.map +1 -0
  109. package/dist/src/mcpl/transport.d.ts +2 -2
  110. package/dist/src/mcpl/transport.d.ts.map +1 -1
  111. package/dist/src/mcpl/transport.js +3 -2
  112. package/dist/src/mcpl/transport.js.map +1 -1
  113. package/dist/src/mcpl/types.d.ts +21 -1
  114. package/dist/src/mcpl/types.d.ts.map +1 -1
  115. package/dist/src/mcpl/types.js.map +1 -1
  116. package/dist/src/mcpl/visible-content.d.ts +7 -9
  117. package/dist/src/mcpl/visible-content.d.ts.map +1 -1
  118. package/dist/src/mcpl/visible-content.js +21 -9
  119. package/dist/src/mcpl/visible-content.js.map +1 -1
  120. package/dist/src/module-registry.d.ts +23 -8
  121. package/dist/src/module-registry.d.ts.map +1 -1
  122. package/dist/src/module-registry.js +68 -15
  123. package/dist/src/module-registry.js.map +1 -1
  124. package/dist/src/modules/discord/index.js +1 -1
  125. package/dist/src/modules/discord/index.js.map +1 -1
  126. package/dist/src/modules/history/index.d.ts +2 -1
  127. package/dist/src/modules/history/index.d.ts.map +1 -1
  128. package/dist/src/modules/history/index.js +51 -17
  129. package/dist/src/modules/history/index.js.map +1 -1
  130. package/dist/src/modules/history/semantic.d.ts +108 -6
  131. package/dist/src/modules/history/semantic.d.ts.map +1 -1
  132. package/dist/src/modules/history/semantic.js +273 -57
  133. package/dist/src/modules/history/semantic.js.map +1 -1
  134. package/dist/src/modules/workspace/direct-write.d.ts +181 -0
  135. package/dist/src/modules/workspace/direct-write.d.ts.map +1 -0
  136. package/dist/src/modules/workspace/direct-write.js +384 -0
  137. package/dist/src/modules/workspace/direct-write.js.map +1 -0
  138. package/dist/src/modules/workspace/edit-text.d.ts +26 -12
  139. package/dist/src/modules/workspace/edit-text.d.ts.map +1 -1
  140. package/dist/src/modules/workspace/edit-text.js +58 -28
  141. package/dist/src/modules/workspace/edit-text.js.map +1 -1
  142. package/dist/src/modules/workspace/effects.d.ts +10 -0
  143. package/dist/src/modules/workspace/effects.d.ts.map +1 -1
  144. package/dist/src/modules/workspace/effects.js +1 -1
  145. package/dist/src/modules/workspace/effects.js.map +1 -1
  146. package/dist/src/modules/workspace/index.d.ts +246 -27
  147. package/dist/src/modules/workspace/index.d.ts.map +1 -1
  148. package/dist/src/modules/workspace/index.js +1216 -236
  149. package/dist/src/modules/workspace/index.js.map +1 -1
  150. package/dist/src/modules/workspace/read-page.d.ts +41 -0
  151. package/dist/src/modules/workspace/read-page.d.ts.map +1 -0
  152. package/dist/src/modules/workspace/read-page.js +213 -0
  153. package/dist/src/modules/workspace/read-page.js.map +1 -0
  154. package/dist/src/modules/workspace/result-text.d.ts +67 -0
  155. package/dist/src/modules/workspace/result-text.d.ts.map +1 -0
  156. package/dist/src/modules/workspace/result-text.js +94 -0
  157. package/dist/src/modules/workspace/result-text.js.map +1 -0
  158. package/dist/src/modules/workspace/text.d.ts +29 -12
  159. package/dist/src/modules/workspace/text.d.ts.map +1 -1
  160. package/dist/src/modules/workspace/text.js +35 -33
  161. package/dist/src/modules/workspace/text.js.map +1 -1
  162. package/dist/src/modules/workspace/types.d.ts +41 -17
  163. package/dist/src/modules/workspace/types.d.ts.map +1 -1
  164. package/dist/src/modules/workspace/types.js.map +1 -1
  165. package/dist/src/sent-rows.d.ts +59 -0
  166. package/dist/src/sent-rows.d.ts.map +1 -0
  167. package/dist/src/sent-rows.js +145 -0
  168. package/dist/src/sent-rows.js.map +1 -0
  169. package/dist/src/silent-heartbeat.d.ts +17 -10
  170. package/dist/src/silent-heartbeat.d.ts.map +1 -1
  171. package/dist/src/silent-heartbeat.js.map +1 -1
  172. package/dist/src/stable-hash.d.ts +3 -0
  173. package/dist/src/stable-hash.d.ts.map +1 -0
  174. package/dist/src/stable-hash.js +15 -0
  175. package/dist/src/stable-hash.js.map +1 -0
  176. package/dist/src/stored-injections.d.ts +99 -0
  177. package/dist/src/stored-injections.d.ts.map +1 -0
  178. package/dist/src/stored-injections.js +106 -0
  179. package/dist/src/stored-injections.js.map +1 -0
  180. package/dist/src/text-as-stored.d.ts +4 -3
  181. package/dist/src/text-as-stored.d.ts.map +1 -1
  182. package/dist/src/text-as-stored.js +4 -3
  183. package/dist/src/text-as-stored.js.map +1 -1
  184. package/dist/src/thinking-drops.d.ts +81 -0
  185. package/dist/src/thinking-drops.d.ts.map +1 -0
  186. package/dist/src/thinking-drops.js +103 -0
  187. package/dist/src/thinking-drops.js.map +1 -0
  188. package/dist/src/tool-image-ledger.d.ts +9 -8
  189. package/dist/src/tool-image-ledger.d.ts.map +1 -1
  190. package/dist/src/tool-image-ledger.js +9 -8
  191. package/dist/src/tool-image-ledger.js.map +1 -1
  192. package/dist/src/tool-presentation.d.ts.map +1 -1
  193. package/dist/src/tool-presentation.js +3 -1
  194. package/dist/src/tool-presentation.js.map +1 -1
  195. package/dist/src/tool-result-guard.d.ts +4 -2
  196. package/dist/src/tool-result-guard.d.ts.map +1 -1
  197. package/dist/src/tool-result-guard.js +21 -6
  198. package/dist/src/tool-result-guard.js.map +1 -1
  199. package/dist/src/tool-result-history.d.ts +186 -21
  200. package/dist/src/tool-result-history.d.ts.map +1 -1
  201. package/dist/src/tool-result-history.js +428 -89
  202. package/dist/src/tool-result-history.js.map +1 -1
  203. package/dist/src/tune-out/coordinator.d.ts +17 -9
  204. package/dist/src/tune-out/coordinator.d.ts.map +1 -1
  205. package/dist/src/tune-out/coordinator.js +84 -37
  206. package/dist/src/tune-out/coordinator.js.map +1 -1
  207. package/dist/src/types/agent.d.ts +186 -7
  208. package/dist/src/types/agent.d.ts.map +1 -1
  209. package/dist/src/types/events.d.ts +19 -2
  210. package/dist/src/types/events.d.ts.map +1 -1
  211. package/dist/src/types/framework.d.ts +24 -3
  212. package/dist/src/types/framework.d.ts.map +1 -1
  213. package/dist/src/types/module.d.ts +90 -7
  214. package/dist/src/types/module.d.ts.map +1 -1
  215. package/dist/src/types/trace.d.ts +57 -2
  216. package/dist/src/types/trace.d.ts.map +1 -1
  217. package/package.json +4 -3
  218. package/src/agent.ts +593 -60
  219. package/src/api/server.ts +1 -2
  220. package/src/auto-continue.ts +42 -0
  221. package/src/code-execution/isolate/scripts.ts +11 -3
  222. package/src/framework.ts +4004 -1026
  223. package/src/gate/event-gate.ts +2315 -389
  224. package/src/gate/index.ts +2 -1
  225. package/src/gate/recipe-reconcile.ts +355 -0
  226. package/src/gate/types.ts +70 -8
  227. package/src/index.ts +7 -4
  228. package/src/mcpl/channel-registry.ts +1220 -467
  229. package/src/mcpl/conversation-router.ts +34 -16
  230. package/src/mcpl/convert-block.ts +14 -1
  231. package/src/mcpl/hook-orchestrator.ts +33 -6
  232. package/src/mcpl/index.ts +6 -1
  233. package/src/mcpl/inference-router.ts +79 -34
  234. package/src/mcpl/prose-grammar.ts +7 -0
  235. package/src/mcpl/protocol-family.ts +2 -0
  236. package/src/mcpl/push-coalescer.ts +51 -13
  237. package/src/mcpl/reference-fetcher.ts +201 -162
  238. package/src/mcpl/references.ts +852 -78
  239. package/src/mcpl/server-connection.ts +145 -7
  240. package/src/mcpl/tool-lifecycle.ts +21 -0
  241. package/src/mcpl/tool-presence.ts +650 -0
  242. package/src/mcpl/tool-result-normalize.ts +18 -6
  243. package/src/mcpl/tool-signature.ts +285 -0
  244. package/src/mcpl/toolset-host.ts +637 -0
  245. package/src/mcpl/toolsets-module.ts +451 -0
  246. package/src/mcpl/transport.ts +3 -2
  247. package/src/mcpl/types.ts +22 -1
  248. package/src/mcpl/visible-content.ts +22 -9
  249. package/src/module-registry.ts +77 -24
  250. package/src/modules/discord/index.ts +1 -1
  251. package/src/modules/history/index.ts +48 -18
  252. package/src/modules/history/semantic.ts +308 -54
  253. package/src/modules/workspace/direct-write.ts +425 -0
  254. package/src/modules/workspace/edit-text.ts +82 -35
  255. package/src/modules/workspace/effects.ts +1 -1
  256. package/src/modules/workspace/index.ts +1274 -241
  257. package/src/modules/workspace/read-page.ts +222 -0
  258. package/src/modules/workspace/result-text.ts +119 -0
  259. package/src/modules/workspace/text.ts +58 -32
  260. package/src/modules/workspace/types.ts +41 -17
  261. package/src/sent-rows.ts +162 -0
  262. package/src/silent-heartbeat.ts +17 -10
  263. package/src/stable-hash.ts +14 -0
  264. package/src/stored-injections.ts +131 -0
  265. package/src/text-as-stored.ts +4 -3
  266. package/src/thinking-drops.ts +141 -0
  267. package/src/tool-image-ledger.ts +9 -8
  268. package/src/tool-presentation.ts +3 -1
  269. package/src/tool-result-guard.ts +19 -6
  270. package/src/tool-result-history.ts +488 -88
  271. package/src/tune-out/coordinator.ts +80 -37
  272. package/src/types/agent.ts +189 -7
  273. package/src/types/events.ts +19 -2
  274. package/src/types/framework.ts +21 -3
  275. package/src/types/module.ts +86 -7
  276. package/src/types/trace.ts +58 -2
package/src/gate/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { EventGate, wakeProvenance, type WakeProvenance } from './event-gate.js';
1
+ export { EventGate, wakeProvenance, wakeLineText, type WakeProvenance, type WritePlacement } from './event-gate.js';
2
2
  export type {
3
3
  GateConfig,
4
4
  GateOptions,
@@ -9,4 +9,5 @@ export type {
9
9
  GateEventInfo,
10
10
  GateStatus,
11
11
  GatePolicyStats,
12
+ WakeLine,
12
13
  } from './types.js';
@@ -0,0 +1,355 @@
1
+ /**
2
+ * What a start does to an existing gate.json, given the recipe's gate config.
3
+ *
4
+ * The gate keeps the recipe it last applied in a file of its own beside
5
+ * gate.json (`gate.recipe.json` for `gate.json`), written by the gate. A start
6
+ * merges the recipe into gate.json three ways, rule by rule, against it:
7
+ * - a rule as the last recipe left it follows the new recipe: it takes the
8
+ * new definition, or goes when the new recipe no longer declares it, so a
9
+ * renamed rule goes under its old name and comes in under its new one;
10
+ * - a rule edited since the last recipe keeps its definition, unless it now
11
+ * agrees with the recipe's;
12
+ * - a rule both recipes declare, removed from gate.json since, comes back;
13
+ * - a rule no recipe declared (added at run time, or by hand) stays, and if
14
+ * the recipe now declares it as it stands, it's the recipe's from then on;
15
+ * - a rule new in the recipe comes in.
16
+ *
17
+ * Order is an edit of its own, apart from a rule's definition. When gate.json
18
+ * holds the rules both recipes declare in the last recipe's order, the recipe's
19
+ * rules take the new recipe's order, in the places gate.json gives them, and
20
+ * every other rule keeps its place. When one of them was moved since
21
+ * (wake_add_rule's position moves one), gate.json's order stays. Either way, a
22
+ * recipe rule new to gate.json goes before the recipe rule that follows it
23
+ * there, or after the one before it when none follows.
24
+ * `default` follows the same rule as a definition.
25
+ *
26
+ * Without a last recipe, as at the first start after this change, nothing is
27
+ * removed or redefined: the rules that match the recipe by name and definition
28
+ * take its order, every other rule stays, and the line names those that differ
29
+ * from the recipe or aren't in it. A rule that differs can be removed, and the
30
+ * next start restores it from the recipe.
31
+ *
32
+ * Rules are compared by what they mean and kept as written. Two rules agree
33
+ * when the gate reads them the same, so a rule `wake_add_rule` wrote as the
34
+ * gate reads it agrees with the recipe's rule as written, and a change to a
35
+ * field the gate doesn't read alone isn't an edit; the caller gives the
36
+ * meaning (`meaningOf`). A rule the gate leaves out has no reading, so it is
37
+ * compared as written, a change to any of its fields an edit. A rule that agrees with the recipe stays as gate.json
38
+ * holds it, and one that follows the recipe takes the recipe's rule as
39
+ * written. A rule with no name stays where gate.json holds it, and a recipe's
40
+ * rule with no name is never merged: the merge keys rules by name.
41
+ */
42
+
43
+ /** What a start changed, by rule name, for its trace and its log line. */
44
+ export interface RecipeChanges {
45
+ added: string[];
46
+ removed: string[];
47
+ updated: string[];
48
+ restored: string[];
49
+ moved: string[];
50
+ /** Edited since the last recipe, and kept, though the new recipe changed or dropped it. */
51
+ keptEdited: string[];
52
+ /** Kept, though the recipe declares the name with another definition. */
53
+ differsFromRecipe: string[];
54
+ /** Kept, and in no recipe: named at a start without a last recipe only. */
55
+ outsideRecipe: string[];
56
+ /** gate.json's order was kept, though the recipe changed its order, since a recipe rule was moved in it. */
57
+ orderKept: boolean;
58
+ defaultChange?: { from: string; to: string };
59
+ /** The live `default`, kept as edited, though the recipe changed it. */
60
+ defaultKept?: string;
61
+ /** At a start without a last recipe: the live `default`, kept, and the recipe's, which differs. */
62
+ defaultDiffers?: { kept: string; recipe: string };
63
+ /** Why the recorded recipe couldn't be read, when it exists and doesn't read. */
64
+ recordUnreadable?: string;
65
+ }
66
+
67
+ /** The file beside gate.json that holds the recipe the gate last applied. */
68
+ export function lastRecipePath(configPath: string): string {
69
+ return `${configPath.replace(/\.json$/, '')}.recipe.json`;
70
+ }
71
+
72
+ function sortKeys(value: unknown): unknown {
73
+ if (Array.isArray(value)) return value.map(sortKeys);
74
+ if (value && typeof value === 'object') {
75
+ // No prototype, so a key such as "__proto__", which JSON.parse gives as an
76
+ // own field, is copied as one rather than setting the prototype.
77
+ const out: Record<string, unknown> = Object.create(null);
78
+ for (const key of Object.keys(value as Record<string, unknown>).sort()) {
79
+ out[key] = sortKeys((value as Record<string, unknown>)[key]);
80
+ }
81
+ return out;
82
+ }
83
+ return value;
84
+ }
85
+
86
+ /** A config or policy as text that doesn't depend on its keys' order. */
87
+ export function canonical(value: unknown): string {
88
+ return JSON.stringify(sortKeys(value));
89
+ }
90
+
91
+ const same = (a: unknown, b: unknown): boolean => canonical(a) === canonical(b);
92
+
93
+ /** Changes with nothing in them, for a start to fill. */
94
+ export function noChanges(): RecipeChanges {
95
+ return {
96
+ added: [], removed: [], updated: [], restored: [], moved: [],
97
+ keptEdited: [], differsFromRecipe: [], outsideRecipe: [], orderKept: false,
98
+ };
99
+ }
100
+
101
+ /** A gate config as written: its rule list and its `default`, each as given. */
102
+ export interface WrittenConfig { rules: readonly unknown[]; default: unknown }
103
+
104
+ /** A rule's name as written, when it has a usable one. */
105
+ export function ruleName(rule: unknown): string | undefined {
106
+ if (!rule || typeof rule !== 'object' || Array.isArray(rule)) return undefined;
107
+ const name = (rule as { name?: unknown }).name;
108
+ return typeof name === 'string' && name ? name : undefined;
109
+ }
110
+
111
+ /** A rule name the list gives more than once, if any: the merge keys rules by name. */
112
+ export function duplicateName(rules: readonly unknown[]): string | undefined {
113
+ const seen = new Set<string>();
114
+ for (const rule of rules) {
115
+ const name = ruleName(rule);
116
+ if (name === undefined) continue;
117
+ if (seen.has(name)) return name;
118
+ seen.add(name);
119
+ }
120
+ return undefined;
121
+ }
122
+
123
+ /** A `default` as the gate reads one: left out, null or "" is "always". */
124
+ function behaviorOf(value: unknown): unknown {
125
+ return value === undefined || value === null || value === '' ? 'always' : value;
126
+ }
127
+
128
+ /** The names in one longest sequence that `a` and `b` share in order. */
129
+ function longestShared(a: string[], b: string[]): Set<string> {
130
+ const rows = a.length + 1;
131
+ const cols = b.length + 1;
132
+ const table: number[][] = Array.from({ length: rows }, () => new Array<number>(cols).fill(0));
133
+ for (let i = a.length - 1; i >= 0; i--) {
134
+ for (let j = b.length - 1; j >= 0; j--) {
135
+ table[i][j] = a[i] === b[j] ? table[i + 1][j + 1] + 1 : Math.max(table[i + 1][j], table[i][j + 1]);
136
+ }
137
+ }
138
+ const shared = new Set<string>();
139
+ let i = 0;
140
+ let j = 0;
141
+ while (i < a.length && j < b.length) {
142
+ if (a[i] === b[j]) {
143
+ shared.add(a[i]);
144
+ i++;
145
+ j++;
146
+ } else if (table[i + 1][j] > table[i][j + 1]) {
147
+ i++;
148
+ } else {
149
+ // On a tie, the result's rule is the one named as moved: the rule a
150
+ // swap brought earlier, such as one the old appending left behind a
151
+ // catch-all.
152
+ j++;
153
+ }
154
+ }
155
+ return shared;
156
+ }
157
+
158
+ /**
159
+ * Merge `recipe` into `live` against `last`, the recipe last applied (null
160
+ * when there is none), each as written. Pure: the caller reads and writes the
161
+ * files, and gives `meaningOf`, a rule's meaning as a string two rules share
162
+ * when they mean the same (as written, unless given). Returns the rule list
163
+ * and `default` to write, each as written, and whether `default` changed.
164
+ * Throws when `live` or `recipe` names a rule twice, since the merge keys
165
+ * rules by name.
166
+ */
167
+ export function reconcileWithRecipe(
168
+ live: WrittenConfig,
169
+ recipe: WrittenConfig,
170
+ last: WrittenConfig | null,
171
+ meaningOf: (rule: unknown) => string = canonical,
172
+ ): { rules: unknown[]; default: unknown; defaultChanged: boolean; changes: RecipeChanges } {
173
+ const agree = (a: unknown, b: unknown): boolean => meaningOf(a) === meaningOf(b);
174
+ const liveDuplicate = duplicateName(live.rules);
175
+ if (liveDuplicate !== undefined) {
176
+ throw new Error(`gate.json names the rule "${liveDuplicate}" twice`);
177
+ }
178
+ const recipeDuplicate = duplicateName(recipe.rules);
179
+ if (recipeDuplicate !== undefined) {
180
+ throw new Error(`the recipe names the rule "${recipeDuplicate}" twice`);
181
+ }
182
+
183
+ const changes = noChanges();
184
+ // Each rule by its name; a rule with no name by a key no name can be, so
185
+ // it keeps its place and matches nothing.
186
+ const keyOf = (rule: unknown, index: number): string => ruleName(rule) ?? `\u0000nameless ${index}`;
187
+ const byName = (rules: readonly unknown[]) => new Map(rules.flatMap((rule) => {
188
+ const name = ruleName(rule);
189
+ return name === undefined ? [] : [[name, rule] as const];
190
+ }));
191
+ const inLive = new Map(live.rules.map((rule, index) => [keyOf(rule, index), rule] as const));
192
+ const inRecipe = byName(recipe.rules);
193
+ const inLast = last ? byName(last.rules) : null;
194
+
195
+ // Each rule gate.json keeps, with its definition, and which of them take
196
+ // the recipe's order (`ordered`); the rest keep their places.
197
+ const definitions = new Map<string, unknown>();
198
+ const ordered = new Set<string>();
199
+ live.rules.forEach((rule, index) => {
200
+ const name = keyOf(rule, index);
201
+ const declared = inRecipe.get(name);
202
+ const before = inLast?.get(name);
203
+ if (declared !== undefined && agree(rule, declared)) {
204
+ // As the recipe has it: kept as written, whatever the record says, and
205
+ // nothing to change or name.
206
+ definitions.set(name, rule);
207
+ ordered.add(name);
208
+ } else if (ruleName(rule) === undefined) {
209
+ definitions.set(name, rule);
210
+ } else if (!inLast) {
211
+ definitions.set(name, rule);
212
+ (declared !== undefined ? changes.differsFromRecipe : changes.outsideRecipe).push(name);
213
+ } else if (before !== undefined) {
214
+ const edited = !agree(rule, before);
215
+ if (declared === undefined) {
216
+ if (edited) {
217
+ definitions.set(name, rule);
218
+ changes.keptEdited.push(name);
219
+ } else {
220
+ changes.removed.push(name);
221
+ }
222
+ } else {
223
+ // Both recipes declare it: it's a recipe rule, so it takes the recipe's order.
224
+ ordered.add(name);
225
+ if (edited) {
226
+ definitions.set(name, rule);
227
+ if (!agree(declared, before)) changes.keptEdited.push(name);
228
+ } else {
229
+ definitions.set(name, declared);
230
+ changes.updated.push(name);
231
+ }
232
+ }
233
+ } else {
234
+ // No recipe declared it as it stands: a run-time rule, or one the recipe
235
+ // now declares otherwise.
236
+ definitions.set(name, rule);
237
+ if (declared !== undefined) changes.differsFromRecipe.push(name);
238
+ }
239
+ });
240
+
241
+ const liveOrder = live.rules.map(keyOf);
242
+ const recipeOrder = recipe.rules.flatMap((rule) => {
243
+ const name = ruleName(rule);
244
+ return name === undefined ? [] : [name];
245
+ });
246
+ const staying = liveOrder.filter((name) => definitions.has(name));
247
+
248
+ // Whether gate.json holds the rules both recipes declare in the last recipe's order.
249
+ const shared = (order: string[]) => order.filter((name) => inLast?.has(name) && inRecipe.has(name) && inLive.has(name));
250
+ const lastOrder = last ? last.rules.flatMap((rule) => {
251
+ const name = ruleName(rule);
252
+ return name === undefined ? [] : [name];
253
+ }) : [];
254
+ const followsLast = canonical(shared(liveOrder)) === canonical(shared(lastOrder));
255
+
256
+ let order: string[];
257
+ if (followsLast) {
258
+ // The recipe's rules fill the places its rules hold, in the recipe's order.
259
+ const recipeRules = recipeOrder.filter((name) => ordered.has(name));
260
+ let next = 0;
261
+ order = staying.map((name) => (ordered.has(name) ? recipeRules[next++] : name));
262
+ } else {
263
+ order = staying;
264
+ // Named only when it holds something back: the recipe reordered these
265
+ // rules, and gate.json doesn't already hold the recipe's order.
266
+ changes.orderKept = canonical(shared(recipeOrder)) !== canonical(shared(lastOrder))
267
+ && canonical(shared(liveOrder)) !== canonical(shared(recipeOrder));
268
+ }
269
+ // A recipe rule new to gate.json goes before the recipe rule that follows it
270
+ // there, or after the one before it when none follows.
271
+ recipeOrder.forEach((name, index) => {
272
+ if (inLive.has(name)) return;
273
+ definitions.set(name, inRecipe.get(name)!);
274
+ (inLast?.has(name) ? changes.restored : changes.added).push(name);
275
+ const following = recipeOrder.slice(index + 1).find((later) => order.includes(later));
276
+ if (following !== undefined) {
277
+ order.splice(order.indexOf(following), 0, name);
278
+ return;
279
+ }
280
+ const preceding = recipeOrder.slice(0, index).reverse().find((earlier) => order.includes(earlier));
281
+ if (preceding !== undefined) order.splice(order.indexOf(preceding) + 1, 0, name);
282
+ else order.push(name);
283
+ });
284
+
285
+ // Only the recipe's rules move; every other rule keeps its place.
286
+ const stayedInOrder = longestShared(staying.filter((name) => ordered.has(name)), order.filter((name) => ordered.has(name)));
287
+ changes.moved = order.filter((name) => ordered.has(name) && !stayedInOrder.has(name));
288
+
289
+ // `default` as the gate reads it, compared as written once read.
290
+ const liveDefault = behaviorOf(live.default);
291
+ const recipeDefault = behaviorOf(recipe.default);
292
+ let defaultBehavior = live.default;
293
+ let defaultChanged = false;
294
+ const shownBehavior = (value: unknown): string => (typeof value === 'string' ? value : JSON.stringify(value));
295
+ if (!last) {
296
+ if (!same(liveDefault, recipeDefault)) changes.defaultDiffers = { kept: shownBehavior(liveDefault), recipe: shownBehavior(recipeDefault) };
297
+ } else {
298
+ const lastDefault = behaviorOf(last.default);
299
+ if (same(liveDefault, lastDefault) || same(liveDefault, recipeDefault)) {
300
+ if (!same(recipeDefault, liveDefault)) {
301
+ defaultBehavior = recipe.default;
302
+ defaultChanged = true;
303
+ changes.defaultChange = { from: shownBehavior(liveDefault), to: shownBehavior(recipeDefault) };
304
+ }
305
+ } else if (!same(recipeDefault, lastDefault)) {
306
+ changes.defaultKept = shownBehavior(liveDefault);
307
+ }
308
+ }
309
+
310
+ return {
311
+ rules: order.map((name) => definitions.get(name)),
312
+ default: defaultBehavior,
313
+ defaultChanged,
314
+ changes,
315
+ };
316
+ }
317
+
318
+ /** The log line a start gives its changes, or null when it changed nothing worth saying. */
319
+ export function reconcileLine(changes: RecipeChanges): string | null {
320
+ const parts: string[] = [];
321
+ if (changes.recordUnreadable !== undefined) {
322
+ parts.push(`the recorded recipe couldn't be read (${changes.recordUnreadable}), so this start removed and redefined nothing`);
323
+ }
324
+ const list = (label: string, names: string[]) => {
325
+ if (names.length > 0) parts.push(`${label} ${names.join(', ')}`);
326
+ };
327
+ list('added', changes.added);
328
+ list('removed', changes.removed);
329
+ list('updated', changes.updated);
330
+ list('restored', changes.restored);
331
+ list('moved', changes.moved);
332
+ list('kept as edited, though the recipe changed or dropped it:', changes.keptEdited);
333
+ if (changes.differsFromRecipe.length > 0) {
334
+ parts.push(
335
+ `kept, though the recipe defines it otherwise: ${changes.differsFromRecipe.join(', ')} `
336
+ + '(remove such a rule, and the next start restores it from the recipe)',
337
+ );
338
+ }
339
+ if (changes.outsideRecipe.length > 0) {
340
+ parts.push(
341
+ `kept, not in the recipe: ${changes.outsideRecipe.join(', ')} `
342
+ + "(a stale one can be removed, and no start brings it back)",
343
+ );
344
+ }
345
+ if (changes.defaultChange) parts.push(`default ${changes.defaultChange.from} → ${changes.defaultChange.to}`);
346
+ if (changes.defaultKept) parts.push(`default kept as edited (${changes.defaultKept}), though the recipe changed it`);
347
+ if (changes.defaultDiffers) {
348
+ parts.push(
349
+ `default kept as gate.json has it (${changes.defaultDiffers.kept}), though the recipe's is ${changes.defaultDiffers.recipe} `
350
+ + "(set it to the recipe's, and later starts follow the recipe)",
351
+ );
352
+ }
353
+ if (changes.orderKept) parts.push("gate.json's order kept, though the recipe reordered its rules, since a recipe rule was moved in it");
354
+ return parts.length > 0 ? `[gate] the recipe was applied to gate.json: ${parts.join('; ')}` : null;
355
+ }
package/src/gate/types.ts CHANGED
@@ -17,9 +17,10 @@
17
17
  * bucket are denied (decision: trigger=false). Tokens regenerate at one per
18
18
  * `refillIntervalMs`, capped at `tokens`. Buckets are partitioned by the
19
19
  * value of `keyBy` resolved against event metadata — so a per-user limit
20
- * uses `keyBy: "senderId"`, a per-channel limit uses `keyBy: "channelId"`,
21
- * etc. With no `keyBy` (or when the resolved value is missing), one shared
22
- * bucket is used for the policy.
20
+ * uses `keyBy: "senderId"`, a per-channel limit uses `keyBy: "channelId"`
21
+ * (resolved to the channel's registered id, as `keyBy` says), etc. With no
22
+ * `keyBy` (or when the resolved value is missing), one shared bucket is used
23
+ * for the policy.
23
24
  */
24
25
  export interface RateLimitConfig {
25
26
  /** Max bucket capacity, also the initial fill. Must be > 0. */
@@ -27,8 +28,12 @@ export interface RateLimitConfig {
27
28
  /** Milliseconds between token regenerations. Must be > 0. */
28
29
  refillIntervalMs: number;
29
30
  /**
30
- * Metadata field name to partition buckets by. If absent or the metadata
31
- * field is missing on a given event, all events share one bucket.
31
+ * Metadata field name to partition buckets by. `"channelId"` takes the
32
+ * channel's registered id where the gate resolves one, as `match.channel`
33
+ * does, so a channel's push events share its messages' bucket, a voice
34
+ * report's included, though it carries no `channelId`. Otherwise, if
35
+ * absent or the metadata field is missing on a given event, all such
36
+ * events share one bucket.
32
37
  */
33
38
  keyBy?: string;
34
39
  }
@@ -45,7 +50,8 @@ export interface RateLimitConfig {
45
50
  export interface PassiveSampleConfig {
46
51
  /** Trigger every N-th match. Must be > 0. */
47
52
  every: number;
48
- /** Optional metadata field name for separate per-key counters. */
53
+ /** Optional metadata field name for separate per-key counters, resolved
54
+ * as `RateLimitConfig.keyBy` is. */
49
55
  keyBy?: string;
50
56
  }
51
57
 
@@ -68,7 +74,12 @@ export interface GatePolicyMatch {
68
74
  scope?: string[];
69
75
  /** ServerId to match (exact or glob with *). */
70
76
  source?: string;
71
- /** ChannelId to match (exact or glob with *). */
77
+ /** Channel to match (exact or glob with *): against the event's channel
78
+ * id, then the registered id the gate resolves for it
79
+ * (`resolveRouteChannel`). A channels/incoming message carries the
80
+ * registered id (`discord:<guild>:<channel>`), and a push the adapter's
81
+ * raw one or none, so a registered-form pattern meets every event in its
82
+ * channel whose push names the channel, and a raw id only push events. */
72
83
  channel?: string;
73
84
  /** Content text filter. */
74
85
  filter?: { type: 'text' | 'regex'; pattern: string };
@@ -155,7 +166,17 @@ export interface GateConfig {
155
166
  export interface GateOptions {
156
167
  /** Path to gate.json. Default: derived from storePath. */
157
168
  configPath?: string;
158
- /** Initial config, seeded to gate.json if the file doesn't exist. */
169
+ /**
170
+ * The recipe's gate config. The first start seeds gate.json with it. Each
171
+ * later start merges it into gate.json against the recipe last applied,
172
+ * which the gate keeps beside gate.json (`gate.recipe.json`): a rule the
173
+ * recipe dropped, renamed, moved or redefined follows it, a rule edited at
174
+ * run time or by hand stays, and so does one no recipe declared. Rules are
175
+ * merged by name, compared by what they mean and kept as written: a
176
+ * recipe's rule with no name is never merged, as one with no name was never
177
+ * added, and where gate.json or the recipe names a rule twice, a start only
178
+ * adds the rules gate.json lacks. See `recipe-reconcile.ts`.
179
+ */
159
180
  config?: GateConfig;
160
181
  /**
161
182
  * Path to a JSON file listing user ids that may wake the agent even while
@@ -231,6 +252,47 @@ export interface GateEventInfo {
231
252
  tags?: string[];
232
253
  }
233
254
 
255
+ /** An MCPL event whose content the framework stored at arrival, as a
256
+ * batched wake names it to the framework (EventGate's `isNewestEntry`):
257
+ * a channel message by its server, channel and message id, a push event by
258
+ * its server and event id. */
259
+ export interface GateEntryRef {
260
+ eventType: string;
261
+ serverId?: string;
262
+ channelId?: string;
263
+ messageId?: string;
264
+ eventId?: string;
265
+ }
266
+
267
+ /** A batched wake's `[Gate: …]` line that the gate handed to the wake rather
268
+ * than wrote: its batch was one event whose entry was still the newest in
269
+ * the window when the batch fired (EventGate's `isNewestEntry`). A fresh
270
+ * start of the wake's turn decides it (the framework's placeWakeLines): the
271
+ * delivery agent's own start leaves it out while that entry is still the
272
+ * newest, and writes it once something follows; a co-resident's start
273
+ * places none; two or more such lines are each written;
274
+ * and none is written while the host is quiesced or another reader of the
275
+ * residents' shared slot has a turn alive. A line left out stays for a
276
+ * later fresh start of the same wake. Its text is rendered when it's
277
+ * written (wakeLineText), so its age is true then.
278
+ *
279
+ * Every request for one wake carries the same object: the framework writes
280
+ * each line once by its identity, across the wake's retries and holds. A
281
+ * host that hands the gate's requests on itself passes the line on as it
282
+ * is, not a copy per agent. */
283
+ export interface WakeLine {
284
+ /** The event's own entry. */
285
+ entry: GateEntryRef;
286
+ policyNames: string[];
287
+ /** The channel as the line names it to the agent. */
288
+ shown: string;
289
+ /** When the event arrived (ms): the line's age is counted from it to when
290
+ * the line is written (wakeLineText). */
291
+ at: number;
292
+ /** The channel the line names, as the omission's trace reports it. */
293
+ channelId?: string;
294
+ }
295
+
234
296
  /** Per-policy runtime statistics (for gate:status). */
235
297
  export interface GatePolicyStats {
236
298
  name: string;
package/src/index.ts CHANGED
@@ -2,9 +2,10 @@
2
2
  export * from './types/index.js';
3
3
 
4
4
  // Core classes
5
- export { AgentFramework, BudgetPreflightError, ProviderWaitNotSleptError, QuiesceBlockedError, ResumeBlockedError } from './framework.js';
5
+ export { AgentFramework, BudgetPreflightError, DefaultErrorPolicy, ProviderWaitNotSleptError, QuiesceBlockedError, ResumeBlockedError } from './framework.js';
6
6
  export type { RuntimeSettingsPreview, HostModeStatus } from './framework.js';
7
7
  export { Agent } from './agent.js';
8
+ export { storedInjectionMarkOf, type StoredInjectionMark } from './stored-injections.js';
8
9
  export type { StartStreamResult } from './agent.js';
9
10
  export { ProcessQueueImpl } from './queue.js';
10
11
  export { ModuleRegistry } from './module-registry.js';
@@ -67,12 +68,12 @@ export { buildCodeExecutionToolDefinition, CODE_EXECUTION_TOOL_NAME } from './co
67
68
  export type { SessionUsage, AgentUsage, SessionUsageSnapshot, UsageUpdatedEvent } from './usage/index.js';
68
69
 
69
70
  // EventGate
70
- export { EventGate, type WakeProvenance } from './gate/index.js';
71
- export type { GateConfig, GateOptions, GatePolicy, GatePolicyMatch, GateBehavior } from './gate/index.js';
71
+ export { EventGate, wakeLineText, type WakeProvenance, type WritePlacement } from './gate/index.js';
72
+ export type { GateConfig, GateOptions, GatePolicy, GatePolicyMatch, GateBehavior, WakeLine } from './gate/index.js';
72
73
 
73
74
  // MCPL channel registry (exposed for modules that need channel-level operations)
74
75
  export { ChannelRegistry } from './mcpl/index.js';
75
- export type { PublishRefusalReason } from './mcpl/index.js';
76
+ export type { ChannelRef, PublishRefusalReason } from './mcpl/index.js';
76
77
 
77
78
  // Where each accepted inbound item came from (`metadata.inboundSource`), and
78
79
  // the digest its delivered and stored bodies are stamped with
@@ -95,6 +96,8 @@ export type { McplServerConfig } from './mcpl/index.js';
95
96
  // exactly the way the framework host does: MCPL over stdio or WebSocket, and
96
97
  // modern MCP over stdio or Streamable HTTP)
97
98
  export { McplServerConnection, McplRequestError, McplProtocolVersionError, McplUnreapedLaunchError } from './mcpl/index.js';
99
+ // RFC-014: the meta-tools' module and its names.
100
+ export { ToolsetsModule, TOOLSETS_MODULE, META_TOOL_NAMES } from './mcpl/index.js';
98
101
  export { ModernMcpConnection } from './mcpl/index.js';
99
102
  export type { ModernToolCallResult } from './mcpl/index.js';
100
103
  export {