@animalabs/agent-framework 0.26.2 → 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 +56 -6
  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 +759 -129
  18. package/dist/src/framework.d.ts.map +1 -1
  19. package/dist/src/framework.js +3482 -928
  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 +736 -77
  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 +176 -24
  200. package/dist/src/tool-result-history.d.ts.map +1 -1
  201. package/dist/src/tool-result-history.js +411 -91
  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 +15 -1
  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 +3601 -967
  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 +851 -77
  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 +471 -90
  271. package/src/tune-out/coordinator.ts +80 -37
  272. package/src/types/agent.ts +189 -7
  273. package/src/types/events.ts +15 -1
  274. package/src/types/framework.ts +21 -3
  275. package/src/types/module.ts +86 -7
  276. package/src/types/trace.ts +58 -2
@@ -21,6 +21,7 @@ import type {
21
21
  ProcessEvent,
22
22
  TraceEventListener,
23
23
  ModuleMessage,
24
+ ModuleContextInjection,
24
25
  MessageQueryResult,
25
26
  } from './types/index.js';
26
27
  import { heldWriteId } from './held-writes.js';
@@ -110,11 +111,11 @@ export class ModuleRegistry {
110
111
  private store: JsStore;
111
112
  private queue: ModuleQueue;
112
113
  private getAgents: () => Agent[];
113
- private addMessageFn: (participant: string, content: ContentBlock[], metadata?: MessageMetadata) => MessageId;
114
+ private addMessageFn: (participant: string, content: ContentBlock[], metadata?: MessageMetadata, opts?: { forAgent?: string }) => MessageId;
114
115
  private editMessageFn: (id: MessageId, content: ContentBlock[]) => void;
115
116
  private removeMessageFn: (id: MessageId) => void;
116
117
  private getMessageFn: (id: MessageId) => ModuleMessage | null;
117
- private queryMessagesFn: (filter: MessageQuery) => MessageQueryResult;
118
+ private queryMessagesFn: (filter: MessageQuery, opts?: { forAgent?: string }) => MessageQueryResult;
118
119
 
119
120
  private pushEventFn: (event: ProcessEvent, moduleName?: string) => void;
120
121
  private onTraceFn: (listener: TraceEventListener) => () => void;
@@ -134,11 +135,11 @@ export class ModuleRegistry {
134
135
  queue: ModuleQueue,
135
136
  options: {
136
137
  getAgents: () => Agent[];
137
- addMessage: (participant: string, content: ContentBlock[], metadata?: MessageMetadata) => MessageId;
138
+ addMessage: (participant: string, content: ContentBlock[], metadata?: MessageMetadata, opts?: { forAgent?: string }) => MessageId;
138
139
  editMessage: (id: MessageId, content: ContentBlock[]) => void;
139
140
  removeMessage: (id: MessageId) => void;
140
141
  getMessage: (id: MessageId) => ModuleMessage | null;
141
- queryMessages: (filter: MessageQuery) => MessageQueryResult;
142
+ queryMessages: (filter: MessageQuery, opts?: { forAgent?: string }) => MessageQueryResult;
142
143
  /** Called with the pushing module's name. */
143
144
  pushEvent: (event: ProcessEvent, moduleName?: string) => void;
144
145
  onTrace: (listener: TraceEventListener) => () => void;
@@ -230,7 +231,8 @@ export class ModuleRegistry {
230
231
 
231
232
  /**
232
233
  * Set state for a module by name.
233
- * Used by the framework to apply stateUpdate from EventResponse atomically.
234
+ * Used by the framework to apply stateUpdate from EventResponse after its
235
+ * message changes.
234
236
  */
235
237
  setModuleState(moduleName: string, state: unknown): void {
236
238
  const ctx = this.moduleContexts.get(moduleName);
@@ -439,8 +441,18 @@ export class ModuleRegistry {
439
441
 
440
442
  /**
441
443
  * Gather context injections from all modules before inference.
442
- * Calls each module's gatherContext() in parallel with a per-module timeout.
443
- * Fail-open: timed-out or erroring modules are skipped (with a stderr line).
444
+ * Calls each module's gatherContext() in parallel with a per-module timeout,
445
+ * and returns their injections in the modules' registration order, whatever
446
+ * order they finish in. Fail-open: timed-out or erroring modules are skipped
447
+ * (with a stderr line), whether the call throws, rejects, or answers with
448
+ * something that isn't an array of objects, or with a `system` entry whose
449
+ * content isn't an array of objects, every block of which the agent's
450
+ * buildSystemPrompt reads, and one that answers after its timeout adds
451
+ * nothing. An array returned without a promise is taken as the module's
452
+ * answer. An entry's other fields are the module's to get right. Each
453
+ * module's budget starts once its call returns, after its own synchronous
454
+ * work and every earlier module's; a later module's synchronous work still
455
+ * counts against it.
444
456
  *
445
457
  * The per-module budget is `module.contextTimeoutMs` when declared, else the
446
458
  * `timeoutMs` default (15s). Modules whose gatherContext does real work —
@@ -450,12 +462,16 @@ export class ModuleRegistry {
450
462
  * to MAX_CONTEXT_TIMEOUT_MS so no single module can hold every turn hostage.
451
463
  * Adapted from Anarchid/agent-framework@mcpl-module-proto.
452
464
  */
453
- async gatherContext(agentName: string, timeoutMs = 15_000): Promise<ContextInjection[]> {
454
- const injections: ContextInjection[] = [];
465
+ async gatherContext(agentName: string, timeoutMs = 15_000): Promise<ModuleContextInjection[]> {
466
+ // Each module's result in registration order, whatever order they finish
467
+ // in: a stored injection's content hash follows that order, and a module
468
+ // that answers after its timeout adds nothing (it's skipped, fail-open).
469
+ const results: ModuleContextInjection[][] = [];
455
470
  const promises: Promise<void>[] = [];
456
471
 
457
472
  for (const module of this.modules.values()) {
458
- if (!module.gatherContext) continue;
473
+ const gather = module.gatherContext;
474
+ if (!gather) continue;
459
475
 
460
476
  // Clamp the per-module budget. A module declaring `contextTimeoutMs`
461
477
  // legitimately needs more than the shared default (e.g. a retrieval
@@ -467,14 +483,43 @@ export class ModuleRegistry {
467
483
  MAX_CONTEXT_TIMEOUT_MS,
468
484
  );
469
485
  let timer: ReturnType<typeof setTimeout> | undefined;
486
+ let timedOut = false;
487
+ const slot = results.length;
488
+ results.push([]);
489
+ // Called here, before its timer is set, so its budget starts after its
490
+ // synchronous work; a call that throws is skipped as a rejection is.
491
+ let call: Promise<ModuleContextInjection[]>;
492
+ try {
493
+ call = Promise.resolve(gather.call(module, agentName));
494
+ } catch (err) {
495
+ call = Promise.reject(err);
496
+ }
470
497
  const promise = Promise.race([
471
- module.gatherContext(agentName).then(r => {
472
- injections.push(...r);
498
+ call.then(r => {
499
+ if (timedOut) return;
500
+ // Checked before it's kept, so an answer that isn't an array of
501
+ // objects skips the module, as does a system entry whose content
502
+ // isn't an array of objects: buildSystemPrompt reads every block
503
+ // of it, and one it can't read would fail the turn. Each array is
504
+ // read through Array.from, which reads a hole as `undefined`, as
505
+ // context-manager's compile does when it spreads the content.
506
+ if (!Array.isArray(r) || Array.from(r).some((entry) => entry === null || typeof entry !== 'object')) {
507
+ throw new TypeError(`gatherContext answered with ${Array.isArray(r) ? 'an array holding an entry that is not an object' : r === null ? 'null' : typeof r}, not an array of objects`);
508
+ }
509
+ const unreadable = r.find((entry) => entry.position === 'system'
510
+ && !(Array.isArray(entry.content) && Array.from(entry.content).every((block) => block !== null && typeof block === 'object')));
511
+ if (unreadable) {
512
+ throw new TypeError(`gatherContext answered with a system injection in namespace ${logValue(unreadable.namespace)} whose content isn't an array of objects`);
513
+ }
473
514
  this.noteBlocksNotShown(module.name, r);
515
+ results[slot] = [...r];
474
516
  }),
475
517
  new Promise<void>((_, rej) => {
476
518
  timer = setTimeout(
477
- () => rej(new Error(`gatherContext timed out after ${budgetMs}ms`)),
519
+ () => {
520
+ timedOut = true;
521
+ rej(new Error(`gatherContext timed out after ${budgetMs}ms`));
522
+ },
478
523
  budgetMs,
479
524
  );
480
525
  }),
@@ -486,7 +531,7 @@ export class ModuleRegistry {
486
531
  }
487
532
 
488
533
  await Promise.all(promises);
489
- return injections;
534
+ return results.flat();
490
535
  }
491
536
 
492
537
  /**
@@ -607,11 +652,11 @@ class ModuleContextImpl implements ModuleContext {
607
652
  private registry: ModuleRegistry;
608
653
  readonly isRestart: boolean;
609
654
  private getAgentsFn: () => Agent[];
610
- private addMessageFn: (participant: string, content: ContentBlock[], metadata?: MessageMetadata) => MessageId;
655
+ private addMessageFn: (participant: string, content: ContentBlock[], metadata?: MessageMetadata, opts?: { forAgent?: string }) => MessageId;
611
656
  private editMessageFn: (id: MessageId, content: ContentBlock[]) => void;
612
657
  private removeMessageFn: (id: MessageId) => void;
613
658
  private getMessageFn: (id: MessageId) => ModuleMessage | null;
614
- private queryMessagesFn: (filter: MessageQuery) => MessageQueryResult;
659
+ private queryMessagesFn: (filter: MessageQuery, opts?: { forAgent?: string }) => MessageQueryResult;
615
660
  private pushEventFn: (event: ProcessEvent, moduleName?: string) => void;
616
661
  private onTraceFn: (listener: TraceEventListener) => () => void;
617
662
  private callToolFn: (call: ToolCall) => Promise<ToolResult>;
@@ -627,11 +672,11 @@ class ModuleContextImpl implements ModuleContext {
627
672
  registry: ModuleRegistry,
628
673
  isRestart: boolean,
629
674
  getAgents: () => Agent[],
630
- addMessage: (participant: string, content: ContentBlock[], metadata?: MessageMetadata) => MessageId,
675
+ addMessage: (participant: string, content: ContentBlock[], metadata?: MessageMetadata, opts?: { forAgent?: string }) => MessageId,
631
676
  editMessage: (id: MessageId, content: ContentBlock[]) => void,
632
677
  removeMessage: (id: MessageId) => void,
633
678
  getMessage: (id: MessageId) => ModuleMessage | null,
634
- queryMessages: (filter: MessageQuery) => MessageQueryResult,
679
+ queryMessages: (filter: MessageQuery, opts?: { forAgent?: string }) => MessageQueryResult,
635
680
  pushEvent: (event: ProcessEvent, moduleName?: string) => void,
636
681
  onTrace: (listener: TraceEventListener) => () => void,
637
682
  callTool: (call: ToolCall) => Promise<ToolResult>
@@ -753,9 +798,17 @@ class ModuleContextImpl implements ModuleContext {
753
798
  addMessage(
754
799
  participant: string,
755
800
  content: ContentBlock[],
756
- metadata?: MessageMetadata & { external?: ExternalIdRef }
801
+ metadata?: MessageMetadata & { external?: ExternalIdRef },
802
+ opts?: { forAgent?: string },
757
803
  ): MessageId {
758
- const id = this.addMessageFn(participant, content, metadata);
804
+ // The external id map is read in the primary's store
805
+ // (findMessageByExternalId), so a mapping into another window would
806
+ // answer wrong.
807
+ if (metadata?.external && opts?.forAgent !== undefined) {
808
+ throw new Error(`A message for agent ${JSON.stringify(opts.forAgent)} can't carry metadata.external: `
809
+ + "its external id would be looked up in the primary's window. Leave forAgent out, or the external id.");
810
+ }
811
+ const id = this.addMessageFn(participant, content, metadata, opts);
759
812
 
760
813
  // Track external ID if provided. A write the framework holds keeps '',
761
814
  // as it did before held writes had handles: the map is module state an
@@ -819,8 +872,8 @@ class ModuleContextImpl implements ModuleContext {
819
872
  return this.getMessageFn(id);
820
873
  }
821
874
 
822
- queryMessages(filter: MessageQuery): MessageQueryResult {
823
- return this.queryMessagesFn(filter);
875
+ queryMessages(filter: MessageQuery, opts?: { forAgent?: string }): MessageQueryResult {
876
+ return this.queryMessagesFn(filter, opts);
824
877
  }
825
878
 
826
879
  getAgents(): AgentInfo[] {
@@ -976,7 +1029,7 @@ function withoutRefusal(call: ToolCall, result: ToolResult): ToolResult {
976
1029
  }
977
1030
 
978
1031
  /** A result's flags, which consumers read for their truth: booleans here. */
979
- const RESULT_FLAGS = ['success', 'isError', 'endTurn'] as const;
1032
+ const RESULT_FLAGS = ['success', 'isError', 'endTurn', 'dataIsContent'] as const;
980
1033
 
981
1034
  /**
982
1035
  * A module's tool result as the framework can store, send and trace it. A
@@ -1047,7 +1100,7 @@ export function checkedToolResult(call: ToolCall, result: unknown): { result: To
1047
1100
  endTurn = Boolean(fields.endTurn);
1048
1101
  read('error');
1049
1102
  ownText = errorText(fields.error);
1050
- for (const key of ['success', 'isError', 'data']) read(key);
1103
+ for (const key of ['success', 'isError', 'data', 'dataIsContent']) read(key);
1051
1104
  for (const key of Object.keys(candidate)) {
1052
1105
  if (key !== 'toJSON' && !(key in fields)) fields[key] = candidate[key];
1053
1106
  }
@@ -828,7 +828,7 @@ export class DiscordModule implements Module {
828
828
  const stored = this.storedRow(msg.discordMessageId);
829
829
  if (live) live.phase = 'writing';
830
830
 
831
- // Return declarative response - framework applies all writes atomically
831
+ // Return declarative response - the framework applies its writes
832
832
  return {
833
833
  ...(stored ? {} : {
834
834
  addMessages: [
@@ -49,7 +49,7 @@ import type { ToolDefinition, ToolCall, ToolResult, ProcessEvent } from '../../t
49
49
  import type { EventResponse, ProcessState } from '../../types/module.js';
50
50
  import type { SearchWorkerMessage, SearchWorkerMatch } from './search-regex-worker.js';
51
51
  import type { ChannelRegistry } from '../../mcpl/channel-registry.js';
52
- import { EmbedServiceError, SemanticIndexClient, SemanticIndexer, messageIndexText, summaryIndexText, type SearchResponse, type SemanticIndexConfig, type SyncReport, type PendingChanges } from './semantic.js';
52
+ import { EmbedServiceError, SemanticIndexClient, SemanticIndexer, messageIndexText, parseSummaryKey, summaryIndexText, type SearchResponse, type SemanticIndexConfig, type SyncReport, type PendingChanges, type SyncCursors } from './semantic.js';
53
53
  import { unknownToolMessage } from '../unknown-tool.js';
54
54
  import { safeSlice } from '../../safe-slice.js';
55
55
 
@@ -624,6 +624,16 @@ export class HistoryModule implements Module {
624
624
  const prev = this.ctx?.getState<Record<string, unknown>>() ?? {};
625
625
  this.ctx?.setState({ ...prev, semanticPending: p });
626
626
  };
627
+ // Module state follows the store's branches, so each branch keeps its
628
+ // own sync cursors, and a branch made at an earlier point inherits them
629
+ // as they were there.
630
+ this.indexer.cursorStore = {
631
+ read: () => this.ctx?.getState<{ semanticCursors?: SyncCursors }>()?.semanticCursors ?? null,
632
+ write: (cursors) => {
633
+ const prev = this.ctx?.getState<Record<string, unknown>>() ?? {};
634
+ this.ctx?.setState({ ...prev, semanticCursors: cursors });
635
+ },
636
+ };
627
637
  this.indexer.attach();
628
638
  }
629
639
 
@@ -631,13 +641,16 @@ export class HistoryModule implements Module {
631
641
  this.ctx = null;
632
642
  if (this.syncTimer) { clearInterval(this.syncTimer); this.syncTimer = null; }
633
643
  if (this.firstSyncTimer) { clearTimeout(this.firstSyncTimer); this.firstSyncTimer = null; }
634
- if (this.indexer) this.indexer.onPendingChange = null;
644
+ // Unwired from the state it can no longer reach, the indexer goes on
645
+ // from the cursors it last wrote for each branch, kept in memory.
646
+ if (this.indexer) { this.indexer.onPendingChange = null; this.indexer.cursorStore = null; }
635
647
  this.indexer?.dispose();
636
648
  }
637
649
 
638
650
  /**
639
651
  * Background sync into the semantic index: every `syncIntervalMs` (default
640
- * 60 s) push up to `maxSyncPerTick` new items. Idempotent to call — starts
652
+ * 60 s) write at most `maxSyncPerTick` items to the index, counted as its
653
+ * doc says. Idempotent to call — starts
641
654
  * once we have both a context-manager (bind) and a config; `unref`'d so it
642
655
  * never keeps a shutting-down process alive. A first tick runs after 5 s so
643
656
  * a fresh store starts backfilling immediately rather than a minute later.
@@ -1608,7 +1621,10 @@ export class HistoryModule implements Module {
1608
1621
  );
1609
1622
  }
1610
1623
 
1611
- // Bounded catch-up so the newest messages are searchable; never block on a backlog.
1624
+ // A catch-up bounded in what it writes, so the newest messages are
1625
+ // searchable without waiting on a backlog. It can re-send many items the
1626
+ // index holds unchanged, as on a branch's first pass (maxSyncPerTick's
1627
+ // doc), which costs a request per batch and no embedding.
1612
1628
  let sync: SyncReport | null = null;
1613
1629
  if (this.indexer) {
1614
1630
  try { sync = await this.indexer.catchUp(this.semanticCfg.maxSyncBeforeSearch ?? 256); } catch { /* reported via indexer.lastError */ }
@@ -1672,9 +1688,10 @@ export class HistoryModule implements Module {
1672
1688
  // to the model — getMessage/getSummary rebuild on branch switch, so an
1673
1689
  // undone message, or a summary this branch doesn't hold, answers null.
1674
1690
  // A branch made from an earlier point, as an undo makes one, can mint a
1675
- // summary id another branch already used, so a summary hit's score,
1676
- // timestamp and date filtering can still be that other summary's.
1677
- // k is small, so this is at most `limit` local lookups.
1691
+ // summary id another branch already used, so a summary's key carries its
1692
+ // creation time too, and a hit whose time isn't this branch's summary's
1693
+ // is another branch's. k is small, so this is at most `limit` local
1694
+ // lookups.
1678
1695
  // A message hit whose current text has nothing left to index (edited
1679
1696
  // blank, delete not yet applied) is dropped too, so its stale snippet is
1680
1697
  // never served. Dropped hits are replaced by over-fetching: when the
@@ -1685,14 +1702,12 @@ export class HistoryModule implements Module {
1685
1702
  // A message hit's snippet is rebuilt from the CURRENT local text, never
1686
1703
  // the remote copy: an edit not yet synced (or one the index never got)
1687
1704
  // must not leak a passage the edit removed. Only the ranking can be stale.
1688
- // A summary hit's snippet is rebuilt from the local summary too: a
1689
- // service that keeps only vectors and ids (no text) still yields one, and
1690
- // a summary whose id another branch also minted shows this branch's text
1691
- // in its snippet and chars.
1705
+ // A summary hit's snippet is rebuilt from the local summary too, so a
1706
+ // service that keeps only vectors and ids (no text) still yields one.
1692
1707
  const currentText = new Map<string, string>();
1693
- const keep = (h: { id: string }): 'ok' | 'offBranch' | 'stale' => {
1708
+ const keep = (h: { id: string }): 'ok' | 'offBranch' | 'stale' | 'superseded' => {
1694
1709
  const msgId = /^msg:(.+)$/.exec(h.id)?.[1];
1695
- const sumId = /^sum:(.+)$/.exec(h.id)?.[1];
1710
+ const sum = parseSummaryKey(h.id);
1696
1711
  if (msgId !== undefined) {
1697
1712
  const m = cm.getMessage(msgId);
1698
1713
  if (!m) return 'offBranch';
@@ -1701,9 +1716,18 @@ export class HistoryModule implements Module {
1701
1716
  currentText.set(h.id, text);
1702
1717
  return 'ok';
1703
1718
  }
1704
- if (sumId !== undefined) {
1705
- const s = cm.getSummary(sumId);
1719
+ if (sum !== null) {
1720
+ // An item from before keys carried the creation time is never a hit.
1721
+ // One whose id names no summary on this branch is another branch's,
1722
+ // and is counted as such. One whose id this branch has can hold this
1723
+ // branch's summary or another's under the same id, and isn't counted.
1724
+ // Until this branch's passes send the summary under its own key, as
1725
+ // the search's catch-up does within its budget after any queued
1726
+ // edits, a search can miss it.
1727
+ const s = cm.getSummary(sum.id);
1706
1728
  if (!s) return 'offBranch';
1729
+ if (sum.createdMs === null) return 'superseded';
1730
+ if (s.created !== sum.createdMs) return 'offBranch';
1707
1731
  const text = summaryIndexText(s, this.semanticCfg!);
1708
1732
  // An empty text here is mostly defensive: context-manager drops a
1709
1733
  // summary that's empty after trimming when it loads, and refuses to
@@ -1771,10 +1795,13 @@ export class HistoryModule implements Module {
1771
1795
  const meta = typeof h.meta === 'object' && h.meta !== null && !Array.isArray(h.meta)
1772
1796
  ? h.meta as { participant?: unknown; author?: unknown }
1773
1797
  : null;
1798
+ // A summary is shown as `sum:<id>`, as it was before its key carried its creation time.
1799
+ const sum = parseSummaryKey(h.id);
1800
+ const id = sum ? `sum:${sum.id}` : h.id;
1774
1801
  const missing = [...(h.ts !== null && timestamp === null ? ['ts'] : []), ...(meta === null ? ['meta'] : [])];
1775
- if (missing.length > 0) unread.push(`${shown(h.id)} (${missing.join(' and ')})`);
1802
+ if (missing.length > 0) unread.push(`${shown(id)} (${missing.join(' and ')})`);
1776
1803
  return {
1777
- id: h.id,
1804
+ id,
1778
1805
  /** Raw message id for a `msg:` hit (null for summaries) — pass it to extract({ aroundId }). */
1779
1806
  messageId: h.id.startsWith('msg:') ? h.id.slice(4) : null,
1780
1807
  kind: h.kind,
@@ -1797,7 +1824,10 @@ export class HistoryModule implements Module {
1797
1824
  hits,
1798
1825
  index: {
1799
1826
  indexed: res.count_indexed,
1800
- behind: sync ? sync.more : this.indexer === null ? null : true,
1827
+ // The pass may have read another branch: one the search joined
1828
+ // that started before a switch, or one the search left while it
1829
+ // waited.
1830
+ behind: this.indexer === null ? null : sync && this.indexer.passedOn(sync) === this.indexer.branchId() ? sync.more : true,
1801
1831
  syncedThisCall: sync ? sync.pushed : 0,
1802
1832
  lastError: this.indexer?.lastError ?? null,
1803
1833
  /** Hits the index returned for messages/summaries not on the current branch (undone, checked out past). */