@dxos/assistant 0.8.4-main.ae835ea → 0.8.4-main.bc674ce

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 (116) hide show
  1. package/dist/lib/browser/{chunk-4VIJTKPM.mjs → chunk-7CDDG6LL.mjs} +252 -28
  2. package/dist/lib/browser/chunk-7CDDG6LL.mjs.map +7 -0
  3. package/dist/lib/browser/chunk-J5LGTIGS.mjs +10 -0
  4. package/dist/lib/browser/extraction/index.mjs +119 -166
  5. package/dist/lib/browser/extraction/index.mjs.map +4 -4
  6. package/dist/lib/browser/index.mjs +239 -289
  7. package/dist/lib/browser/index.mjs.map +4 -4
  8. package/dist/lib/browser/meta.json +1 -1
  9. package/dist/lib/browser/testing/index.mjs +32 -0
  10. package/dist/lib/browser/testing/index.mjs.map +7 -0
  11. package/dist/lib/node-esm/{chunk-AUUC7II4.mjs → chunk-CC7ER6Q3.mjs} +251 -28
  12. package/dist/lib/node-esm/chunk-CC7ER6Q3.mjs.map +7 -0
  13. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs +11 -0
  14. package/dist/lib/node-esm/extraction/index.mjs +119 -166
  15. package/dist/lib/node-esm/extraction/index.mjs.map +4 -4
  16. package/dist/lib/node-esm/index.mjs +239 -289
  17. package/dist/lib/node-esm/index.mjs.map +4 -4
  18. package/dist/lib/node-esm/meta.json +1 -1
  19. package/dist/lib/node-esm/testing/index.mjs +33 -0
  20. package/dist/lib/node-esm/testing/index.mjs.map +7 -0
  21. package/dist/types/src/conversation/context.d.ts +88 -62
  22. package/dist/types/src/conversation/context.d.ts.map +1 -1
  23. package/dist/types/src/conversation/context.test.d.ts +2 -0
  24. package/dist/types/src/conversation/context.test.d.ts.map +1 -0
  25. package/dist/types/src/conversation/conversation.d.ts +16 -16
  26. package/dist/types/src/conversation/conversation.d.ts.map +1 -1
  27. package/dist/types/src/conversation/conversation.test.d.ts +2 -0
  28. package/dist/types/src/conversation/conversation.test.d.ts.map +1 -0
  29. package/dist/types/src/errors.d.ts +22 -30
  30. package/dist/types/src/errors.d.ts.map +1 -1
  31. package/dist/types/src/execution-graph.d.ts +5 -2
  32. package/dist/types/src/execution-graph.d.ts.map +1 -1
  33. package/dist/types/src/extraction/extraction-llm-function/extraction-llm-function.d.ts.map +1 -1
  34. package/dist/types/src/extraction/extraction-ner-function/extraction-ner-function.d.ts +254 -252
  35. package/dist/types/src/extraction/extraction-ner-function/extraction-ner-function.d.ts.map +1 -1
  36. package/dist/types/src/extraction/extraction.d.ts +344 -309
  37. package/dist/types/src/extraction/extraction.d.ts.map +1 -1
  38. package/dist/types/src/extraction/quotes.d.ts +2 -2
  39. package/dist/types/src/extraction/quotes.d.ts.map +1 -1
  40. package/dist/types/src/functions/services.d.ts +4 -3
  41. package/dist/types/src/functions/services.d.ts.map +1 -1
  42. package/dist/types/src/functions.test.d.ts +2 -0
  43. package/dist/types/src/functions.test.d.ts.map +1 -0
  44. package/dist/types/src/session/GenericToolkit.d.ts +85 -0
  45. package/dist/types/src/session/GenericToolkit.d.ts.map +1 -0
  46. package/dist/types/src/session/artifact-diff.d.ts +1 -1
  47. package/dist/types/src/session/artifact-diff.d.ts.map +1 -1
  48. package/dist/types/src/session/format.d.ts +4 -178
  49. package/dist/types/src/session/format.d.ts.map +1 -1
  50. package/dist/types/src/session/index.d.ts +1 -0
  51. package/dist/types/src/session/index.d.ts.map +1 -1
  52. package/dist/types/src/session/observer.d.ts +6 -5
  53. package/dist/types/src/session/observer.d.ts.map +1 -1
  54. package/dist/types/src/session/session.d.ts +16 -11
  55. package/dist/types/src/session/session.d.ts.map +1 -1
  56. package/dist/types/src/session/toolkit.d.ts +5 -6
  57. package/dist/types/src/session/toolkit.d.ts.map +1 -1
  58. package/dist/types/src/session/version-pin.d.ts +3 -3
  59. package/dist/types/src/session/version-pin.d.ts.map +1 -1
  60. package/dist/types/src/testing/index.d.ts +2 -0
  61. package/dist/types/src/testing/index.d.ts.map +1 -0
  62. package/dist/types/src/testing/layer.d.ts +21 -0
  63. package/dist/types/src/testing/layer.d.ts.map +1 -0
  64. package/dist/types/src/types/artifact.d.ts +14 -2
  65. package/dist/types/src/types/artifact.d.ts.map +1 -1
  66. package/dist/types/tsconfig.tsbuildinfo +1 -1
  67. package/package.json +38 -27
  68. package/src/conversation/context.test.ts +46 -0
  69. package/src/conversation/context.ts +183 -66
  70. package/src/conversation/conversation.test.ts +65 -0
  71. package/src/conversation/conversation.ts +51 -54
  72. package/src/diff.test.ts +3 -3
  73. package/src/errors.ts +2 -2
  74. package/src/execution-graph.test.ts +45 -43
  75. package/src/execution-graph.ts +23 -14
  76. package/src/extraction/extraction-llm-function/extraction-llm-function.test.ts +2 -2
  77. package/src/extraction/extraction-llm-function/extraction-llm-function.ts +44 -54
  78. package/src/extraction/extraction-ner-function/extraction-ner-function.test.ts +14 -13
  79. package/src/extraction/extraction-ner-function/extraction-ner-function.ts +36 -44
  80. package/src/extraction/extraction-ner-function/named-entity-recognition.test.ts +10 -5
  81. package/src/extraction/extraction.ts +9 -8
  82. package/src/extraction/quotes.ts +4 -5
  83. package/src/functions/services.ts +41 -19
  84. package/src/functions.conversations.json +1 -0
  85. package/src/functions.test.ts +74 -0
  86. package/src/session/GenericToolkit.ts +131 -0
  87. package/src/session/artifact-diff.ts +1 -1
  88. package/src/session/format.test.ts +103 -39
  89. package/src/session/format.ts +17 -16
  90. package/src/session/index.ts +2 -0
  91. package/src/session/observer.ts +5 -3
  92. package/src/session/session.conversations.json +1 -1
  93. package/src/session/session.test.ts +7 -13
  94. package/src/session/session.ts +40 -19
  95. package/src/session/toolkit.ts +13 -22
  96. package/src/session/version-pin.ts +6 -6
  97. package/src/templates/{format.tpl → instructions/format.tpl} +1 -0
  98. package/src/templates/system.ts +2 -2
  99. package/src/testing/index.ts +5 -0
  100. package/src/testing/layer.ts +78 -0
  101. package/src/types/artifact.ts +33 -3
  102. package/dist/lib/browser/chunk-4VIJTKPM.mjs.map +0 -7
  103. package/dist/lib/browser/chunk-UB26N5UF.mjs +0 -89
  104. package/dist/lib/browser/chunk-UB26N5UF.mjs.map +0 -7
  105. package/dist/lib/browser/named-entity-recognition-QPYHML66.mjs +0 -13
  106. package/dist/lib/node-esm/chunk-AUUC7II4.mjs.map +0 -7
  107. package/dist/lib/node-esm/chunk-BCFXTNSK.mjs +0 -91
  108. package/dist/lib/node-esm/chunk-BCFXTNSK.mjs.map +0 -7
  109. package/dist/lib/node-esm/named-entity-recognition-2HDF46YX.mjs +0 -14
  110. package/dist/types/src/summarization/summarization.test.d.ts +0 -2
  111. package/dist/types/src/summarization/summarization.test.d.ts.map +0 -1
  112. package/src/summarization/summarization.test.ts +0 -202
  113. /package/dist/lib/browser/{named-entity-recognition-QPYHML66.mjs.map → chunk-J5LGTIGS.mjs.map} +0 -0
  114. /package/dist/lib/node-esm/{named-entity-recognition-2HDF46YX.mjs.map → chunk-HSLMI22Q.mjs.map} +0 -0
  115. /package/src/extraction/extraction-llm-function/{instructions.tpl → transcription-instructions.tpl} +0 -0
  116. /package/src/templates/{system.tpl → instructions/system.tpl} +0 -0
package/package.json CHANGED
@@ -1,9 +1,13 @@
1
1
  {
2
2
  "name": "@dxos/assistant",
3
- "version": "0.8.4-main.ae835ea",
3
+ "version": "0.8.4-main.bc674ce",
4
4
  "description": "Natural language interface to DXOS",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/dxos/dxos"
10
+ },
7
11
  "license": "MIT",
8
12
  "author": "info@dxos.org",
9
13
  "sideEffects": true,
@@ -20,6 +24,12 @@
20
24
  "types": "./dist/types/src/extraction/index.d.ts",
21
25
  "browser": "./dist/lib/browser/extraction/index.mjs",
22
26
  "node": "./dist/lib/node-esm/extraction/index.mjs"
27
+ },
28
+ "./testing": {
29
+ "source": "./src/testing/index.ts",
30
+ "types": "./dist/types/src/testing/index.d.ts",
31
+ "browser": "./dist/lib/browser/testing/index.mjs",
32
+ "node": "./dist/lib/node-esm/testing/index.mjs"
23
33
  }
24
34
  },
25
35
  "types": "dist/types/src/index.d.ts",
@@ -38,39 +48,40 @@
38
48
  "src"
39
49
  ],
40
50
  "dependencies": {
41
- "@automerge/automerge": "3.1.2",
42
- "@effect/ai": "0.29.1",
43
- "@effect/experimental": "0.56.0",
44
- "@effect/platform": "^0.92.1",
45
- "@preact/signals-core": "^1.12.1",
51
+ "@automerge/automerge": "3.2.3",
52
+ "@effect-atom/atom-react": "^0.4.6",
53
+ "@effect/ai": "0.32.1",
54
+ "@effect/experimental": "0.57.11",
55
+ "@effect/platform": "0.93.6",
46
56
  "@xenova/transformers": "^2.17.2",
47
57
  "lodash.defaultsdeep": "^4.6.1",
48
- "@dxos/async": "0.8.4-main.ae835ea",
49
- "@dxos/blueprints": "0.8.4-main.ae835ea",
50
- "@dxos/context": "0.8.4-main.ae835ea",
51
- "@dxos/echo": "0.8.4-main.ae835ea",
52
- "@dxos/debug": "0.8.4-main.ae835ea",
53
- "@dxos/echo-db": "0.8.4-main.ae835ea",
54
- "@dxos/ai": "0.8.4-main.ae835ea",
55
- "@dxos/errors": "0.8.4-main.ae835ea",
56
- "@dxos/effect": "0.8.4-main.ae835ea",
57
- "@dxos/functions": "0.8.4-main.ae835ea",
58
- "@dxos/graph": "0.8.4-main.ae835ea",
59
- "@dxos/invariant": "0.8.4-main.ae835ea",
60
- "@dxos/live-object": "0.8.4-main.ae835ea",
61
- "@dxos/keys": "0.8.4-main.ae835ea",
62
- "@dxos/node-std": "0.8.4-main.ae835ea",
63
- "@dxos/log": "0.8.4-main.ae835ea",
64
- "@dxos/schema": "0.8.4-main.ae835ea",
65
- "@dxos/echo-protocol": "0.8.4-main.ae835ea",
66
- "@dxos/util": "0.8.4-main.ae835ea"
58
+ "@dxos/ai": "0.8.4-main.bc674ce",
59
+ "@dxos/blueprints": "0.8.4-main.bc674ce",
60
+ "@dxos/context": "0.8.4-main.bc674ce",
61
+ "@dxos/async": "0.8.4-main.bc674ce",
62
+ "@dxos/echo": "0.8.4-main.bc674ce",
63
+ "@dxos/debug": "0.8.4-main.bc674ce",
64
+ "@dxos/echo-db": "0.8.4-main.bc674ce",
65
+ "@dxos/echo-protocol": "0.8.4-main.bc674ce",
66
+ "@dxos/effect": "0.8.4-main.bc674ce",
67
+ "@dxos/functions": "0.8.4-main.bc674ce",
68
+ "@dxos/errors": "0.8.4-main.bc674ce",
69
+ "@dxos/functions-runtime": "0.8.4-main.bc674ce",
70
+ "@dxos/graph": "0.8.4-main.bc674ce",
71
+ "@dxos/invariant": "0.8.4-main.bc674ce",
72
+ "@dxos/keys": "0.8.4-main.bc674ce",
73
+ "@dxos/log": "0.8.4-main.bc674ce",
74
+ "@dxos/schema": "0.8.4-main.bc674ce",
75
+ "@dxos/node-std": "0.8.4-main.bc674ce",
76
+ "@dxos/types": "0.8.4-main.bc674ce",
77
+ "@dxos/util": "0.8.4-main.bc674ce"
67
78
  },
68
79
  "devDependencies": {
69
80
  "@types/lodash.defaultsdeep": "^4.6.6",
70
- "effect": "3.18.3"
81
+ "effect": "3.19.11"
71
82
  },
72
83
  "peerDependencies": {
73
- "effect": "^3.13.3"
84
+ "effect": "3.19.11"
74
85
  },
75
86
  "publishConfig": {
76
87
  "access": "public"
@@ -0,0 +1,46 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import * as Schema from 'effect/Schema';
6
+ import { describe, it } from 'vitest';
7
+
8
+ import { Obj, Ref, Type } from '@dxos/echo';
9
+ import { type Queue } from '@dxos/echo-db';
10
+
11
+ import { AiContextBinder } from './context';
12
+
13
+ // TODO(burdon): Create mock queue?
14
+ const createMockQueue = (): Queue =>
15
+ ({
16
+ query: () =>
17
+ ({
18
+ subscribe: () => ({ unsubscribe: () => {} }),
19
+ results: [],
20
+ }) as any,
21
+ append: async () => {
22
+ return [] as any;
23
+ },
24
+ }) as any;
25
+
26
+ describe('AiContextBinder', () => {
27
+ it('should handle bind with Ref', async () => {
28
+ const queue = createMockQueue();
29
+ const binder = new AiContextBinder({ queue });
30
+
31
+ const TestSchema = Schema.Struct({}).pipe(
32
+ Type.object({
33
+ typename: 'dxos.org/type/Example',
34
+ version: '0.1.0',
35
+ }),
36
+ );
37
+
38
+ const obj = Obj.make(TestSchema, {});
39
+ const ref = Ref.make(obj);
40
+
41
+ await binder.bind({
42
+ blueprints: [],
43
+ objects: [ref],
44
+ });
45
+ });
46
+ });
@@ -2,18 +2,18 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { type ReadonlySignal, computed } from '@preact/signals-core';
6
- import * as Array from 'effect/Array';
5
+ import { Atom, Registry } from '@effect-atom/atom-react';
6
+ import * as EArray from 'effect/Array';
7
7
  import * as Context from 'effect/Context';
8
8
  import * as Function from 'effect/Function';
9
9
  import * as Schema from 'effect/Schema';
10
10
 
11
11
  import { Blueprint } from '@dxos/blueprints';
12
12
  import { Resource } from '@dxos/context';
13
- import { DXN, Filter, Obj, Query, type Ref, type Relation, Type } from '@dxos/echo';
13
+ import { DXN, Obj, Query, type Ref, Type } from '@dxos/echo';
14
14
  import { type Queue } from '@dxos/echo-db';
15
- import { invariant } from '@dxos/invariant';
16
- import { ComplexSet } from '@dxos/util';
15
+ import { log } from '@dxos/log';
16
+ import { ComplexSet, isTruthy } from '@dxos/util';
17
17
 
18
18
  /**
19
19
  * Thread message that binds or unbinds contextual objects to a conversation.
@@ -25,13 +25,12 @@ export const ContextBinding = Schema.Struct({
25
25
  removed: Schema.Array(Type.Ref(Blueprint.Blueprint)),
26
26
  }),
27
27
 
28
- // TODO(burdon): Type.Expando => Type.Obj (or Obj.Any?)
29
28
  objects: Schema.Struct({
30
- added: Schema.Array(Type.Ref(Type.Expando)),
31
- removed: Schema.Array(Type.Ref(Type.Expando)),
29
+ added: Schema.Array(Type.Ref(Type.Obj)),
30
+ removed: Schema.Array(Type.Ref(Type.Obj)),
32
31
  }),
33
32
  }).pipe(
34
- Type.Obj({
33
+ Type.object({
35
34
  typename: 'dxos.org/type/ContextBinding',
36
35
  version: '0.1.0',
37
36
  }),
@@ -41,141 +40,259 @@ export interface ContextBinding extends Schema.Schema.Type<typeof ContextBinding
41
40
 
42
41
  export type BindingProps = Partial<{
43
42
  blueprints: Ref.Ref<Blueprint.Blueprint>[];
44
- objects: Ref.Ref<Type.Expando>[];
43
+ objects: Ref.Ref<Obj.Unknown>[];
45
44
  }>;
46
45
 
47
46
  export class Bindings {
48
47
  readonly blueprints = new ComplexSet<Ref.Ref<Blueprint.Blueprint>>((ref) => ref.dxn.toString());
48
+
49
49
  // TODO(burdon): Some DXNs have the Space prefix so only compare the object ID.
50
- readonly objects = new ComplexSet<Ref.Ref<Type.Expando>>((ref) => ref.dxn.asEchoDXN()?.echoId);
50
+ readonly objects = new ComplexSet<Ref.Ref<Obj.Unknown>>((ref) => ref.dxn.asEchoDXN()?.echoId);
51
51
 
52
52
  toJSON() {
53
53
  return {
54
- blueprints: Array.fromIterable(this.blueprints).map((ref) => ref.dxn.toString()),
55
- objects: Array.fromIterable(this.objects).map((ref) => ref.dxn.toString()),
54
+ blueprints: EArray.fromIterable(this.blueprints).map((ref) => ref.dxn.toString()),
55
+ objects: EArray.fromIterable(this.objects).map((ref) => ref.dxn.toString()),
56
56
  };
57
57
  }
58
58
  }
59
59
 
60
+ export type AiContextBinderOptions = {
61
+ queue: Queue;
62
+ registry?: Registry.Registry;
63
+ };
64
+
60
65
  /**
61
66
  * Manages bindings of blueprints and objects to a conversation.
62
67
  */
63
68
  // TODO(burdon): Context should manage ephemeral state of bindings until prompt is issued?
64
69
  export class AiContextBinder extends Resource {
65
- /**
66
- * Reactive query of all bindings.
67
- */
68
- // TODO(burdon): Cache value?
69
- private _bindings?: ReadonlySignal<Bindings>;
70
- private _blueprints?: ReadonlySignal<Ref.Ref<Blueprint.Blueprint>[]>;
71
- private _objects?: ReadonlySignal<Ref.Ref<Type.Expando>[]>;
70
+ private readonly _blueprints = Atom.make<Blueprint.Blueprint[]>([]).pipe(Atom.keepAlive);
71
+ private readonly _objects = Atom.make<Obj.Unknown[]>([]).pipe(Atom.keepAlive);
72
+ private readonly _registry: Registry.Registry;
73
+ private readonly _queue: Queue;
72
74
 
73
- constructor(private readonly _queue: Queue) {
75
+ constructor(options: AiContextBinderOptions) {
74
76
  super();
77
+ this._queue = options.queue;
78
+ this._registry = options.registry ?? Registry.make();
75
79
  }
76
80
 
77
- get bindings() {
78
- invariant(this._bindings, 'AiContextBinder not open');
79
- return this._bindings;
80
- }
81
-
82
- get blueprints() {
83
- invariant(this._blueprints, 'AiContextBinder not open');
81
+ /**
82
+ * Returns the blueprints atom for subscription.
83
+ */
84
+ get blueprints(): Atom.Atom<Blueprint.Blueprint[]> {
84
85
  return this._blueprints;
85
86
  }
86
87
 
87
- get objects() {
88
- invariant(this._objects, 'AiContextBinder not open');
88
+ /**
89
+ * Returns the objects atom for subscription.
90
+ */
91
+ get objects(): Atom.Atom<Obj.Unknown[]> {
89
92
  return this._objects;
90
93
  }
91
94
 
92
- // TODO(wittjosiah): Use parent context?
93
- protected override async _open(): Promise<void> {
94
- const query = this._queue.query(Query.select(Filter.everything()));
95
- this._ctx.onDispose(query.subscribe(() => {}));
96
- this._bindings = computed(() => this._reduce(query.objects));
97
- this._blueprints = computed(() => [...this.bindings.value.blueprints]);
98
- this._objects = computed(() => [...this.bindings.value.objects]);
95
+ /**
96
+ * Gets the current blueprints value.
97
+ */
98
+ getBlueprints(): Blueprint.Blueprint[] {
99
+ return this._registry.get(this._blueprints);
99
100
  }
100
101
 
101
- protected override async _close(): Promise<void> {
102
- this._bindings = undefined;
103
- this._blueprints = undefined;
104
- this._objects = undefined;
102
+ /**
103
+ * Gets the current objects value.
104
+ */
105
+ getObjects(): Obj.Unknown[] {
106
+ return this._registry.get(this._objects);
107
+ }
108
+
109
+ /**
110
+ * Subscribe to changes in blueprints.
111
+ */
112
+ subscribeBlueprints(cb: (blueprints: Blueprint.Blueprint[]) => void): () => void {
113
+ return this._registry.subscribe(this._blueprints, () => cb(this._registry.get(this._blueprints)));
105
114
  }
106
115
 
107
116
  /**
108
- * Asynchronous query of all bindings.
117
+ * Subscribe to changes in objects.
109
118
  */
110
- async query(): Promise<Bindings> {
111
- const { objects } = await this._queue.query(Query.select(Filter.everything())).run();
112
- return this._reduce(objects);
119
+ subscribeObjects(cb: (objects: Obj.Unknown[]) => void): () => void {
120
+ return this._registry.subscribe(this._objects, () => cb(this._registry.get(this._objects)));
121
+ }
122
+
123
+ protected override async _open(): Promise<void> {
124
+ const query = this._queue.query(Query.type(ContextBinding));
125
+
126
+ // Process initial state before returning.
127
+ const initialResults = await query.run();
128
+ await this._updateBindings(initialResults);
129
+
130
+ // Subscribe to future changes.
131
+ this._ctx.onDispose(
132
+ query.subscribe(async () => {
133
+ await this._updateBindings(query.results);
134
+ }),
135
+ );
136
+ }
137
+
138
+ private async _updateBindings(items: ContextBinding[]): Promise<void> {
139
+ // Skip update if no items - preserve existing state set by bind().
140
+ if (items.length === 0) {
141
+ return;
142
+ }
143
+
144
+ const bindings = this._reduce(items);
145
+
146
+ // Resolve references (loading them first if needed).
147
+ const currentBlueprints = this._registry.get(this._blueprints);
148
+ const currentObjects = this._registry.get(this._objects);
149
+ const newBlueprints = await this._resolve(bindings.blueprints, currentBlueprints);
150
+ const newObjects = await this._resolve(bindings.objects, currentObjects);
151
+
152
+ // Atomic updates - subscribers notified automatically.
153
+ this._registry.set(this._blueprints, newBlueprints);
154
+ this._registry.set(this._objects, newObjects);
155
+
156
+ log('updated bindings', {
157
+ blueprints: newBlueprints.length,
158
+ objects: newObjects.length,
159
+ });
160
+ }
161
+
162
+ protected override async _close(): Promise<void> {
163
+ // Reset atoms to empty state.
164
+ this._registry.set(this._blueprints, []);
165
+ this._registry.set(this._objects, []);
113
166
  }
114
167
 
115
- // TODO(burdon): Pass in Blueprint obj (from registry?) and create reference.
116
- async bind(props: BindingProps): Promise<void> {
117
- const blueprints =
118
- props.blueprints?.filter((ref) => !this.blueprints.peek().find((b) => b.dxn.toString() === ref.dxn.toString())) ??
119
- [];
120
- const objects =
121
- props.objects?.filter((ref) => !this.objects.peek().find((o) => o.dxn.toString() === ref.dxn.toString())) ?? [];
168
+ async bind({ blueprints, objects }: BindingProps): Promise<void> {
169
+ const currentBlueprints = this._registry.get(this._blueprints);
170
+ const currentObjects = this._registry.get(this._objects);
122
171
 
123
- if (!blueprints.length && !objects.length) {
172
+ const { added: addedBlueprints, next: nextBlueprints } = this._processBindings(blueprints, currentBlueprints);
173
+ const { added: addedObjects, next: nextObjects } = this._processBindings(objects, currentObjects);
174
+ if (!addedBlueprints.length && !addedObjects.length) {
124
175
  return;
125
176
  }
126
177
 
178
+ // Atomic updates - subscribers notified automatically.
179
+ this._registry.set(this._blueprints, nextBlueprints);
180
+ this._registry.set(this._objects, nextObjects);
181
+
182
+ log('bind', { blueprints: addedBlueprints.length, objects: addedObjects.length });
127
183
  await this._queue.append([
128
184
  Obj.make(ContextBinding, {
129
185
  blueprints: {
130
- added: blueprints,
186
+ added: addedBlueprints,
131
187
  removed: [],
132
188
  },
133
189
  objects: {
134
- added: objects,
190
+ added: addedObjects,
135
191
  removed: [],
136
192
  },
137
193
  }),
138
194
  ]);
139
195
  }
140
196
 
141
- async unbind(props: BindingProps): Promise<void> {
142
- if (!props.blueprints?.length && !props.objects?.length) {
197
+ async unbind({ blueprints, objects }: BindingProps): Promise<void> {
198
+ if (!blueprints?.length && !objects?.length) {
143
199
  return;
144
200
  }
145
201
 
202
+ log('unbind', { blueprints: blueprints?.length, objects: objects?.length });
146
203
  await this._queue.append([
147
204
  Obj.make(ContextBinding, {
148
205
  blueprints: {
149
206
  added: [],
150
- removed: props.blueprints ?? [],
207
+ removed: blueprints ?? [],
151
208
  },
152
209
  objects: {
153
210
  added: [],
154
- removed: props.objects ?? [],
211
+ removed: objects ?? [],
155
212
  },
156
213
  }),
157
214
  ]);
158
215
  }
159
216
 
160
- private _reduce(items: (Obj.Any | Relation.Any)[]): Bindings {
217
+ /**
218
+ * Process bindings to filter duplicates and determine next state.
219
+ */
220
+ private _processBindings<T extends Obj.Unknown>(
221
+ refs: Ref.Ref<T>[] | undefined,
222
+ current: T[],
223
+ ): { added: Ref.Ref<T>[]; next: T[] } {
224
+ const next = [...current];
225
+ const added: Ref.Ref<T>[] = [];
226
+ if (!refs?.length) {
227
+ return { added, next };
228
+ }
229
+
230
+ const seen = new Set(current.map((obj) => Obj.getDXN(obj).toString()));
231
+ for (const ref of refs) {
232
+ const dxn = ref.dxn.toString();
233
+ if (!seen.has(dxn)) {
234
+ seen.add(dxn);
235
+ added.push(ref);
236
+
237
+ // Only resolve target if available (has target or resolver).
238
+ if (ref.isAvailable) {
239
+ const target = ref.target;
240
+ if (target) {
241
+ next.push(target);
242
+ }
243
+ }
244
+ }
245
+ }
246
+
247
+ return { added, next };
248
+ }
249
+
250
+ /**
251
+ * Reduce results into sets of blueprints and objects.
252
+ */
253
+ private _reduce(items: ContextBinding[]): Bindings {
161
254
  return Function.pipe(
162
255
  items,
163
- Array.filter(Obj.instanceOf(ContextBinding)),
164
- Array.reduce(new Bindings(), (context, item) => {
165
- item.blueprints.removed.forEach((ref) => context.blueprints.delete(ref));
166
- item.blueprints.added.forEach((ref) => context.blueprints.add(ref));
167
- item.objects.removed.forEach((ref) => {
256
+ EArray.reduce(new Bindings(), (context, { blueprints, objects }) => {
257
+ blueprints.added.forEach((ref) => context.blueprints.add(ref));
258
+ blueprints.removed.forEach((ref) => context.blueprints.delete(ref));
259
+
260
+ objects.added.forEach((ref) => context.objects.add(ref));
261
+ objects.removed.forEach((ref) => {
168
262
  for (const obj of context.objects) {
169
263
  if (DXN.equalsEchoId(obj.dxn, ref.dxn)) {
170
264
  context.objects.delete(obj);
171
265
  }
172
266
  }
173
267
  });
174
- item.objects.added.forEach((ref) => context.objects.add(ref));
268
+
175
269
  return context;
176
270
  }),
177
271
  );
178
272
  }
273
+
274
+ /**
275
+ * Resolve references to objects, loading them first if needed and falling back to existing objects.
276
+ */
277
+ private async _resolve<T>(refs: Iterable<Ref.Ref<T>>, current: T[]): Promise<T[]> {
278
+ const refArray = [...refs];
279
+
280
+ // Load all refs that need loading.
281
+ await Promise.all(refArray.map((ref) => ref.load()));
282
+
283
+ return refArray
284
+ .map((ref) => {
285
+ let target: T | undefined;
286
+ // Only resolve target if available (has target or resolver).
287
+ if (ref.isAvailable) {
288
+ target = ref.target;
289
+ }
290
+
291
+ // Fallback to existing object.
292
+ return target ?? current.find((obj) => Obj.getDXN(obj as any).toString() === ref.dxn.toString());
293
+ })
294
+ .filter(isTruthy);
295
+ }
179
296
  }
180
297
 
181
298
  export class AiContextService extends Context.Tag('@dxos/assistant/AiContextService')<
@@ -0,0 +1,65 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { it } from '@effect/vitest';
6
+ import * as Effect from 'effect/Effect';
7
+ import { afterEach, beforeEach, describe, expect } from 'vitest';
8
+
9
+ import { Blueprint } from '@dxos/blueprints';
10
+ import { Obj, Ref } from '@dxos/echo';
11
+ import { EchoTestBuilder } from '@dxos/echo-db/testing';
12
+
13
+ import { ContextBinding } from './context';
14
+ import { AiConversation } from './conversation';
15
+
16
+ describe('AiConversation', () => {
17
+ let builder: EchoTestBuilder;
18
+
19
+ beforeEach(async () => {
20
+ builder = await new EchoTestBuilder().open();
21
+ });
22
+
23
+ afterEach(async () => {
24
+ await builder.close();
25
+ });
26
+
27
+ it.effect('loads blueprints on open', () =>
28
+ Effect.gen(function* () {
29
+ const peer = yield* Effect.promise(() => builder.createPeer({ types: [Blueprint.Blueprint, ContextBinding] }));
30
+ const db = yield* Effect.promise(() => peer.createDatabase());
31
+ const queues = peer.client.constructQueueFactory(db.spaceId);
32
+ const queue = queues.create<ContextBinding>();
33
+
34
+ // Create blueprint.
35
+ const blueprint = db.add(
36
+ Blueprint.make({
37
+ key: 'example.com/blueprint/Test',
38
+ name: 'Test Blueprint',
39
+ }),
40
+ );
41
+
42
+ // Add blueprint to queue via binding.
43
+ yield* Effect.promise(() =>
44
+ queue.append([
45
+ Obj.make(ContextBinding, {
46
+ blueprints: {
47
+ added: [Ref.make(blueprint)],
48
+ removed: [],
49
+ },
50
+ objects: {
51
+ added: [],
52
+ removed: [],
53
+ },
54
+ }),
55
+ ]),
56
+ );
57
+
58
+ const conversation = new AiConversation({ queue });
59
+ yield* Effect.promise(() => conversation.open());
60
+
61
+ expect(conversation.context.getBlueprints()).toHaveLength(1);
62
+ expect(conversation.context.getObjects()).toHaveLength(0);
63
+ }),
64
+ );
65
+ });