@dxos/assistant 0.8.4-main.ead640a → 0.8.4-main.ef1bc66f44

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 (120) hide show
  1. package/dist/lib/neutral/chunk-J5LGTIGS.mjs +10 -0
  2. package/dist/lib/neutral/chunk-ZPC54VRM.mjs +596 -0
  3. package/dist/lib/neutral/chunk-ZPC54VRM.mjs.map +7 -0
  4. package/dist/lib/neutral/extraction/index.mjs +225 -0
  5. package/dist/lib/neutral/extraction/index.mjs.map +7 -0
  6. package/dist/lib/{browser → neutral}/index.mjs +346 -350
  7. package/dist/lib/neutral/index.mjs.map +7 -0
  8. package/dist/lib/neutral/meta.json +1 -0
  9. package/dist/lib/neutral/testing/index.mjs +38 -0
  10. package/dist/lib/neutral/testing/index.mjs.map +7 -0
  11. package/dist/types/src/conversation/context.d.ts +97 -62
  12. package/dist/types/src/conversation/context.d.ts.map +1 -1
  13. package/dist/types/src/conversation/context.test.d.ts +2 -0
  14. package/dist/types/src/conversation/context.test.d.ts.map +1 -0
  15. package/dist/types/src/conversation/conversation.d.ts +41 -16
  16. package/dist/types/src/conversation/conversation.d.ts.map +1 -1
  17. package/dist/types/src/conversation/conversation.test.d.ts +2 -0
  18. package/dist/types/src/conversation/conversation.test.d.ts.map +1 -0
  19. package/dist/types/src/diff.d.ts +3 -1
  20. package/dist/types/src/diff.d.ts.map +1 -1
  21. package/dist/types/src/errors.d.ts +22 -30
  22. package/dist/types/src/errors.d.ts.map +1 -1
  23. package/dist/types/src/execution-graph.d.ts +5 -2
  24. package/dist/types/src/execution-graph.d.ts.map +1 -1
  25. package/dist/types/src/extraction/extraction-llm-function/extraction-llm-function.d.ts.map +1 -1
  26. package/dist/types/src/extraction/extraction-ner-function/extraction-ner-function.d.ts +254 -248
  27. package/dist/types/src/extraction/extraction-ner-function/extraction-ner-function.d.ts.map +1 -1
  28. package/dist/types/src/extraction/extraction.d.ts +344 -303
  29. package/dist/types/src/extraction/extraction.d.ts.map +1 -1
  30. package/dist/types/src/extraction/quotes.d.ts +2 -2
  31. package/dist/types/src/extraction/quotes.d.ts.map +1 -1
  32. package/dist/types/src/functions/services.d.ts +6 -6
  33. package/dist/types/src/functions/services.d.ts.map +1 -1
  34. package/dist/types/src/functions.test.d.ts +2 -0
  35. package/dist/types/src/functions.test.d.ts.map +1 -0
  36. package/dist/types/src/session/GenericToolkit.d.ts +107 -0
  37. package/dist/types/src/session/GenericToolkit.d.ts.map +1 -0
  38. package/dist/types/src/session/artifact-diff.d.ts +1 -1
  39. package/dist/types/src/session/artifact-diff.d.ts.map +1 -1
  40. package/dist/types/src/session/format.d.ts +4 -176
  41. package/dist/types/src/session/format.d.ts.map +1 -1
  42. package/dist/types/src/session/index.d.ts +2 -0
  43. package/dist/types/src/session/index.d.ts.map +1 -1
  44. package/dist/types/src/session/observer.d.ts +6 -5
  45. package/dist/types/src/session/observer.d.ts.map +1 -1
  46. package/dist/types/src/session/session.d.ts +16 -11
  47. package/dist/types/src/session/session.d.ts.map +1 -1
  48. package/dist/types/src/session/toolkit.d.ts +5 -6
  49. package/dist/types/src/session/toolkit.d.ts.map +1 -1
  50. package/dist/types/src/session/version-pin.d.ts +3 -3
  51. package/dist/types/src/session/version-pin.d.ts.map +1 -1
  52. package/dist/types/src/testing/index.d.ts +2 -0
  53. package/dist/types/src/testing/index.d.ts.map +1 -0
  54. package/dist/types/src/testing/layer.d.ts +22 -0
  55. package/dist/types/src/testing/layer.d.ts.map +1 -0
  56. package/dist/types/src/types/artifact.d.ts +14 -2
  57. package/dist/types/src/types/artifact.d.ts.map +1 -1
  58. package/dist/types/tsconfig.tsbuildinfo +1 -1
  59. package/package.json +40 -31
  60. package/src/conversation/context.test.ts +46 -0
  61. package/src/conversation/context.ts +211 -67
  62. package/src/conversation/conversation.test.ts +65 -0
  63. package/src/conversation/conversation.ts +128 -53
  64. package/src/diff.test.ts +37 -6
  65. package/src/diff.ts +18 -4
  66. package/src/errors.ts +2 -2
  67. package/src/execution-graph.test.ts +45 -43
  68. package/src/execution-graph.ts +23 -14
  69. package/src/extraction/extraction-llm-function/extraction-llm-function.test.ts +2 -2
  70. package/src/extraction/extraction-llm-function/extraction-llm-function.ts +44 -54
  71. package/src/extraction/extraction-ner-function/extraction-ner-function.test.ts +15 -14
  72. package/src/extraction/extraction-ner-function/extraction-ner-function.ts +36 -44
  73. package/src/extraction/extraction-ner-function/named-entity-recognition.test.ts +10 -5
  74. package/src/extraction/extraction.ts +9 -8
  75. package/src/extraction/quotes.ts +4 -5
  76. package/src/functions/services.ts +66 -37
  77. package/src/functions.conversations.json +1 -0
  78. package/src/functions.test.ts +74 -0
  79. package/src/session/GenericToolkit.ts +160 -0
  80. package/src/session/artifact-diff.ts +1 -1
  81. package/src/session/format.test.ts +103 -39
  82. package/src/session/format.ts +17 -16
  83. package/src/session/index.ts +3 -0
  84. package/src/session/observer.ts +5 -3
  85. package/src/session/session.conversations.json +1 -1
  86. package/src/session/session.test.ts +28 -15
  87. package/src/session/session.ts +44 -21
  88. package/src/session/toolkit.ts +13 -22
  89. package/src/session/version-pin.ts +6 -6
  90. package/src/templates/{format.tpl → instructions/format.tpl} +1 -4
  91. package/src/templates/system.ts +2 -2
  92. package/src/testing/index.ts +5 -0
  93. package/src/testing/layer.ts +84 -0
  94. package/src/types/artifact.ts +43 -6
  95. package/dist/lib/browser/chunk-CWGVG235.mjs +0 -380
  96. package/dist/lib/browser/chunk-CWGVG235.mjs.map +0 -7
  97. package/dist/lib/browser/chunk-UB26N5UF.mjs +0 -89
  98. package/dist/lib/browser/chunk-UB26N5UF.mjs.map +0 -7
  99. package/dist/lib/browser/extraction/index.mjs +0 -272
  100. package/dist/lib/browser/extraction/index.mjs.map +0 -7
  101. package/dist/lib/browser/index.mjs.map +0 -7
  102. package/dist/lib/browser/meta.json +0 -1
  103. package/dist/lib/browser/named-entity-recognition-QPYHML66.mjs +0 -13
  104. package/dist/lib/node-esm/chunk-BCFXTNSK.mjs +0 -91
  105. package/dist/lib/node-esm/chunk-BCFXTNSK.mjs.map +0 -7
  106. package/dist/lib/node-esm/chunk-OV5PMQPB.mjs +0 -382
  107. package/dist/lib/node-esm/chunk-OV5PMQPB.mjs.map +0 -7
  108. package/dist/lib/node-esm/extraction/index.mjs +0 -273
  109. package/dist/lib/node-esm/extraction/index.mjs.map +0 -7
  110. package/dist/lib/node-esm/index.mjs +0 -934
  111. package/dist/lib/node-esm/index.mjs.map +0 -7
  112. package/dist/lib/node-esm/meta.json +0 -1
  113. package/dist/lib/node-esm/named-entity-recognition-2HDF46YX.mjs +0 -14
  114. package/dist/lib/node-esm/named-entity-recognition-2HDF46YX.mjs.map +0 -7
  115. package/dist/types/src/summarization/summarization.test.d.ts +0 -2
  116. package/dist/types/src/summarization/summarization.test.d.ts.map +0 -1
  117. package/src/summarization/summarization.test.ts +0 -202
  118. /package/dist/lib/{browser/named-entity-recognition-QPYHML66.mjs.map → neutral/chunk-J5LGTIGS.mjs.map} +0 -0
  119. /package/src/extraction/extraction-llm-function/{instructions.tpl → transcription-instructions.tpl} +0 -0
  120. /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.ead640a",
3
+ "version": "0.8.4-main.ef1bc66f44",
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,
@@ -12,14 +16,17 @@
12
16
  ".": {
13
17
  "source": "./src/index.ts",
14
18
  "types": "./dist/types/src/index.d.ts",
15
- "browser": "./dist/lib/browser/index.mjs",
16
- "node": "./dist/lib/node-esm/index.mjs"
19
+ "default": "./dist/lib/neutral/index.mjs"
17
20
  },
18
21
  "./extraction": {
19
22
  "source": "./src/extraction/index.ts",
20
23
  "types": "./dist/types/src/extraction/index.d.ts",
21
- "browser": "./dist/lib/browser/extraction/index.mjs",
22
- "node": "./dist/lib/node-esm/extraction/index.mjs"
24
+ "default": "./dist/lib/neutral/extraction/index.mjs"
25
+ },
26
+ "./testing": {
27
+ "source": "./src/testing/index.ts",
28
+ "types": "./dist/types/src/testing/index.d.ts",
29
+ "default": "./dist/lib/neutral/testing/index.mjs"
23
30
  }
24
31
  },
25
32
  "types": "dist/types/src/index.d.ts",
@@ -38,39 +45,41 @@
38
45
  "src"
39
46
  ],
40
47
  "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",
48
+ "@automerge/automerge": "3.2.3",
49
+ "@effect-atom/atom": "^0.5.1",
50
+ "@effect-atom/atom-react": "^0.5.0",
51
+ "@effect/ai": "0.33.2",
52
+ "@effect/experimental": "0.58.0",
53
+ "@effect/platform": "0.94.4",
46
54
  "@xenova/transformers": "^2.17.2",
47
55
  "lodash.defaultsdeep": "^4.6.1",
48
- "@dxos/ai": "0.8.4-main.ead640a",
49
- "@dxos/async": "0.8.4-main.ead640a",
50
- "@dxos/blueprints": "0.8.4-main.ead640a",
51
- "@dxos/debug": "0.8.4-main.ead640a",
52
- "@dxos/echo": "0.8.4-main.ead640a",
53
- "@dxos/echo-db": "0.8.4-main.ead640a",
54
- "@dxos/echo-protocol": "0.8.4-main.ead640a",
55
- "@dxos/effect": "0.8.4-main.ead640a",
56
- "@dxos/errors": "0.8.4-main.ead640a",
57
- "@dxos/graph": "0.8.4-main.ead640a",
58
- "@dxos/functions": "0.8.4-main.ead640a",
59
- "@dxos/invariant": "0.8.4-main.ead640a",
60
- "@dxos/keys": "0.8.4-main.ead640a",
61
- "@dxos/live-object": "0.8.4-main.ead640a",
62
- "@dxos/log": "0.8.4-main.ead640a",
63
- "@dxos/node-std": "0.8.4-main.ead640a",
64
- "@dxos/context": "0.8.4-main.ead640a",
65
- "@dxos/schema": "0.8.4-main.ead640a",
66
- "@dxos/util": "0.8.4-main.ead640a"
56
+ "@dxos/async": "0.8.4-main.ef1bc66f44",
57
+ "@dxos/blueprints": "0.8.4-main.ef1bc66f44",
58
+ "@dxos/ai": "0.8.4-main.ef1bc66f44",
59
+ "@dxos/context": "0.8.4-main.ef1bc66f44",
60
+ "@dxos/debug": "0.8.4-main.ef1bc66f44",
61
+ "@dxos/echo": "0.8.4-main.ef1bc66f44",
62
+ "@dxos/echo-db": "0.8.4-main.ef1bc66f44",
63
+ "@dxos/echo-protocol": "0.8.4-main.ef1bc66f44",
64
+ "@dxos/effect": "0.8.4-main.ef1bc66f44",
65
+ "@dxos/errors": "0.8.4-main.ef1bc66f44",
66
+ "@dxos/functions-runtime": "0.8.4-main.ef1bc66f44",
67
+ "@dxos/graph": "0.8.4-main.ef1bc66f44",
68
+ "@dxos/functions": "0.8.4-main.ef1bc66f44",
69
+ "@dxos/invariant": "0.8.4-main.ef1bc66f44",
70
+ "@dxos/keys": "0.8.4-main.ef1bc66f44",
71
+ "@dxos/log": "0.8.4-main.ef1bc66f44",
72
+ "@dxos/node-std": "0.8.4-main.ef1bc66f44",
73
+ "@dxos/schema": "0.8.4-main.ef1bc66f44",
74
+ "@dxos/types": "0.8.4-main.ef1bc66f44",
75
+ "@dxos/util": "0.8.4-main.ef1bc66f44"
67
76
  },
68
77
  "devDependencies": {
69
78
  "@types/lodash.defaultsdeep": "^4.6.6",
70
- "effect": "3.18.3"
79
+ "effect": "3.19.16"
71
80
  },
72
81
  "peerDependencies": {
73
- "effect": "^3.13.3"
82
+ "effect": "3.19.16"
74
83
  },
75
84
  "publishConfig": {
76
85
  "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,20 @@
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
+ import * as Effect from 'effect/Effect';
8
9
  import * as Function from 'effect/Function';
9
10
  import * as Schema from 'effect/Schema';
10
11
 
11
12
  import { Blueprint } from '@dxos/blueprints';
12
13
  import { Resource } from '@dxos/context';
13
- import { DXN, Filter, Obj, Query, type Ref, type Relation, Type } from '@dxos/echo';
14
+ import { DXN, Obj, Query, type Ref, Type } from '@dxos/echo';
14
15
  import { type Queue } from '@dxos/echo-db';
15
- import { invariant } from '@dxos/invariant';
16
- import { ComplexSet } from '@dxos/util';
16
+ import { assertArgument } from '@dxos/invariant';
17
+ import { log } from '@dxos/log';
18
+ import { ComplexSet, isTruthy } from '@dxos/util';
17
19
 
18
20
  /**
19
21
  * Thread message that binds or unbinds contextual objects to a conversation.
@@ -25,13 +27,12 @@ export const ContextBinding = Schema.Struct({
25
27
  removed: Schema.Array(Type.Ref(Blueprint.Blueprint)),
26
28
  }),
27
29
 
28
- // TODO(burdon): Type.Expando => Type.Obj (or Obj.Any?)
29
30
  objects: Schema.Struct({
30
- added: Schema.Array(Type.Ref(Type.Expando)),
31
- removed: Schema.Array(Type.Ref(Type.Expando)),
31
+ added: Schema.Array(Type.Ref(Type.Obj)),
32
+ removed: Schema.Array(Type.Ref(Type.Obj)),
32
33
  }),
33
34
  }).pipe(
34
- Type.Obj({
35
+ Type.object({
35
36
  typename: 'dxos.org/type/ContextBinding',
36
37
  version: '0.1.0',
37
38
  }),
@@ -41,141 +42,260 @@ export interface ContextBinding extends Schema.Schema.Type<typeof ContextBinding
41
42
 
42
43
  export type BindingProps = Partial<{
43
44
  blueprints: Ref.Ref<Blueprint.Blueprint>[];
44
- objects: Ref.Ref<Type.Expando>[];
45
+ objects: Ref.Ref<Obj.Unknown>[];
45
46
  }>;
46
47
 
47
48
  export class Bindings {
48
49
  readonly blueprints = new ComplexSet<Ref.Ref<Blueprint.Blueprint>>((ref) => ref.dxn.toString());
50
+
49
51
  // 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);
52
+ readonly objects = new ComplexSet<Ref.Ref<Obj.Unknown>>((ref) => ref.dxn.asEchoDXN()?.echoId);
51
53
 
52
54
  toJSON() {
53
55
  return {
54
- blueprints: Array.fromIterable(this.blueprints).map((ref) => ref.dxn.toString()),
55
- objects: Array.fromIterable(this.objects).map((ref) => ref.dxn.toString()),
56
+ blueprints: EArray.fromIterable(this.blueprints).map((ref) => ref.dxn.toString()),
57
+ objects: EArray.fromIterable(this.objects).map((ref) => ref.dxn.toString()),
56
58
  };
57
59
  }
58
60
  }
59
61
 
62
+ export type AiContextBinderOptions = {
63
+ queue: Queue;
64
+ registry?: Registry.Registry;
65
+ };
66
+
60
67
  /**
61
68
  * Manages bindings of blueprints and objects to a conversation.
62
69
  */
63
70
  // TODO(burdon): Context should manage ephemeral state of bindings until prompt is issued?
64
71
  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>[]>;
72
+ private readonly _blueprints = Atom.make<Blueprint.Blueprint[]>([]).pipe(Atom.keepAlive);
73
+ private readonly _objects = Atom.make<Obj.Unknown[]>([]).pipe(Atom.keepAlive);
74
+ private readonly _registry: Registry.Registry;
75
+ private readonly _queue: Queue;
72
76
 
73
- constructor(private readonly _queue: Queue) {
77
+ constructor(options: AiContextBinderOptions) {
74
78
  super();
79
+ assertArgument(options.queue, 'options.queue', 'Queue is required');
80
+ this._queue = options.queue;
81
+ this._registry = options.registry ?? Registry.make();
75
82
  }
76
83
 
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');
84
+ /**
85
+ * Returns the blueprints atom for subscription.
86
+ */
87
+ get blueprints(): Atom.Atom<Blueprint.Blueprint[]> {
84
88
  return this._blueprints;
85
89
  }
86
90
 
87
- get objects() {
88
- invariant(this._objects, 'AiContextBinder not open');
91
+ /**
92
+ * Returns the objects atom for subscription.
93
+ */
94
+ get objects(): Atom.Atom<Obj.Unknown[]> {
89
95
  return this._objects;
90
96
  }
91
97
 
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]);
98
+ /**
99
+ * Gets the current blueprints value.
100
+ */
101
+ getBlueprints(): Blueprint.Blueprint[] {
102
+ return this._registry.get(this._blueprints);
99
103
  }
100
104
 
101
- protected override async _close(): Promise<void> {
102
- this._bindings = undefined;
103
- this._blueprints = undefined;
104
- this._objects = undefined;
105
+ /**
106
+ * Gets the current objects value.
107
+ */
108
+ getObjects(): Obj.Unknown[] {
109
+ return this._registry.get(this._objects);
105
110
  }
106
111
 
107
112
  /**
108
- * Asynchronous query of all bindings.
113
+ * Subscribe to changes in blueprints.
109
114
  */
110
- async query(): Promise<Bindings> {
111
- const { objects } = await this._queue.query(Query.select(Filter.everything())).run();
112
- return this._reduce(objects);
115
+ subscribeBlueprints(cb: (blueprints: Blueprint.Blueprint[]) => void): () => void {
116
+ return this._registry.subscribe(this._blueprints, () => cb(this._registry.get(this._blueprints)));
113
117
  }
114
118
 
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())) ?? [];
119
+ /**
120
+ * Subscribe to changes in objects.
121
+ */
122
+ subscribeObjects(cb: (objects: Obj.Unknown[]) => void): () => void {
123
+ return this._registry.subscribe(this._objects, () => cb(this._registry.get(this._objects)));
124
+ }
125
+
126
+ protected override async _open(): Promise<void> {
127
+ const query = this._queue.query(Query.type(ContextBinding));
128
+
129
+ // Process initial state before returning.
130
+ const initialResults = await query.run();
131
+ await this._updateBindings(initialResults);
132
+
133
+ // Subscribe to future changes.
134
+ this._ctx.onDispose(
135
+ query.subscribe(async () => {
136
+ await this._updateBindings(query.results);
137
+ }),
138
+ );
139
+ }
122
140
 
123
- if (!blueprints.length && !objects.length) {
141
+ private async _updateBindings(items: ContextBinding[]): Promise<void> {
142
+ // Skip update if no items - preserve existing state set by bind().
143
+ if (items.length === 0) {
124
144
  return;
125
145
  }
126
146
 
147
+ const bindings = this._reduce(items);
148
+
149
+ // Resolve references (loading them first if needed).
150
+ const currentBlueprints = this._registry.get(this._blueprints);
151
+ const currentObjects = this._registry.get(this._objects);
152
+ const newBlueprints = await this._resolve(bindings.blueprints, currentBlueprints);
153
+ const newObjects = await this._resolve(bindings.objects, currentObjects);
154
+
155
+ // Atomic updates - subscribers notified automatically.
156
+ this._registry.set(this._blueprints, newBlueprints);
157
+ this._registry.set(this._objects, newObjects);
158
+
159
+ log('updated bindings', {
160
+ blueprints: newBlueprints.length,
161
+ objects: newObjects.length,
162
+ });
163
+ }
164
+
165
+ protected override async _close(): Promise<void> {
166
+ // Reset atoms to empty state.
167
+ this._registry.set(this._blueprints, []);
168
+ this._registry.set(this._objects, []);
169
+ }
170
+
171
+ async bind({ blueprints, objects }: BindingProps): Promise<void> {
172
+ const currentBlueprints = this._registry.get(this._blueprints);
173
+ const currentObjects = this._registry.get(this._objects);
174
+
175
+ const { added: addedBlueprints, next: nextBlueprints } = this._processBindings(blueprints, currentBlueprints);
176
+ const { added: addedObjects, next: nextObjects } = this._processBindings(objects, currentObjects);
177
+ if (!addedBlueprints.length && !addedObjects.length) {
178
+ return;
179
+ }
180
+
181
+ // Atomic updates - subscribers notified automatically.
182
+ this._registry.set(this._blueprints, nextBlueprints);
183
+ this._registry.set(this._objects, nextObjects);
184
+
185
+ log('bind', { blueprints: addedBlueprints.length, objects: addedObjects.length });
127
186
  await this._queue.append([
128
187
  Obj.make(ContextBinding, {
129
188
  blueprints: {
130
- added: blueprints,
189
+ added: addedBlueprints,
131
190
  removed: [],
132
191
  },
133
192
  objects: {
134
- added: objects,
193
+ added: addedObjects,
135
194
  removed: [],
136
195
  },
137
196
  }),
138
197
  ]);
139
198
  }
140
199
 
141
- async unbind(props: BindingProps): Promise<void> {
142
- if (!props.blueprints?.length && !props.objects?.length) {
200
+ async unbind({ blueprints, objects }: BindingProps): Promise<void> {
201
+ if (!blueprints?.length && !objects?.length) {
143
202
  return;
144
203
  }
145
204
 
205
+ log('unbind', { blueprints: blueprints?.length, objects: objects?.length });
146
206
  await this._queue.append([
147
207
  Obj.make(ContextBinding, {
148
208
  blueprints: {
149
209
  added: [],
150
- removed: props.blueprints ?? [],
210
+ removed: blueprints ?? [],
151
211
  },
152
212
  objects: {
153
213
  added: [],
154
- removed: props.objects ?? [],
214
+ removed: objects ?? [],
155
215
  },
156
216
  }),
157
217
  ]);
158
218
  }
159
219
 
160
- private _reduce(items: (Obj.Any | Relation.Any)[]): Bindings {
220
+ /**
221
+ * Process bindings to filter duplicates and determine next state.
222
+ */
223
+ private _processBindings<T extends Obj.Unknown>(
224
+ refs: Ref.Ref<T>[] | undefined,
225
+ current: T[],
226
+ ): { added: Ref.Ref<T>[]; next: T[] } {
227
+ const next = [...current];
228
+ const added: Ref.Ref<T>[] = [];
229
+ if (!refs?.length) {
230
+ return { added, next };
231
+ }
232
+
233
+ const seen = new Set(current.map((obj) => Obj.getDXN(obj).toString()));
234
+ for (const ref of refs) {
235
+ const dxn = ref.dxn.toString();
236
+ if (!seen.has(dxn)) {
237
+ seen.add(dxn);
238
+ added.push(ref);
239
+
240
+ // Only resolve target if available (has target or resolver).
241
+ if (ref.isAvailable) {
242
+ const target = ref.target;
243
+ if (target) {
244
+ next.push(target);
245
+ }
246
+ }
247
+ }
248
+ }
249
+
250
+ return { added, next };
251
+ }
252
+
253
+ /**
254
+ * Reduce results into sets of blueprints and objects.
255
+ */
256
+ private _reduce(items: ContextBinding[]): Bindings {
161
257
  return Function.pipe(
162
258
  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) => {
259
+ EArray.reduce(new Bindings(), (context, { blueprints, objects }) => {
260
+ blueprints.added.forEach((ref) => context.blueprints.add(ref));
261
+ blueprints.removed.forEach((ref) => context.blueprints.delete(ref));
262
+
263
+ objects.added.forEach((ref) => context.objects.add(ref));
264
+ objects.removed.forEach((ref) => {
168
265
  for (const obj of context.objects) {
169
266
  if (DXN.equalsEchoId(obj.dxn, ref.dxn)) {
170
267
  context.objects.delete(obj);
171
268
  }
172
269
  }
173
270
  });
174
- item.objects.added.forEach((ref) => context.objects.add(ref));
271
+
175
272
  return context;
176
273
  }),
177
274
  );
178
275
  }
276
+
277
+ /**
278
+ * Resolve references to objects, loading them first if needed and falling back to existing objects.
279
+ */
280
+ private async _resolve<T>(refs: Iterable<Ref.Ref<T>>, current: T[]): Promise<T[]> {
281
+ const refArray = [...refs];
282
+
283
+ // Load all refs that need loading.
284
+ await Promise.all(refArray.map((ref) => ref.load()));
285
+
286
+ return refArray
287
+ .map((ref) => {
288
+ let target: T | undefined;
289
+ // Only resolve target if available (has target or resolver).
290
+ if (ref.isAvailable) {
291
+ target = ref.target;
292
+ }
293
+
294
+ // Fallback to existing object.
295
+ return target ?? current.find((obj) => Obj.getDXN(obj as any).toString() === ref.dxn.toString());
296
+ })
297
+ .filter(isTruthy);
298
+ }
179
299
  }
180
300
 
181
301
  export class AiContextService extends Context.Tag('@dxos/assistant/AiContextService')<
@@ -183,4 +303,28 @@ export class AiContextService extends Context.Tag('@dxos/assistant/AiContextServ
183
303
  {
184
304
  binder: AiContextBinder;
185
305
  }
186
- >() {}
306
+ >() {
307
+ static bindContext = ({ blueprints, objects }: BindingProps): Effect.Effect<void, never, AiContextService> =>
308
+ Effect.gen(function* () {
309
+ const { binder } = yield* AiContextService;
310
+ yield* Effect.promise(() => binder.bind({ blueprints, objects }));
311
+ });
312
+
313
+ static findObjects = <T extends Type.Entity.Any>(
314
+ type: T,
315
+ ): Effect.Effect<Schema.Schema.Type<T>[], never, AiContextService> => {
316
+ return Effect.gen(function* () {
317
+ const { binder } = yield* AiContextService;
318
+ return binder.getObjects().filter(Obj.instanceOf(type));
319
+ });
320
+ };
321
+
322
+ /**
323
+ * Glorified type cast until we figure out function service typing.
324
+ * For context, the AiContextService is provided to functions called from tool calls, but it's not implemented in types yet.
325
+ * @deprecated
326
+ */
327
+ static fixFunctionHandlerType = <A, E, R>(
328
+ eff: Effect.Effect<A, E, R>,
329
+ ): Effect.Effect<A, E, Exclude<R, AiContextService>> => eff as any;
330
+ }
@@ -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
+ });