@dxos/assistant 0.8.4-main.fffef41 → 0.8.4-staging.60fe92afc8

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 (174) hide show
  1. package/LICENSE +102 -5
  2. package/README.md +2 -1
  3. package/dist/lib/{browser → neutral}/extraction/index.mjs +42 -59
  4. package/dist/lib/neutral/extraction/index.mjs.map +7 -0
  5. package/dist/lib/neutral/index.mjs +1886 -0
  6. package/dist/lib/neutral/index.mjs.map +7 -0
  7. package/dist/lib/neutral/meta.json +1 -0
  8. package/dist/types/src/extraction/extraction-ner-function/index.d.ts +0 -1
  9. package/dist/types/src/extraction/extraction-ner-function/index.d.ts.map +1 -1
  10. package/dist/types/src/extraction/extraction-ner-function/named-entity-recognition.d.ts.map +1 -1
  11. package/dist/types/src/extraction/extraction.d.ts +1649 -317
  12. package/dist/types/src/extraction/extraction.d.ts.map +1 -1
  13. package/dist/types/src/extraction/index.d.ts +0 -1
  14. package/dist/types/src/extraction/index.d.ts.map +1 -1
  15. package/dist/types/src/extraction/quotes.d.ts +2 -2
  16. package/dist/types/src/extraction/quotes.d.ts.map +1 -1
  17. package/dist/types/src/functions/services.d.ts +14 -16
  18. package/dist/types/src/functions/services.d.ts.map +1 -1
  19. package/dist/types/src/index.d.ts +2 -5
  20. package/dist/types/src/index.d.ts.map +1 -1
  21. package/dist/types/src/request/AiRequest.d.ts +308 -0
  22. package/dist/types/src/request/AiRequest.d.ts.map +1 -0
  23. package/dist/types/src/{session → request}/artifact-diff.d.ts +4 -4
  24. package/dist/types/src/request/artifact-diff.d.ts.map +1 -0
  25. package/dist/types/src/request/format.d.ts +15 -0
  26. package/dist/types/src/request/format.d.ts.map +1 -0
  27. package/dist/types/src/request/index.d.ts +4 -0
  28. package/dist/types/src/request/index.d.ts.map +1 -0
  29. package/dist/types/src/{session → request}/observer.d.ts +4 -3
  30. package/dist/types/src/request/observer.d.ts.map +1 -0
  31. package/dist/types/src/{session → request}/version-pin.d.ts +4 -4
  32. package/dist/types/src/request/version-pin.d.ts.map +1 -0
  33. package/dist/types/src/session/AiContext.d.ts +178 -0
  34. package/dist/types/src/session/AiContext.d.ts.map +1 -0
  35. package/dist/types/src/session/AiContext.test.d.ts +2 -0
  36. package/dist/types/src/session/AiContext.test.d.ts.map +1 -0
  37. package/dist/types/src/session/AiSession.d.ts +79 -0
  38. package/dist/types/src/session/AiSession.d.ts.map +1 -0
  39. package/dist/types/src/session/AiSession.test.d.ts +2 -0
  40. package/dist/types/src/session/AiSession.test.d.ts.map +1 -0
  41. package/dist/types/src/session/SessionLink.d.ts +17 -0
  42. package/dist/types/src/session/SessionLink.d.ts.map +1 -0
  43. package/dist/types/src/session/SessionLoader.d.ts +15 -0
  44. package/dist/types/src/session/SessionLoader.d.ts.map +1 -0
  45. package/dist/types/src/session/SessionLoader.test.d.ts +2 -0
  46. package/dist/types/src/session/SessionLoader.test.d.ts.map +1 -0
  47. package/dist/types/src/session/index.d.ts +4 -5
  48. package/dist/types/src/session/index.d.ts.map +1 -1
  49. package/dist/types/src/session/toolkit.d.ts +10 -9
  50. package/dist/types/src/session/toolkit.d.ts.map +1 -1
  51. package/dist/types/src/templates/system.d.ts +12 -3
  52. package/dist/types/src/templates/system.d.ts.map +1 -1
  53. package/dist/types/src/util/artifact.d.ts +18 -0
  54. package/dist/types/src/util/artifact.d.ts.map +1 -0
  55. package/dist/types/src/{diff.d.ts → util/diff.d.ts} +4 -2
  56. package/dist/types/src/util/diff.d.ts.map +1 -0
  57. package/dist/types/src/{diff.test.d.ts.map → util/diff.test.d.ts.map} +1 -1
  58. package/dist/types/src/util/error-handling.d.ts.map +1 -0
  59. package/dist/types/src/{errors.d.ts → util/errors.d.ts} +24 -28
  60. package/dist/types/src/util/errors.d.ts.map +1 -0
  61. package/dist/types/src/{execution-graph.d.ts → util/execution-graph.d.ts} +2 -2
  62. package/dist/types/src/util/execution-graph.d.ts.map +1 -0
  63. package/dist/types/src/util/execution-graph.test.d.ts.map +1 -0
  64. package/dist/types/src/util/index.d.ts +7 -0
  65. package/dist/types/src/util/index.d.ts.map +1 -0
  66. package/dist/types/src/util/tracing.d.ts +296 -0
  67. package/dist/types/src/util/tracing.d.ts.map +1 -0
  68. package/dist/types/tsconfig.tsbuildinfo +1 -1
  69. package/package.json +36 -44
  70. package/src/extraction/extraction-ner-function/index.ts +0 -1
  71. package/src/extraction/extraction.ts +47 -18
  72. package/src/extraction/index.ts +0 -1
  73. package/src/extraction/quotes.test.ts +2 -2
  74. package/src/extraction/quotes.ts +6 -8
  75. package/src/functions/services.ts +129 -63
  76. package/src/index.ts +2 -5
  77. package/src/request/AiRequest.ts +387 -0
  78. package/src/{session → request}/artifact-diff.ts +4 -4
  79. package/src/{session → request}/format.ts +28 -25
  80. package/src/request/index.ts +8 -0
  81. package/src/{session → request}/observer.ts +4 -1
  82. package/src/{session → request}/version-pin.ts +6 -5
  83. package/src/session/AiContext.test.ts +49 -0
  84. package/src/session/AiContext.ts +420 -0
  85. package/src/session/AiSession.test.ts +111 -0
  86. package/src/session/AiSession.ts +328 -0
  87. package/src/session/SessionLink.ts +23 -0
  88. package/src/session/SessionLoader.test.ts +115 -0
  89. package/src/session/SessionLoader.ts +55 -0
  90. package/src/session/index.ts +4 -5
  91. package/src/session/toolkit.ts +27 -28
  92. package/src/templates/instructions/format.tpl +0 -4
  93. package/src/templates/instructions/system.tpl +50 -2
  94. package/src/templates/system.ts +7 -6
  95. package/src/util/artifact.ts +96 -0
  96. package/src/util/diff.test.ts +74 -0
  97. package/src/{diff.ts → util/diff.ts} +14 -5
  98. package/src/{session → util}/error-handling.ts +3 -2
  99. package/src/util/errors.ts +15 -0
  100. package/src/{execution-graph.test.ts → util/execution-graph.test.ts} +4 -4
  101. package/src/{execution-graph.ts → util/execution-graph.ts} +30 -19
  102. package/src/util/index.ts +10 -0
  103. package/src/util/tracing.ts +60 -0
  104. package/dist/lib/browser/extraction/index.mjs.map +0 -7
  105. package/dist/lib/browser/index.mjs +0 -1290
  106. package/dist/lib/browser/index.mjs.map +0 -7
  107. package/dist/lib/browser/meta.json +0 -1
  108. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs +0 -11
  109. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs.map +0 -7
  110. package/dist/lib/node-esm/extraction/index.mjs +0 -226
  111. package/dist/lib/node-esm/extraction/index.mjs.map +0 -7
  112. package/dist/lib/node-esm/index.mjs +0 -1291
  113. package/dist/lib/node-esm/index.mjs.map +0 -7
  114. package/dist/lib/node-esm/meta.json +0 -1
  115. package/dist/types/src/conversation/context.d.ts +0 -103
  116. package/dist/types/src/conversation/context.d.ts.map +0 -1
  117. package/dist/types/src/conversation/conversation.d.ts +0 -41
  118. package/dist/types/src/conversation/conversation.d.ts.map +0 -1
  119. package/dist/types/src/conversation/index.d.ts +0 -3
  120. package/dist/types/src/conversation/index.d.ts.map +0 -1
  121. package/dist/types/src/diff.d.ts.map +0 -1
  122. package/dist/types/src/errors.d.ts.map +0 -1
  123. package/dist/types/src/execution-graph.d.ts.map +0 -1
  124. package/dist/types/src/execution-graph.test.d.ts.map +0 -1
  125. package/dist/types/src/extraction/extraction-llm-function/extraction-llm-function.d.ts +0 -4
  126. package/dist/types/src/extraction/extraction-llm-function/extraction-llm-function.d.ts.map +0 -1
  127. package/dist/types/src/extraction/extraction-llm-function/extraction-llm-function.test.d.ts +0 -2
  128. package/dist/types/src/extraction/extraction-llm-function/extraction-llm-function.test.d.ts.map +0 -1
  129. package/dist/types/src/extraction/extraction-llm-function/index.d.ts +0 -2
  130. package/dist/types/src/extraction/extraction-llm-function/index.d.ts.map +0 -1
  131. package/dist/types/src/extraction/extraction-ner-function/extraction-ner-function.d.ts +0 -345
  132. package/dist/types/src/extraction/extraction-ner-function/extraction-ner-function.d.ts.map +0 -1
  133. package/dist/types/src/extraction/extraction-ner-function/extraction-ner-function.test.d.ts +0 -2
  134. package/dist/types/src/extraction/extraction-ner-function/extraction-ner-function.test.d.ts.map +0 -1
  135. package/dist/types/src/session/GenericToolkit.d.ts +0 -84
  136. package/dist/types/src/session/GenericToolkit.d.ts.map +0 -1
  137. package/dist/types/src/session/artifact-diff.d.ts.map +0 -1
  138. package/dist/types/src/session/error-handling.d.ts.map +0 -1
  139. package/dist/types/src/session/format.d.ts +0 -12
  140. package/dist/types/src/session/format.d.ts.map +0 -1
  141. package/dist/types/src/session/format.test.d.ts +0 -2
  142. package/dist/types/src/session/format.test.d.ts.map +0 -1
  143. package/dist/types/src/session/observer.d.ts.map +0 -1
  144. package/dist/types/src/session/session.d.ts +0 -51
  145. package/dist/types/src/session/session.d.ts.map +0 -1
  146. package/dist/types/src/session/session.test.d.ts +0 -2
  147. package/dist/types/src/session/session.test.d.ts.map +0 -1
  148. package/dist/types/src/session/version-pin.d.ts.map +0 -1
  149. package/dist/types/src/types/artifact.d.ts +0 -20
  150. package/dist/types/src/types/artifact.d.ts.map +0 -1
  151. package/dist/types/src/types/index.d.ts +0 -2
  152. package/dist/types/src/types/index.d.ts.map +0 -1
  153. package/src/conversation/context.ts +0 -186
  154. package/src/conversation/conversation.ts +0 -128
  155. package/src/conversation/index.ts +0 -6
  156. package/src/diff.test.ts +0 -43
  157. package/src/errors.ts +0 -15
  158. package/src/extraction/extraction-llm-function/extraction-llm-function.test.ts +0 -84
  159. package/src/extraction/extraction-llm-function/extraction-llm-function.ts +0 -62
  160. package/src/extraction/extraction-llm-function/index.ts +0 -5
  161. package/src/extraction/extraction-ner-function/extraction-ner-function.test.ts +0 -116
  162. package/src/extraction/extraction-ner-function/extraction-ner-function.ts +0 -56
  163. package/src/session/GenericToolkit.ts +0 -130
  164. package/src/session/format.test.ts +0 -61
  165. package/src/session/session.conversations.json +0 -1
  166. package/src/session/session.test.ts +0 -149
  167. package/src/session/session.ts +0 -213
  168. package/src/types/artifact.ts +0 -71
  169. package/src/types/index.ts +0 -5
  170. /package/dist/lib/{browser → neutral}/chunk-J5LGTIGS.mjs +0 -0
  171. /package/dist/lib/{browser → neutral}/chunk-J5LGTIGS.mjs.map +0 -0
  172. /package/dist/types/src/{diff.test.d.ts → util/diff.test.d.ts} +0 -0
  173. /package/dist/types/src/{session → util}/error-handling.d.ts +0 -0
  174. /package/dist/types/src/{execution-graph.test.d.ts → util/execution-graph.test.d.ts} +0 -0
@@ -0,0 +1,420 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ // @import-as-namespace
6
+
7
+ import { Atom, Registry as AtomRegistry } from '@effect-atom/atom-react';
8
+ import * as EArray from 'effect/Array';
9
+ import * as Context from 'effect/Context';
10
+ import * as Effect from 'effect/Effect';
11
+ import * as Function from 'effect/Function';
12
+ import * as Runtime from 'effect/Runtime';
13
+ import * as Schema from 'effect/Schema';
14
+
15
+ import { Blueprint } from '@dxos/compute';
16
+ import { Resource } from '@dxos/context';
17
+ import { Annotation, DXN, Feed, Obj, type QueryResult, Query, Ref, Type } from '@dxos/echo';
18
+ import { assertArgument } from '@dxos/invariant';
19
+ import { EID, type URI } from '@dxos/keys';
20
+ import { log } from '@dxos/log';
21
+ import { ComplexSet, isNonNullable } from '@dxos/util';
22
+
23
+ /**
24
+ * Thread message that binds or unbinds contextual objects to a conversation.
25
+ */
26
+ export const Binding = Schema.Struct({
27
+ blueprints: Schema.Struct({
28
+ added: Schema.Array(Ref.Ref(Blueprint.Blueprint)),
29
+ removed: Schema.Array(Ref.Ref(Blueprint.Blueprint)),
30
+ }),
31
+
32
+ objects: Schema.Struct({
33
+ added: Schema.Array(Ref.Ref(Obj.Unknown)),
34
+ removed: Schema.Array(Ref.Ref(Obj.Unknown)),
35
+ }),
36
+ }).pipe(Annotation.HiddenAnnotation.set(true), Type.makeObject(DXN.make('org.dxos.type.contextBinding', '0.1.0')));
37
+
38
+ export type Binding = Type.InstanceType<typeof Binding>;
39
+ export type BindingProps = Partial<{
40
+ blueprints: Ref.Ref<Blueprint.Blueprint>[];
41
+ objects: Ref.Ref<Obj.Unknown>[];
42
+ }>;
43
+
44
+ export class Bindings {
45
+ readonly blueprints = new ComplexSet<Ref.Ref<Blueprint.Blueprint>>((ref) => ref.uri);
46
+
47
+ // TODO(burdon): Some DXNs have the Space prefix so only compare the object ID.
48
+ readonly objects = new ComplexSet<Ref.Ref<Obj.Unknown>>((ref) => {
49
+ const echoUri = EID.tryParse(ref.uri);
50
+ return echoUri ? EID.getEntityId(echoUri) : undefined;
51
+ });
52
+
53
+ toJSON(): { blueprints: URI.URI[]; objects: URI.URI[] } {
54
+ return {
55
+ blueprints: EArray.fromIterable(this.blueprints).map((ref) => ref.uri),
56
+ objects: EArray.fromIterable(this.objects).map((ref) => ref.uri),
57
+ };
58
+ }
59
+ }
60
+
61
+ export type BinderOptions = {
62
+ feed: Feed.Feed;
63
+ runtime: Runtime.Runtime<Feed.FeedService>;
64
+ /** @effect-atom/atom-react Registry for reactive state management. */
65
+ registry?: AtomRegistry.Registry;
66
+ };
67
+
68
+ /**
69
+ * Manages bindings of blueprints and objects to a conversation.
70
+ */
71
+ // TODO(burdon): Context should manage ephemeral state of bindings until prompt is issued?
72
+ export class Binder extends Resource {
73
+ private readonly _blueprints = Atom.make<Blueprint.Blueprint[]>([]).pipe(Atom.keepAlive);
74
+ private readonly _objects = Atom.make<Obj.Unknown[]>([]).pipe(Atom.keepAlive);
75
+ private readonly _registry: AtomRegistry.Registry;
76
+ private readonly _feed: Feed.Feed;
77
+ private readonly _runtime: Runtime.Runtime<Feed.FeedService>;
78
+
79
+ #bindingsQuery: QueryResult.QueryResult<Binding> | undefined;
80
+
81
+ constructor(options: BinderOptions) {
82
+ super();
83
+ assertArgument(options.feed, 'options.feed', 'Feed is required');
84
+ assertArgument(options.runtime, 'options.runtime', 'Feed runtime is required');
85
+ this._feed = options.feed;
86
+ this._runtime = options.runtime;
87
+ this._registry = options.registry ?? AtomRegistry.make();
88
+ }
89
+
90
+ /**
91
+ * Returns the blueprints atom for subscription.
92
+ */
93
+ get blueprints(): Atom.Atom<Blueprint.Blueprint[]> {
94
+ return this._blueprints;
95
+ }
96
+
97
+ /**
98
+ * Returns the objects atom for subscription.
99
+ */
100
+ get objects(): Atom.Atom<Obj.Unknown[]> {
101
+ return this._objects;
102
+ }
103
+
104
+ /**
105
+ * Gets the current blueprints value.
106
+ */
107
+ getBlueprints(): Blueprint.Blueprint[] {
108
+ return this._registry.get(this._blueprints);
109
+ }
110
+
111
+ /**
112
+ * Gets the current objects value.
113
+ */
114
+ getObjects(): Obj.Unknown[] {
115
+ return this._registry.get(this._objects);
116
+ }
117
+
118
+ /**
119
+ * Subscribe to changes in blueprints.
120
+ */
121
+ subscribeBlueprints(cb: (blueprints: Blueprint.Blueprint[]) => void): () => void {
122
+ return this._registry.subscribe(this._blueprints, () => cb(this._registry.get(this._blueprints)));
123
+ }
124
+
125
+ /**
126
+ * Subscribe to changes in objects.
127
+ */
128
+ subscribeObjects(cb: (objects: Obj.Unknown[]) => void): () => void {
129
+ return this._registry.subscribe(this._objects, () => cb(this._registry.get(this._objects)));
130
+ }
131
+
132
+ protected override async _open(): Promise<void> {
133
+ this.#bindingsQuery = await Runtime.runPromise(this._runtime)(Feed.query(this._feed, Query.type(Binding)));
134
+
135
+ // Process initial state before returning.
136
+ const initialResults = await this.#bindingsQuery.run();
137
+ await this._updateBindings(initialResults);
138
+
139
+ // Subscribe to future changes.
140
+ this._ctx.onDispose(
141
+ this.#bindingsQuery.subscribe(async () => {
142
+ await this._updateBindings(this.#bindingsQuery!.results);
143
+ }),
144
+ );
145
+ }
146
+
147
+ /**
148
+ * Re-reads bindings from the feed to pick up changes made by other processes.
149
+ */
150
+ async sync(): Promise<void> {
151
+ if (this.#bindingsQuery) {
152
+ const results = await this.#bindingsQuery.run();
153
+ log('sync', { bindingItems: results.length });
154
+ await this._updateBindings(results);
155
+ log('sync complete', {
156
+ blueprints: this._registry.get(this._blueprints).length,
157
+ blueprintKeys: this._registry.get(this._blueprints).map((bp) => Blueprint.getKey(bp)),
158
+ });
159
+ }
160
+ }
161
+
162
+ private async _updateBindings(items: Binding[]): Promise<void> {
163
+ // Skip update if no items - preserve existing state set by bind().
164
+ if (items.length === 0) {
165
+ return;
166
+ }
167
+
168
+ const bindings = this._reduce(items);
169
+
170
+ log('_updateBindings', {
171
+ items: items.length,
172
+ blueprintRefs: [...bindings.blueprints].map((ref) => ({ uri: ref.uri, available: ref.isAvailable })),
173
+ });
174
+
175
+ // Resolve references (loading them first if needed).
176
+ const currentBlueprints = this._registry.get(this._blueprints);
177
+ const currentObjects = this._registry.get(this._objects);
178
+ const resolvedBlueprints = await this._resolve(bindings.blueprints, currentBlueprints);
179
+ const resolvedObjects = await this._resolve(bindings.objects, currentObjects);
180
+
181
+ log('_updateBindings resolved', {
182
+ resolvedBlueprints: resolvedBlueprints.length,
183
+ resolvedBlueprintKeys: resolvedBlueprints.map((bp) => Obj.getMeta(bp).key ?? '<missing>'),
184
+ });
185
+
186
+ // Drop blueprints that have no registry key — they cannot be used downstream
187
+ // (e.g. tool/operation registration calls Blueprint.getKey which throws).
188
+ const keyedBlueprints = resolvedBlueprints.filter((bp) => {
189
+ if (Obj.getMeta(bp).key === undefined) {
190
+ log.warn('dropping blueprint with no meta key', { uri: Obj.getURI(bp) });
191
+ return false;
192
+ }
193
+ return true;
194
+ });
195
+
196
+ // Filter current state to only items still in the reduced binding set,
197
+ // then merge in newly resolved items. This ensures unbind events are respected.
198
+ const reducedBlueprintDxns = new Set<URI.URI>([...bindings.blueprints].map((ref) => ref.uri));
199
+ const reducedObjectDxns = new Set<URI.URI>([...bindings.objects].map((ref) => ref.uri));
200
+ const filteredBlueprints = currentBlueprints.filter((obj) => {
201
+ const uri = Obj.getURI(obj);
202
+ return uri != null && reducedBlueprintDxns.has(uri) && Obj.getMeta(obj).key !== undefined;
203
+ });
204
+ const filteredObjects = currentObjects.filter((obj) => {
205
+ const uri = Obj.getURI(obj);
206
+ return uri != null && reducedObjectDxns.has(uri);
207
+ });
208
+ const mergedBlueprints = this._mergeInto(filteredBlueprints, keyedBlueprints);
209
+ const mergedObjects = this._mergeInto(filteredObjects, resolvedObjects);
210
+
211
+ this._registry.set(this._blueprints, mergedBlueprints);
212
+ this._registry.set(this._objects, mergedObjects);
213
+
214
+ log('updated bindings', {
215
+ blueprints: mergedBlueprints.length,
216
+ objects: mergedObjects.length,
217
+ });
218
+ }
219
+
220
+ protected override async _close(): Promise<void> {
221
+ // Reset atoms to empty state.
222
+ this._registry.set(this._blueprints, []);
223
+ this._registry.set(this._objects, []);
224
+ }
225
+
226
+ async bind({ blueprints, objects }: BindingProps): Promise<void> {
227
+ const currentBlueprints = this._registry.get(this._blueprints);
228
+ const currentObjects = this._registry.get(this._objects);
229
+
230
+ const { added: addedBlueprints, next: nextBlueprints } = this._processBindings(blueprints, currentBlueprints);
231
+ const { added: addedObjects, next: nextObjects } = this._processBindings(objects, currentObjects);
232
+ if (!addedBlueprints.length && !addedObjects.length) {
233
+ return;
234
+ }
235
+
236
+ // Atomic updates - subscribers notified automatically.
237
+ this._registry.set(this._blueprints, nextBlueprints);
238
+ this._registry.set(this._objects, nextObjects);
239
+
240
+ log('bind', { blueprints: addedBlueprints.length, objects: addedObjects.length });
241
+ await Runtime.runPromise(this._runtime)(
242
+ Feed.append(this._feed, [
243
+ Obj.make(Binding, {
244
+ blueprints: {
245
+ added: addedBlueprints,
246
+ removed: [],
247
+ },
248
+ objects: {
249
+ added: addedObjects,
250
+ removed: [],
251
+ },
252
+ }),
253
+ ]),
254
+ );
255
+ }
256
+
257
+ async unbind({ blueprints, objects }: BindingProps): Promise<void> {
258
+ if (!blueprints?.length && !objects?.length) {
259
+ return;
260
+ }
261
+
262
+ // Immediately update atom state so removals are reflected before the queue round-trips.
263
+ const removedBlueprintDxns = (blueprints ?? []).map((ref) => ref.uri);
264
+ const removedObjectDxns = (objects ?? []).map((ref) => ref.uri);
265
+ if (removedBlueprintDxns.length > 0) {
266
+ const current = this._registry.get(this._blueprints);
267
+ this._registry.set(
268
+ this._blueprints,
269
+ current.filter((obj) => !removedBlueprintDxns.some((uri) => Obj.getURI(obj) === uri)),
270
+ );
271
+ }
272
+ if (removedObjectDxns.length > 0) {
273
+ const current = this._registry.get(this._objects);
274
+ this._registry.set(
275
+ this._objects,
276
+ current.filter((obj) => !removedObjectDxns.some((uri) => Obj.getURI(obj) === uri)),
277
+ );
278
+ }
279
+
280
+ log('unbind', { blueprints: blueprints?.length, objects: objects?.length });
281
+ await Runtime.runPromise(this._runtime)(
282
+ Feed.append(this._feed, [
283
+ Obj.make(Binding, {
284
+ blueprints: {
285
+ added: [],
286
+ removed: blueprints ?? [],
287
+ },
288
+ objects: {
289
+ added: [],
290
+ removed: objects ?? [],
291
+ },
292
+ }),
293
+ ]),
294
+ );
295
+ }
296
+
297
+ /**
298
+ * Process bindings to filter duplicates and determine next state.
299
+ */
300
+ private _processBindings<T extends Obj.Unknown>(
301
+ refs: Ref.Ref<T>[] | undefined,
302
+ current: T[],
303
+ ): { added: Ref.Ref<T>[]; next: T[] } {
304
+ const next = [...current];
305
+ const added: Ref.Ref<T>[] = [];
306
+ if (!refs?.length) {
307
+ return { added, next };
308
+ }
309
+
310
+ const seen = new Set<URI.URI>(current.map((obj) => Obj.getURI(obj)));
311
+ for (const ref of refs) {
312
+ const uri = ref.uri;
313
+ if (seen.has(uri)) {
314
+ continue;
315
+ }
316
+ seen.add(uri);
317
+ added.push(ref);
318
+
319
+ // Only resolve target if available (has target or resolver).
320
+ if (ref.isAvailable) {
321
+ const target = ref.target;
322
+ if (target) {
323
+ next.push(target);
324
+ }
325
+ }
326
+ }
327
+
328
+ return { added, next };
329
+ }
330
+
331
+ /**
332
+ * Reduce results into sets of blueprints and objects.
333
+ */
334
+ private _reduce(items: Binding[]): Bindings {
335
+ return Function.pipe(
336
+ items,
337
+ EArray.reduce(new Bindings(), (context, { blueprints, objects }) => {
338
+ blueprints.added.forEach((ref) => context.blueprints.add(ref));
339
+ blueprints.removed.forEach((ref) => context.blueprints.delete(ref));
340
+
341
+ objects.added.forEach((ref) => context.objects.add(ref));
342
+ objects.removed.forEach((ref) => {
343
+ for (const obj of context.objects) {
344
+ if (
345
+ obj.uri === ref.uri ||
346
+ (EID.tryParse(obj.uri) &&
347
+ EID.tryParse(ref.uri) &&
348
+ EID.getEntityId(EID.tryParse(obj.uri)!) === EID.getEntityId(EID.tryParse(ref.uri)!))
349
+ ) {
350
+ context.objects.delete(obj);
351
+ }
352
+ }
353
+ });
354
+
355
+ return context;
356
+ }),
357
+ );
358
+ }
359
+
360
+ /**
361
+ * Merge resolved items into the current set, adding only items not already present (by DXN).
362
+ */
363
+ private _mergeInto<T extends Obj.Unknown>(current: T[], resolved: T[]): T[] {
364
+ const seen = new Set(current.map((obj) => Obj.getURI(obj)));
365
+ const merged = [...current];
366
+ for (const obj of resolved) {
367
+ const uri = Obj.getURI(obj);
368
+ if (!seen.has(uri)) {
369
+ seen.add(uri);
370
+ merged.push(obj);
371
+ }
372
+ }
373
+ return merged;
374
+ }
375
+
376
+ /**
377
+ * Resolve references to objects, loading them first if needed and falling back to existing objects.
378
+ * DXN refs (e.g. `dxn:org.dxos.blueprint.database`) resolve via the wired-up ECHO ref resolver
379
+ * which already spans both the space DB and the hypergraph registry.
380
+ */
381
+ private async _resolve<T extends Obj.Unknown>(refs: Iterable<Ref.Ref<T>>, current: T[]): Promise<T[]> {
382
+ const refArray = [...refs];
383
+
384
+ // Load all refs that need loading.
385
+ await Promise.all(refArray.map((ref) => ref.tryLoad()));
386
+
387
+ return refArray
388
+ .map((ref) => {
389
+ let target: T | undefined;
390
+ // Only resolve target if available (has target or resolver).
391
+ if (ref.isAvailable) {
392
+ target = ref.target;
393
+ }
394
+
395
+ // Fallback to existing object.
396
+ return target ?? current.find((obj) => Obj.getURI(obj) === ref.uri);
397
+ })
398
+ .filter(isNonNullable);
399
+ }
400
+ }
401
+
402
+ export class Service extends Context.Tag('@dxos/assistant/AiContextService')<
403
+ Service,
404
+ {
405
+ binder: Binder;
406
+ }
407
+ >() {
408
+ static bindContext = ({ blueprints, objects }: BindingProps): Effect.Effect<void, never, Service> =>
409
+ Effect.gen(function* () {
410
+ const { binder } = yield* Service;
411
+ yield* Effect.promise(() => binder.bind({ blueprints, objects }));
412
+ });
413
+
414
+ static findObjects = <T extends Type.AnyObj>(type: T): Effect.Effect<Type.InstanceType<T>[], never, Service> => {
415
+ return Effect.gen(function* () {
416
+ const { binder } = yield* Service;
417
+ return binder.getObjects().filter(Obj.instanceOf(type));
418
+ });
419
+ };
420
+ }
@@ -0,0 +1,111 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { describe, expect, it } from '@effect/vitest';
6
+ import * as Effect from 'effect/Effect';
7
+
8
+ import { Database, Feed, Obj, Ref } from '@dxos/echo';
9
+ import { TestDatabaseLayer } from '@dxos/echo-client/testing';
10
+ import { Message } from '@dxos/types';
11
+
12
+ import * as AiSession from './AiSession';
13
+ import * as SessionLink from './SessionLink';
14
+
15
+ // Monotonic timestamps so chronological sorting in SessionLoader is deterministic.
16
+ let clock = 0;
17
+ const makeMessage = (text: string, sender: 'user' | 'assistant' = 'user') =>
18
+ Message.make({ created: new Date(clock++).toISOString(), sender, blocks: [{ _tag: 'text', text }] });
19
+
20
+ describe('AiSession.Session.getHistory', () => {
21
+ const TestLayer = TestDatabaseLayer({ types: [Feed.Feed, Message.Message, SessionLink.SessionLink] });
22
+
23
+ it.effect('returns messages from the session feed', () =>
24
+ Effect.gen(function* () {
25
+ const { db } = yield* Database.Service;
26
+ const feed = db.add(Feed.make());
27
+ const message = makeMessage('hello');
28
+ yield* Feed.append(feed, [message]);
29
+
30
+ const runtime = yield* Effect.runtime<Feed.FeedService>();
31
+ const session = new AiSession.Session({ feed, runtime });
32
+
33
+ const result = yield* Effect.promise(() => session.getHistory());
34
+
35
+ expect(result.map((msg) => msg.id)).toEqual([message.id]);
36
+ }).pipe(Effect.provide(TestLayer)),
37
+ );
38
+
39
+ it.effect('prepends source history through a SessionLink (fork scenario)', () =>
40
+ Effect.gen(function* () {
41
+ const { db } = yield* Database.Service;
42
+ const sourceFeed = db.add(Feed.make());
43
+ const forkFeed = db.add(Feed.make());
44
+
45
+ const msg1 = makeMessage('original question');
46
+ const msg2 = makeMessage('original answer', 'assistant');
47
+ const forkMsg = makeMessage('follow-up from fork');
48
+ yield* Feed.append(sourceFeed, [msg1, msg2]);
49
+ yield* Feed.append(forkFeed, [
50
+ Obj.make(SessionLink.SessionLink, { feedRef: Ref.make(sourceFeed), messageId: msg2.id }),
51
+ forkMsg,
52
+ ]);
53
+
54
+ const runtime = yield* Effect.runtime<Feed.FeedService>();
55
+ const session = new AiSession.Session({ feed: forkFeed, runtime });
56
+
57
+ const result = yield* Effect.promise(() => session.getHistory());
58
+
59
+ // Source messages up to the cutoff are prepended; the fork message follows.
60
+ expect(result.map((msg) => msg.id)).toEqual([msg1.id, msg2.id, forkMsg.id]);
61
+ }).pipe(Effect.provide(TestLayer)),
62
+ );
63
+
64
+ it.effect('respects the fork cutoff — excludes source messages after messageId', () =>
65
+ Effect.gen(function* () {
66
+ const { db } = yield* Database.Service;
67
+ const sourceFeed = db.add(Feed.make());
68
+ const forkFeed = db.add(Feed.make());
69
+
70
+ const msg1 = makeMessage('a');
71
+ const msg2 = makeMessage('b', 'assistant');
72
+ const msg3 = makeMessage('c — beyond fork point');
73
+ yield* Feed.append(sourceFeed, [msg1, msg2, msg3]);
74
+ yield* Feed.append(forkFeed, [
75
+ // Fork at msg2; msg3 must be excluded.
76
+ Obj.make(SessionLink.SessionLink, { feedRef: Ref.make(sourceFeed), messageId: msg2.id }),
77
+ ]);
78
+
79
+ const runtime = yield* Effect.runtime<Feed.FeedService>();
80
+ const session = new AiSession.Session({ feed: forkFeed, runtime });
81
+
82
+ const result = yield* Effect.promise(() => session.getHistory());
83
+
84
+ expect(result.map((msg) => msg.id)).toEqual([msg1.id, msg2.id]);
85
+ }).pipe(Effect.provide(TestLayer)),
86
+ );
87
+
88
+ it.effect('returns only fork-feed messages when SessionLink messageId is not found (fail-closed)', () =>
89
+ Effect.gen(function* () {
90
+ const { db } = yield* Database.Service;
91
+ const sourceFeed = db.add(Feed.make());
92
+ const forkFeed = db.add(Feed.make());
93
+
94
+ const sourceMsg = makeMessage('source');
95
+ const forkMsg = makeMessage('fork only');
96
+ yield* Feed.append(sourceFeed, [sourceMsg]);
97
+ yield* Feed.append(forkFeed, [
98
+ // messageId does not exist in sourceFeed.
99
+ Obj.make(SessionLink.SessionLink, { feedRef: Ref.make(sourceFeed), messageId: Obj.ID.random() }),
100
+ forkMsg,
101
+ ]);
102
+
103
+ const runtime = yield* Effect.runtime<Feed.FeedService>();
104
+ const session = new AiSession.Session({ feed: forkFeed, runtime });
105
+
106
+ const result = yield* Effect.promise(() => session.getHistory());
107
+
108
+ expect(result.map((msg) => msg.id)).toEqual([forkMsg.id]);
109
+ }).pipe(Effect.provide(TestLayer)),
110
+ );
111
+ });