@animalabs/agent-framework 0.32.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 (288) 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 +1976 -818
  59. package/dist/src/framework.d.ts.map +1 -1
  60. package/dist/src/framework.js +7566 -3422
  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 +82 -20
  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 +32 -12
  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 +7804 -3546
  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 +86 -20
  285. package/src/types/events.ts +2 -0
  286. package/src/types/framework.ts +32 -12
  287. package/src/types/module.ts +181 -68
  288. package/src/types/trace.ts +542 -40
@@ -16,8 +16,11 @@ import type {
16
16
  AgentRecord,
17
17
  AgentRef,
18
18
  AgentSpec,
19
+ ContextInheritance,
19
20
  EndOutcome,
20
21
  HeldMail,
22
+ MessageSlotRef,
23
+ ObserveEdge,
21
24
  RegistrySnapshot,
22
25
  } from './types.js';
23
26
 
@@ -76,6 +79,123 @@ function clone<T>(value: T): T {
76
79
  return value === undefined ? value : (JSON.parse(JSON.stringify(value)) as T);
77
80
  }
78
81
 
82
+ /** AgentRegistry.register's options (see there). */
83
+ export interface RegisterOptions {
84
+ resume?: boolean;
85
+ after?: number;
86
+ declaredWith?: readonly AgentName[];
87
+ }
88
+
89
+ /** The residents' shared un-namespaced slot, on the host's current branch. */
90
+ export const RESIDENTS_SLOT: Readonly<MessageSlotRef> = Object.freeze({ namespace: null });
91
+
92
+ /** Whether two refs name one slot: the same namespace on the same branch. */
93
+ export function sameSlot(a: MessageSlotRef | undefined, b: MessageSlotRef | undefined): boolean {
94
+ return !!a && !!b && a.namespace === b.namespace && a.branch === b.branch;
95
+ }
96
+
97
+ /**
98
+ * The slot an observe edge reads. An edge that names a slot reads that
99
+ * namespace on the host's current branch. One that names an agent and no
100
+ * slot reads that agent's own slot, wherever it is (a shared fork's is on its
101
+ * own branch), as `ownSlotOf` looks it up: undefined where the lookup doesn't
102
+ * know the agent, or none is given. The registry's `observedSlotOf` passes its
103
+ * records. An edge that names neither reads the residents' shared slot.
104
+ */
105
+ export function observedSlot(
106
+ edge: ObserveEdge,
107
+ ownSlotOf?: (agent: AgentName) => MessageSlotRef | undefined,
108
+ ): MessageSlotRef | undefined {
109
+ if (edge.slot !== undefined) return { namespace: edge.slot };
110
+ if (edge.agent === undefined) return { namespace: null };
111
+ const own = ownSlotOf?.(edge.agent);
112
+ return own ? { namespace: own.namespace, ...(own.branch !== undefined ? { branch: own.branch } : {}) } : undefined;
113
+ }
114
+
115
+ /**
116
+ * The slot a shared derivation's own rows are stored in: the parent's
117
+ * message slot, as context-manager's derivation record names it, on the
118
+ * child's own branch. Undefined for any other inheritance, or one whose
119
+ * record doesn't say.
120
+ */
121
+ function derivedSlot(inherit: ContextInheritance | undefined): MessageSlotRef | undefined {
122
+ if (inherit?.mode !== 'shared' || typeof inherit.ownBranch !== 'string') return undefined;
123
+ const slots = (inherit.derivation as { slots?: { messageNamespace?: unknown } } | undefined)?.slots;
124
+ const namespace = slots?.messageNamespace;
125
+ if (namespace !== null && typeof namespace !== 'string') return undefined;
126
+ return { namespace, branch: inherit.ownBranch };
127
+ }
128
+
129
+ /**
130
+ * The own slot a spec or record declares, or, where it declares none (one
131
+ * written before `ownSlot` existed), the slot it meant: the shared slot for
132
+ * one that reads that slot with no observe edge on it, which is how a
133
+ * resident was declared; a shared derivation's slot; else the isolated slot
134
+ * of its namespace. A subconscious declared its reading of the shared slot
135
+ * as an observe edge from the start (subconsciousSpec), so it reads as
136
+ * isolated, as it was opened. An edge on the shared slot here is one with
137
+ * no `slot`, whatever agent it names: when the flag was all there was, such
138
+ * an edge read the shared slot, and that is how withOwnSlot restores it, so
139
+ * a spec reads as the same record does when it is restored.
140
+ */
141
+ function resolveOwnSlot(declared: {
142
+ ownSlot?: MessageSlotRef;
143
+ readsSharedSlot?: boolean;
144
+ observes?: ObserveEdge[];
145
+ inherit?: ContextInheritance;
146
+ namespace?: string;
147
+ }): MessageSlotRef | undefined {
148
+ const own = declared.ownSlot;
149
+ if (own) return { namespace: own.namespace, ...(own.branch !== undefined ? { branch: own.branch } : {}) };
150
+ const derived = derivedSlot(declared.inherit);
151
+ if (derived) return derived;
152
+ if (declared.readsSharedSlot === true && !(declared.observes ?? []).some((edge) => edge.slot === undefined)) {
153
+ return { namespace: null };
154
+ }
155
+ return typeof declared.namespace === 'string' ? { namespace: declared.namespace } : undefined;
156
+ }
157
+
158
+ /** What a record's deprecated `readsSharedSlot` says: it owns the residents' slot or observes it. */
159
+ function readsResidentsSlot(
160
+ ownSlot: MessageSlotRef | undefined,
161
+ observes: ObserveEdge[],
162
+ ownSlotOf: (agent: AgentName) => MessageSlotRef | undefined,
163
+ ): boolean {
164
+ return sameSlot(ownSlot, RESIDENTS_SLOT) || observes.some((edge) => sameSlot(observedSlot(edge, ownSlotOf), RESIDENTS_SLOT));
165
+ }
166
+
167
+ /**
168
+ * A restored record with its own slot: one written before `ownSlot` existed
169
+ * gets the slot its `readsSharedSlot`, observe edges, inheritance and
170
+ * namespace meant (resolveOwnSlot). Its `readsSharedSlot` is kept as written.
171
+ *
172
+ * Its observe edges are read as that code read them too: an edge with no
173
+ * `slot` read the residents' shared slot, whatever agent it named, so one
174
+ * naming an agent is restored without the name, an edge on the shared slot
175
+ * (which names neither), rather than one on that agent's own slot, which is
176
+ * what such an edge reads since own slots. A host that wants the agent's
177
+ * slot read declares the edge again (`{ agent }` in a spec now reads it).
178
+ */
179
+ function withOwnSlot(record: AgentRecord): AgentRecord {
180
+ if (record.ownSlot !== undefined) return record;
181
+ const observes = record.relationships?.observes;
182
+ if (Array.isArray(observes)) {
183
+ record.relationships.observes = observes.map((edge) => {
184
+ if (edge.slot !== undefined || edge.agent === undefined) return edge;
185
+ const { agent: _named, ...onShared } = edge;
186
+ return onShared;
187
+ });
188
+ }
189
+ const ownSlot = resolveOwnSlot({
190
+ readsSharedSlot: record.readsSharedSlot,
191
+ observes: record.relationships?.observes,
192
+ inherit: record.inherit,
193
+ namespace: record.namespace,
194
+ });
195
+ if (ownSlot) record.ownSlot = ownSlot;
196
+ return record;
197
+ }
198
+
79
199
  export class AgentRegistry {
80
200
  private readonly live = new Map<AgentName, AgentRecord>();
81
201
  /** Ended records, oldest first. */
@@ -128,19 +248,25 @@ export class AgentRegistry {
128
248
  * spawner's orphan, as if it had been live when the spawner ended.
129
249
  * `after` is the incarnation a resumption continues from: the new one is
130
250
  * past it, since the counter of a name that aged out of memory isn't kept.
251
+ * `declaredWith` names the agents declared with this one, such as a
252
+ * configuration's residents, which register one after another: an observe
253
+ * edge may name one of them before it is registered (two residents may
254
+ * observe each other). The edge reads that agent's slot once it is.
131
255
  */
132
- register(spec: AgentSpec, options: { resume?: boolean; after?: number } = {}): AgentRecord {
256
+ register(spec: AgentSpec, options: RegisterOptions = {}): AgentRecord {
133
257
  return this.requested(() => this.registerUnguarded(spec, options));
134
258
  }
135
259
 
136
- private registerUnguarded(spec: AgentSpec, options: { resume?: boolean; after?: number } = {}): AgentRecord {
137
- this.validate(spec, options.resume === true);
260
+ private registerUnguarded(spec: AgentSpec, options: RegisterOptions = {}): AgentRecord {
261
+ this.validate(spec, options.resume === true, options.declaredWith ?? []);
138
262
  const previous = this.awaiting.get(spec.name);
139
263
  this.awaiting.delete(spec.name);
140
264
 
141
265
  const incarnation = Math.max(this.incarnations.get(spec.name) ?? 0, options.after ?? 0) + 1;
142
266
  this.incarnations.set(spec.name, incarnation);
143
267
 
268
+ const observes = (spec.observes ?? []).map((edge) => ({ ...edge }));
269
+ const ownSlot = resolveOwnSlot({ ...spec, observes });
144
270
  const record: AgentRecord = {
145
271
  name: spec.name,
146
272
  kind: spec.kind,
@@ -152,7 +278,7 @@ export class AgentRegistry {
152
278
  onParentEnd: spec.onParentEnd ?? 'orphan',
153
279
  relationships: {
154
280
  ...(spec.spawnedBy ? { spawnedBy: spec.spawnedBy } : {}),
155
- observes: (spec.observes ?? []).map((edge) => ({ ...edge })),
281
+ observes,
156
282
  // Explicit message paths accumulate over an identity's life.
157
283
  messagePeers: previous ? [...previous.relationships.messagePeers] : [],
158
284
  ...(spec.resultTo ? { resultTo: { ...spec.resultTo } } : {}),
@@ -161,7 +287,10 @@ export class AgentRegistry {
161
287
  ...(spec.selfParticipants?.length ? { selfParticipants: [...spec.selfParticipants] } : {}),
162
288
  ...(spec.presentAs ? { presentAs: spec.presentAs } : {}),
163
289
  ...(spec.homeChannel ? { homeChannel: { serverId: spec.homeChannel.serverId, channelId: spec.homeChannel.channelId } } : {}),
164
- readsSharedSlot: spec.readsSharedSlot ?? false,
290
+ ...(ownSlot ? { ownSlot } : {}),
291
+ // Kept for readers of the record from before the split: derived from
292
+ // it, and again as agents register (refreshReadsSharedSlot).
293
+ readsSharedSlot: readsResidentsSlot(ownSlot, observes, (name) => this.recordedSlotOf(name)),
165
294
  ...(spec.namespace ? { namespace: spec.namespace } : {}),
166
295
  ...(spec.metadata ? { metadata: clone(spec.metadata) } : {}),
167
296
  createdAt: previous?.createdAt ?? this.now(),
@@ -181,11 +310,22 @@ export class AgentRegistry {
181
310
  };
182
311
  }
183
312
  this.live.set(spec.name, record);
313
+ // The deprecated flag of each live record, the new one's included, as
314
+ // the slots it names read now: an edge on an agent declared with it
315
+ // (declaredWith) reads that agent's slot only once it is registered.
316
+ this.refreshReadsSharedSlot();
184
317
  this.persist();
185
318
  return record;
186
319
  }
187
320
 
188
- private validate(spec: AgentSpec, resume = false): void {
321
+ /** Each live record's deprecated `readsSharedSlot`, derived again from its own slot and edges (readsResidentsSlot). */
322
+ private refreshReadsSharedSlot(): void {
323
+ for (const record of this.live.values()) {
324
+ record.readsSharedSlot = readsResidentsSlot(record.ownSlot, record.relationships.observes, (name) => this.recordedSlotOf(name));
325
+ }
326
+ }
327
+
328
+ private validate(spec: AgentSpec, resume = false, declaredWith: readonly AgentName[] = []): void {
189
329
  const fail = (problem: string): never => {
190
330
  throw new AgentSpecError(spec.name || '(unnamed)', problem);
191
331
  };
@@ -233,7 +373,7 @@ export class AgentRegistry {
233
373
  fail(`context source "${spec.inherit.from}" is not a registered agent`);
234
374
  }
235
375
  for (const edge of spec.observes ?? []) {
236
- if (edge.agent !== undefined && !this.live.has(edge.agent)) {
376
+ if (edge.agent !== undefined && !this.live.has(edge.agent) && !declaredWith.includes(edge.agent)) {
237
377
  fail(`observed agent "${edge.agent}" is not a registered agent`);
238
378
  }
239
379
  }
@@ -265,7 +405,9 @@ export class AgentRegistry {
265
405
  for (const { ended } of this.interruptedPending.values()) {
266
406
  if (ended.name === name && (!newest || ended.incarnation > newest.incarnation)) newest = ended;
267
407
  }
268
- return newest ?? this.findEnded?.(name);
408
+ if (newest) return newest;
409
+ const logged = this.findEnded?.(name);
410
+ return logged && withOwnSlot(clone(logged));
269
411
  }
270
412
 
271
413
  /** Flat list of agents — the discovery surface. Ended ones on request. */
@@ -322,15 +464,65 @@ export class AgentRegistry {
322
464
  return this.live.get(name)?.roles.ownsProviderScheduling === true;
323
465
  }
324
466
 
325
- /** Agents reading the residents' shared message slot. */
467
+ /**
468
+ * Agents reading the residents' shared message slot.
469
+ *
470
+ * @deprecated Use `readersOf(RESIDENTS_SLOT)`, or `slotOwners` and
471
+ * `observersOf` where owning and observing differ.
472
+ */
326
473
  sharedSlotReaders(): AgentName[] {
327
474
  return this.namesWhere((r) => r.readsSharedSlot);
328
475
  }
329
476
 
477
+ /** @deprecated Use `ownSlotOf` and `observersOf`. */
330
478
  readsSharedSlot(name: AgentName): boolean {
331
479
  return this.live.get(name)?.readsSharedSlot === true;
332
480
  }
333
481
 
482
+ /** The slot a live agent's own rows are stored in, if it names one. */
483
+ ownSlotOf(name: AgentName): MessageSlotRef | undefined {
484
+ const slot = this.live.get(name)?.ownSlot;
485
+ return slot ? { ...slot } : undefined;
486
+ }
487
+
488
+ /** Live agents whose own rows are stored in `slot`, in registration order. */
489
+ slotOwners(slot: MessageSlotRef): AgentName[] {
490
+ return this.namesWhere((r) => sameSlot(r.ownSlot, slot));
491
+ }
492
+
493
+ /**
494
+ * The slot an observe edge reads (observedSlot): an edge that names an
495
+ * agent and no slot reads that agent's own slot, as its record names it,
496
+ * the live one, else the one awaiting re-declaration, else its newest
497
+ * ended one, so an edge on an agent that ended still reads the slot it
498
+ * read. Undefined for an agent the registry doesn't know.
499
+ */
500
+ observedSlotOf(edge: ObserveEdge): MessageSlotRef | undefined {
501
+ return observedSlot(edge, (name) => this.recordedSlotOf(name));
502
+ }
503
+
504
+ /**
505
+ * Live agents that read `slot` through an observe edge, in registration
506
+ * order. With `held`, only those whose edge shows held (policy-diverted)
507
+ * traffic.
508
+ */
509
+ observersOf(slot: MessageSlotRef, options: { held?: boolean } = {}): AgentName[] {
510
+ return this.namesWhere((r) => r.relationships.observes.some(
511
+ (edge) => sameSlot(this.observedSlotOf(edge), slot) && (!options.held || edge.includeHeld === true),
512
+ ));
513
+ }
514
+
515
+ /** Every live agent that reads `slot`, as its own or through an observe edge, in registration order. */
516
+ readersOf(slot: MessageSlotRef): AgentName[] {
517
+ return this.namesWhere((r) => sameSlot(r.ownSlot, slot)
518
+ || r.relationships.observes.some((edge) => sameSlot(this.observedSlotOf(edge), slot)));
519
+ }
520
+
521
+ /** The own slot an agent's record names: live, else awaiting re-declaration, else its newest ended record. */
522
+ private recordedSlotOf(name: AgentName): MessageSlotRef | undefined {
523
+ return (this.live.get(name) ?? this.awaiting.get(name) ?? this.inspect(name))?.ownSlot;
524
+ }
525
+
334
526
  /** The channel an agent is bound to, if it has one. */
335
527
  homeChannel(name: AgentName): AgentRecord['homeChannel'] {
336
528
  return this.live.get(name)?.homeChannel;
@@ -598,7 +790,7 @@ export class AgentRegistry {
598
790
  restore(snapshot: RegistrySnapshot | null | undefined, endedHistory: AgentRecord[] = []): RestoreOutcome {
599
791
  const interrupted: AgentRecord[] = [];
600
792
  for (const raw of endedHistory.slice(-this.endedRetention)) {
601
- if (raw && typeof raw === 'object' && raw.ended) this.ended.push(clone(raw));
793
+ if (raw && typeof raw === 'object' && raw.ended) this.ended.push(withOwnSlot(clone(raw)));
602
794
  }
603
795
  if (!snapshot) return { interrupted };
604
796
  if (snapshot.version !== 1) {
@@ -623,7 +815,7 @@ export class AgentRegistry {
623
815
  const endedKeys = new Set(this.ended.map((record) => `${record.name}#${record.incarnation}`));
624
816
  let dropped = false;
625
817
  for (const raw of snapshot.records) {
626
- const record = clone(raw);
818
+ const record = withOwnSlot(clone(raw));
627
819
  if (record.ended) continue; // history lives in the ended log
628
820
  if (record.lifetime.kind === 'persistent') {
629
821
  this.awaiting.set(record.name, record);
@@ -649,12 +841,17 @@ export class AgentRegistry {
649
841
  * the newest `endedRetention`. An end whose write fails is reported and
650
842
  * stays pending, so the snapshot still lists it and a later settlement (the
651
843
  * next boot's) writes it; a failed snapshot write is reported too.
844
+ * `unsettled` names jobs the host couldn't tell of yet, or whose notices
845
+ * for the primary it couldn't pass on (by name and incarnation): they stay
846
+ * pending, as such a failed end does.
652
847
  * @internal For the framework's boot, once the interrupted jobs'
653
848
  * recipients are told; not a host API.
654
849
  */
655
- settleInterrupted(): void {
850
+ settleInterrupted(unsettled: ReadonlyArray<Pick<AgentRecord, 'name' | 'incarnation'>> = []): void {
656
851
  if (this.interruptedPending.size === 0) return;
852
+ const kept = new Set(unsettled.map((record) => `${record.name}#${record.incarnation}`));
657
853
  for (const [key, { ended }] of [...this.interruptedPending]) {
854
+ if (kept.has(key)) continue;
658
855
  try {
659
856
  this.appendEndedFn?.(clone(ended));
660
857
  } catch (error) {
@@ -122,14 +122,57 @@ export interface ResultRoute {
122
122
  as: 'message' | 'tool-result';
123
123
  }
124
124
 
125
- /** Read access to another slot, merged into this agent's view. */
125
+ /**
126
+ * A message slot: where an agent's rows are stored, or a slot another agent
127
+ * reads. `namespace` null is the residents' shared un-namespaced slot; a
128
+ * string is the isolated slot of that namespace. `branch` is set for a
129
+ * derived agent, whose slot is its parent's slot on a Chronicle branch of
130
+ * its own; without it the slot is the one on the host's current branch,
131
+ * where residents and the agents beside them write. Two refs name one slot
132
+ * when both fields are equal (sameSlot).
133
+ */
134
+ export interface MessageSlotRef {
135
+ namespace: string | null;
136
+ branch?: string;
137
+ }
138
+
139
+ /**
140
+ * Read access to a slot that isn't the agent's own, merged into its view:
141
+ * the relationship a watcher has with the slot it watches (the subconscious
142
+ * with the residents' slot). The slot read is the one `slot` names, else the
143
+ * own slot of the agent `agent` names, else the residents' shared slot (the
144
+ * registry's `observedSlotOf`).
145
+ */
126
146
  export interface ObserveEdge {
127
- /** The agent whose slot is read, when the slot belongs to one. */
147
+ /**
148
+ * The agent whose slot is read. With no `slot`, the edge reads that
149
+ * agent's own slot (its `ownSlot`), on whatever branch that slot is, not
150
+ * the residents' shared slot. Where that slot is the observer's own too (a
151
+ * resident observing another, AgentConfig.observes), the edge shows the
152
+ * observer that agent's own rows there, and no other agent's. It must be
153
+ * registered when the edge is, or declared with it
154
+ * (AgentRegistry.register's `declaredWith`).
155
+ */
128
156
  agent?: AgentName;
129
- /** Message-slot namespace; undefined = the shared un-namespaced slot. */
157
+ /**
158
+ * Message-slot namespace, on the host's current branch. Undefined: the
159
+ * own slot of the agent `agent` names, or the shared un-namespaced slot
160
+ * when it names none.
161
+ */
130
162
  slot?: string;
131
- /** Only messages at or after this sequence. */
163
+ /**
164
+ * Only messages at or after this sequence.
165
+ *
166
+ * @deprecated A sequence is a coordinate of one branch's record, not of
167
+ * what an agent was shown; use `fromMessageId`. Nothing reads either yet.
168
+ */
132
169
  fromSequence?: number;
170
+ /**
171
+ * Only messages from this one on, by message id, which names one row on
172
+ * every branch and in every agent's presentation order (#291). Not read
173
+ * yet: an edge that sets it shows the rows one without it shows.
174
+ */
175
+ fromMessageId?: string;
133
176
  /** Whether held (policy-diverted) traffic in that slot is visible. */
134
177
  includeHeld?: boolean;
135
178
  }
@@ -163,7 +206,15 @@ export interface ContextInheritance {
163
206
  * (requires store support; refused when unavailable).
164
207
  */
165
208
  mode: 'copy' | 'shared';
166
- /** The parent checkpoint (sequence on the parent's branch). Filled at creation. */
209
+ /**
210
+ * The parent checkpoint: a sequence on the parent's branch. Filled at
211
+ * creation, from the derived context (its derivation's checkpoint). One a
212
+ * host sets, for a resume or a derive from an earlier state, is a
213
+ * presentation boundary (#291), such as `currentSequence()` read after a
214
+ * presentation into the parent's window, not a row's own sequence: a row
215
+ * enters the window at a write after its own, so a cut at its own
216
+ * sequence holds the row in the child's record and not in its window.
217
+ */
167
218
  atSequence?: number;
168
219
  /** The parent branch the checkpoint is on. Filled at creation. */
169
220
  branch?: string;
@@ -213,9 +264,10 @@ export interface AgentSpec {
213
264
  kind: AgentKind;
214
265
  /**
215
266
  * The model the agent runs on — "whose weights", next to `inherit.from`
216
- * for "whose context". Together they are what a consent decision about a
217
- * derived agent is made on. Filled by the framework from the agent's
218
- * configuration.
267
+ * for "whose context". Both are on the `dendrite:agent-created` trace and
268
+ * in the spec a host's `dendrite.beforeDerive` decides on, which the
269
+ * framework gives no meaning of its own. Filled by the framework from the
270
+ * agent's configuration.
219
271
  */
220
272
  model?: string;
221
273
  roles: AgentRoles;
@@ -242,9 +294,27 @@ export interface AgentSpec {
242
294
  * registry's `ChannelRef`).
243
295
  */
244
296
  homeChannel?: { serverId: string; channelId: string };
297
+ /**
298
+ * The slot this agent's own rows are stored in: the residents' shared
299
+ * slot for a resident, its own namespace's slot for an isolated agent, the
300
+ * parent's slot on its own branch for a shared fork. The resident and
301
+ * subconscious presets set it (the others leave it to the reading below),
302
+ * and the framework fills it for the agents it derives or creates from
303
+ * how it opens their context. Absent, the registry reads it as an older
304
+ * spec meant it: a shared derivation's slot; the shared slot when
305
+ * `readsSharedSlot` is set and no observe edge without a `slot` explains
306
+ * it (one naming an agent reads as on the shared slot here);
307
+ * else the isolated slot of `namespace`. A slot it reads without owning
308
+ * it is an observe edge (`observes`).
309
+ */
310
+ ownSlot?: MessageSlotRef;
245
311
  /**
246
312
  * Whether the agent reads the residents' shared un-namespaced message
247
313
  * slot (as its own slot or through an observe edge).
314
+ *
315
+ * @deprecated One flag for two relationships: owning the shared slot and
316
+ * observing it. Use `ownSlot` and `observes`. Still read for a spec that
317
+ * sets no `ownSlot` (see there).
248
318
  */
249
319
  readsSharedSlot?: boolean;
250
320
  /** Chronicle namespace owning this agent's context. */
@@ -315,6 +385,20 @@ export interface AgentRecord {
315
385
  * as a wake that names a channel without its server is.
316
386
  */
317
387
  homeChannel?: { serverId: string; channelId: string } | string;
388
+ /**
389
+ * The slot the agent's own rows are stored in (see AgentSpec.ownSlot). A
390
+ * record written before the field existed has it filled at restore from
391
+ * what the record says. Absent only for an agent whose spec named neither
392
+ * a slot nor a namespace.
393
+ */
394
+ ownSlot?: MessageSlotRef;
395
+ /**
396
+ * Whether the agent reads the residents' shared slot, as its own slot or
397
+ * through an observe edge: derived from `ownSlot` and `relationships.observes`
398
+ * at each registration of the record (a restored one included), not once.
399
+ *
400
+ * @deprecated Use `ownSlot` and `relationships.observes`.
401
+ */
318
402
  readsSharedSlot: boolean;
319
403
  namespace?: string;
320
404
  metadata?: Record<string, unknown>;
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Another agent's rounds as an agent that watches its slot is sent them
3
+ * (#291, answer 13).
4
+ *
5
+ * An agent reads another agent's rounds through an observe edge (decision
6
+ * 1), as the persistent subconscious watches the residents' slot, and an
7
+ * agent derived or forked from such a watcher inherits them, sent in its
8
+ * mode. A co-resident's rows stored before audience stamps, which a window
9
+ * written down from the record keeps, reach an agent that observes no one
10
+ * too: they go out as stored there, as earlier releases sent them. An agent
11
+ * sent others' rounds as a transcript is sent those as one, read by their
12
+ * participant.
13
+ * Those rounds are stored as the other agent's: its signed thinking, its
14
+ * tool calls and their results. None of that is valid as the watcher's own.
15
+ * A provider takes tool blocks only in their own pairing, in the turns of
16
+ * the agent that made the call, and a thinking block's signature binds it
17
+ * to the prefix the other agent was sent. Sent raw in a user turn, the
18
+ * blocks are refused, or moved into the watcher's assistant turns, where
19
+ * they read as calls the watcher made.
20
+ *
21
+ * So by default the watcher is sent them as a transcript, a line per block:
22
+ * a call as the tool's name and its input in brief, a result as the tool's
23
+ * name and its outcome in brief, and no thinking at all. Text, images and
24
+ * the rest go as stored. The signature forbids sending a thinking block as
25
+ * part of another prefix, not its text: leaving the text out is this
26
+ * transcript's choice. A host can have the rounds sent as stored instead
27
+ * (PersistentReaderConfig.foreignRounds `'raw'`, as 0.30.0 sent them). The rule is one function of the stored row, applied the same
28
+ * way to a compile's messages and to a row a tool boundary injects, so what
29
+ * the watcher's window holds and what its requests carried stay one thing.
30
+ *
31
+ * The watcher's own rows go as stored: its rounds (its own participant's
32
+ * messages), and the results of its own calls, which are stored under
33
+ * another participant (`user`) and are told by the call they answer: the
34
+ * nearest call before the result with its id, since a provider (or a mock)
35
+ * that numbers calls per response gives two agents' calls the same id.
36
+ */
37
+ import type { ContentBlock, NormalizedMessage } from '@animalabs/membrane';
38
+ import { NOTICE_TAG } from './notice-tags.js';
39
+ import { cutToFit, markedTexts, oneLine } from './supplied-text.js';
40
+
41
+ /** How long one transcript line's call or outcome runs before it is cut. */
42
+ const BRIEF_CHARS = 160;
43
+
44
+ /** A piece of a transcript line's call or outcome, and whether the other
45
+ * agent supplied it (a call's input, a result's text) or the framework
46
+ * wrote it (a placeholder naming a result block's kind). */
47
+ interface Piece {
48
+ text: string;
49
+ supplied: boolean;
50
+ }
51
+
52
+ /** `text` on one line: each line break the framework counts as one
53
+ * (oneLine), and each other run of whitespace, as one space. */
54
+ function flat(text: string): string {
55
+ return oneLine(text).replace(/\s+/g, ' ').trim();
56
+ }
57
+
58
+ /** A tool result's content as pieces: its text, and its other blocks named
59
+ * by kind, in placeholders that are the framework's own. */
60
+ function resultPieces(content: string | ContentBlock[]): Piece[] {
61
+ if (typeof content === 'string') return [{ text: content, supplied: true }];
62
+ return content.map((b) => (b.type === 'text'
63
+ ? { text: (b as { text: string }).text, supplied: true }
64
+ : { text: `[${b.type}]`, supplied: false }));
65
+ }
66
+
67
+ /** A call's input as a piece: its JSON, or what the model sent when that
68
+ * wasn't an object. */
69
+ function callPiece(block: Extract<ContentBlock, { type: 'tool_use' }>): Piece {
70
+ if (block.unparseableInput !== undefined) return { text: block.unparseableInput, supplied: true };
71
+ try {
72
+ return { text: JSON.stringify(block.input ?? {}) ?? '', supplied: true };
73
+ } catch {
74
+ return { text: '', supplied: true };
75
+ }
76
+ }
77
+
78
+ /**
79
+ * A transcript line: the framework's bracket around the tool's name, which
80
+ * the other agent's call supplied, the framework's words for what happened
81
+ * (`words[0]` before a call or outcome, `words[1]` when there is none), and
82
+ * the call or outcome in brief. What the line holds is supplied text
83
+ * (#852): set inside the framework's bracket here, where a tool block's own
84
+ * content isn't, an opening of one of the framework's notices is marked
85
+ * wherever it forms, read across the whole line (markedTexts): inside a
86
+ * piece, across a result's blocks, or where the name meets the bracket or
87
+ * the framework's words. Those words and the placeholders aren't marked.
88
+ * Each piece goes on one line first (flat), and the call or outcome is cut
89
+ * to BRIEF_CHARS with an ellipsis, at a whole character, never parting a
90
+ * mark from its bracket (cutToFit).
91
+ */
92
+ function transcriptLine(name: string, words: readonly [string, string], body: readonly Piece[]): string {
93
+ const pieces = body.map((p) => ({ ...p, text: flat(p.text) })).filter((p) => p.text.length > 0);
94
+ const word = pieces.length > 0 ? words[0] : words[1];
95
+ const marked = markedTexts(['[', oneLine(name), word, ...pieces.map((p) => p.text)], [true, true, false, ...pieces.map((p) => p.supplied)]);
96
+ const said = marked.slice(3).join(' ');
97
+ return `${marked.slice(0, 3).join('')}${said.length > BRIEF_CHARS ? `${cutToFit(said, BRIEF_CHARS - 1)}…` : said}]`;
98
+ }
99
+
100
+ /** Where a row with nothing left to send stands, so the transcript keeps a row for each stored one. */
101
+ const NOTHING_SAID = `[${NOTICE_TAG.noText}]`;
102
+
103
+ /**
104
+ * One stored row of another agent's as the watcher is sent it: thinking
105
+ * dropped, each call and each result a line of text, a text block's
106
+ * provider-native item (`rawItem`) dropped with it, everything else as it is.
107
+ * `ownCalls` are the watcher's own calls, whose results it is sent as they
108
+ * are; `callNames` names the calls the row's results answer, where the
109
+ * result doesn't name its tool itself. A row left with nothing says so in
110
+ * a line, rather than going empty.
111
+ */
112
+ export function foreignRowAsSent(
113
+ content: readonly ContentBlock[],
114
+ context: { ownCalls?: { has(id: string): boolean }; callNames?: { get(id: string): string | undefined } } = {},
115
+ ): ContentBlock[] {
116
+ let changed = false;
117
+ const out: ContentBlock[] = [];
118
+ for (const block of content) {
119
+ switch (block.type) {
120
+ case 'thinking':
121
+ case 'redacted_thinking':
122
+ changed = true;
123
+ break;
124
+ case 'tool_use': {
125
+ changed = true;
126
+ out.push({ type: 'text', text: transcriptLine(block.name, [' called: ', ' called: '], [callPiece(block)]) });
127
+ break;
128
+ }
129
+ case 'tool_result': {
130
+ if (context.ownCalls?.has(block.toolUseId)) {
131
+ out.push(block);
132
+ break;
133
+ }
134
+ changed = true;
135
+ const name = block.toolName ?? context.callNames?.get(block.toolUseId) ?? 'a tool';
136
+ const word = block.isError ? ' failed' : ' returned';
137
+ out.push({ type: 'text', text: transcriptLine(name, [`${word}: `, word], resultPieces(block.content)) });
138
+ break;
139
+ }
140
+ case 'text': {
141
+ if ((block as { rawItem?: unknown }).rawItem === undefined) {
142
+ out.push(block);
143
+ break;
144
+ }
145
+ changed = true;
146
+ if (block.text.length > 0) out.push({ type: 'text', text: block.text });
147
+ break;
148
+ }
149
+ default:
150
+ out.push(block);
151
+ }
152
+ }
153
+ if (!changed) return content as ContentBlock[];
154
+ return out.length > 0 ? out : [{ type: 'text', text: NOTHING_SAID }];
155
+ }
156
+
157
+ /**
158
+ * A request's messages as an agent that watches another's slot is sent them:
159
+ * each message whose participant isn't the agent's own (`isOwn`) through
160
+ * foreignRowAsSent, with the results of the agent's own calls kept, and
161
+ * each result named by the call it answers. A result answers the nearest
162
+ * call before it with its id, read in one pass in order, so another
163
+ * agent's result whose id an own call of the watcher's also has, before or
164
+ * after it, is still transcribed. A call answers one result: a result after
165
+ * the one that answered it, in the same message or a later one, names no
166
+ * call (another agent's, whose call the request doesn't carry), and is
167
+ * transcribed. A message nothing changes is the same object, and so is the
168
+ * array when none changes.
169
+ */
170
+ export function transcribeForeignRounds(
171
+ messages: readonly NormalizedMessage[],
172
+ isOwn: (participant: string) => boolean,
173
+ ): NormalizedMessage[] {
174
+ const latest = new Map<string, { own: boolean; name: string }>();
175
+ // The watcher's own call is answered by the first result read for it,
176
+ // block by block: that result takes its entry, so a second result for it
177
+ // in the same message names no call.
178
+ const ownCalls = {
179
+ has: (id: string) => {
180
+ if (latest.get(id)?.own !== true) return false;
181
+ latest.delete(id);
182
+ return true;
183
+ },
184
+ };
185
+ const callNames = { get: (id: string) => latest.get(id)?.name };
186
+ let changed = false;
187
+ const out = messages.map((m) => {
188
+ const own = isOwn(m.participant);
189
+ for (const b of m.content) if (b.type === 'tool_use') latest.set(b.id, { own, name: b.name });
190
+ const content = own ? m.content : foreignRowAsSent(m.content, { ownCalls, callNames });
191
+ // A call answers one result: a later result with its id (another
192
+ // agent's, whose call the request doesn't carry) answers no call here.
193
+ for (const b of m.content) if (b.type === 'tool_result') latest.delete(b.toolUseId);
194
+ if (content === m.content) return m;
195
+ changed = true;
196
+ return { ...m, content };
197
+ });
198
+ return changed ? out : messages as NormalizedMessage[];
199
+ }