@dxos/assistant 0.8.4-main.937b3ca → 0.8.4-main.9be5663bfe

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 (124) hide show
  1. package/README.md +1 -0
  2. package/dist/lib/neutral/chunk-UAWIXKF6.mjs +1875 -0
  3. package/dist/lib/neutral/chunk-UAWIXKF6.mjs.map +7 -0
  4. package/dist/lib/{browser → neutral}/extraction/index.mjs +26 -32
  5. package/dist/lib/neutral/extraction/index.mjs.map +7 -0
  6. package/dist/lib/{browser → neutral}/index.mjs +73 -372
  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 +114 -0
  10. package/dist/lib/neutral/testing/index.mjs.map +7 -0
  11. package/dist/types/src/conversation/context.d.ts +46 -20
  12. package/dist/types/src/conversation/context.d.ts.map +1 -1
  13. package/dist/types/src/conversation/conversation.d.ts +37 -10
  14. package/dist/types/src/conversation/conversation.d.ts.map +1 -1
  15. package/dist/types/src/diff.d.ts +3 -1
  16. package/dist/types/src/diff.d.ts.map +1 -1
  17. package/dist/types/src/extraction/extraction-ner-function/index.d.ts +0 -1
  18. package/dist/types/src/extraction/extraction-ner-function/index.d.ts.map +1 -1
  19. package/dist/types/src/extraction/extraction.d.ts +933 -221
  20. package/dist/types/src/extraction/extraction.d.ts.map +1 -1
  21. package/dist/types/src/extraction/index.d.ts +0 -1
  22. package/dist/types/src/extraction/index.d.ts.map +1 -1
  23. package/dist/types/src/functions/services.d.ts +27 -8
  24. package/dist/types/src/functions/services.d.ts.map +1 -1
  25. package/dist/types/src/index.d.ts +2 -0
  26. package/dist/types/src/index.d.ts.map +1 -1
  27. package/dist/types/src/service/AgentService.d.ts +61 -0
  28. package/dist/types/src/service/AgentService.d.ts.map +1 -0
  29. package/dist/types/src/service/AgentService.test.d.ts +2 -0
  30. package/dist/types/src/service/AgentService.test.d.ts.map +1 -0
  31. package/dist/types/src/service/agent-process.d.ts +17 -0
  32. package/dist/types/src/service/agent-process.d.ts.map +1 -0
  33. package/dist/types/src/service/index.d.ts +3 -0
  34. package/dist/types/src/service/index.d.ts.map +1 -0
  35. package/dist/types/src/session/format.d.ts.map +1 -1
  36. package/dist/types/src/session/index.d.ts +1 -1
  37. package/dist/types/src/session/index.d.ts.map +1 -1
  38. package/dist/types/src/session/observer.d.ts.map +1 -1
  39. package/dist/types/src/session/session.d.ts +64 -8
  40. package/dist/types/src/session/session.d.ts.map +1 -1
  41. package/dist/types/src/session/toolkit.d.ts +6 -2
  42. package/dist/types/src/session/toolkit.d.ts.map +1 -1
  43. package/dist/types/src/templates/system.d.ts +0 -1
  44. package/dist/types/src/templates/system.d.ts.map +1 -1
  45. package/dist/types/src/testing/layer.d.ts +23 -9
  46. package/dist/types/src/testing/layer.d.ts.map +1 -1
  47. package/dist/types/src/tracing.d.ts +264 -0
  48. package/dist/types/src/tracing.d.ts.map +1 -0
  49. package/dist/types/src/types/artifact.d.ts +5 -5
  50. package/dist/types/src/types/artifact.d.ts.map +1 -1
  51. package/dist/types/tsconfig.tsbuildinfo +1 -1
  52. package/package.json +33 -33
  53. package/src/conversation/context.test.ts +19 -13
  54. package/src/conversation/context.ts +150 -50
  55. package/src/conversation/conversation.test.ts +25 -32
  56. package/src/conversation/conversation.ts +197 -47
  57. package/src/diff.test.ts +36 -5
  58. package/src/diff.ts +13 -4
  59. package/src/execution-graph.test.ts +2 -2
  60. package/src/execution-graph.ts +3 -3
  61. package/src/extraction/extraction-ner-function/index.ts +0 -1
  62. package/src/extraction/extraction.ts +38 -10
  63. package/src/extraction/index.ts +0 -1
  64. package/src/functions/services.ts +199 -50
  65. package/src/functions.conversations.json +1 -1
  66. package/src/functions.test.ts +28 -20
  67. package/src/index.ts +2 -0
  68. package/src/service/AgentService.conversations.json +1 -0
  69. package/src/service/AgentService.test.ts +169 -0
  70. package/src/service/AgentService.ts +165 -0
  71. package/src/service/agent-process.ts +359 -0
  72. package/src/service/index.ts +6 -0
  73. package/src/session/format.test.ts +23 -15
  74. package/src/session/format.ts +0 -1
  75. package/src/session/index.ts +1 -2
  76. package/src/session/observer.ts +3 -2
  77. package/src/session/session.conversations.json +1 -1
  78. package/src/session/session.test.ts +52 -5
  79. package/src/session/session.ts +232 -117
  80. package/src/session/toolkit.ts +12 -7
  81. package/src/templates/instructions/format.tpl +0 -4
  82. package/src/templates/instructions/system.tpl +39 -2
  83. package/src/templates/system.ts +6 -5
  84. package/src/testing/layer.ts +205 -24
  85. package/src/tracing.ts +113 -0
  86. package/src/types/artifact.ts +17 -10
  87. package/dist/lib/browser/chunk-7CDDG6LL.mjs +0 -578
  88. package/dist/lib/browser/chunk-7CDDG6LL.mjs.map +0 -7
  89. package/dist/lib/browser/extraction/index.mjs.map +0 -7
  90. package/dist/lib/browser/index.mjs.map +0 -7
  91. package/dist/lib/browser/meta.json +0 -1
  92. package/dist/lib/browser/testing/index.mjs +0 -32
  93. package/dist/lib/browser/testing/index.mjs.map +0 -7
  94. package/dist/lib/node-esm/chunk-CC7ER6Q3.mjs +0 -579
  95. package/dist/lib/node-esm/chunk-CC7ER6Q3.mjs.map +0 -7
  96. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs +0 -11
  97. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs.map +0 -7
  98. package/dist/lib/node-esm/extraction/index.mjs +0 -226
  99. package/dist/lib/node-esm/extraction/index.mjs.map +0 -7
  100. package/dist/lib/node-esm/index.mjs +0 -851
  101. package/dist/lib/node-esm/index.mjs.map +0 -7
  102. package/dist/lib/node-esm/meta.json +0 -1
  103. package/dist/lib/node-esm/testing/index.mjs +0 -33
  104. package/dist/lib/node-esm/testing/index.mjs.map +0 -7
  105. package/dist/types/src/extraction/extraction-llm-function/extraction-llm-function.d.ts +0 -4
  106. package/dist/types/src/extraction/extraction-llm-function/extraction-llm-function.d.ts.map +0 -1
  107. package/dist/types/src/extraction/extraction-llm-function/extraction-llm-function.test.d.ts +0 -2
  108. package/dist/types/src/extraction/extraction-llm-function/extraction-llm-function.test.d.ts.map +0 -1
  109. package/dist/types/src/extraction/extraction-llm-function/index.d.ts +0 -2
  110. package/dist/types/src/extraction/extraction-llm-function/index.d.ts.map +0 -1
  111. package/dist/types/src/extraction/extraction-ner-function/extraction-ner-function.d.ts +0 -347
  112. package/dist/types/src/extraction/extraction-ner-function/extraction-ner-function.d.ts.map +0 -1
  113. package/dist/types/src/extraction/extraction-ner-function/extraction-ner-function.test.d.ts +0 -2
  114. package/dist/types/src/extraction/extraction-ner-function/extraction-ner-function.test.d.ts.map +0 -1
  115. package/dist/types/src/session/GenericToolkit.d.ts +0 -85
  116. package/dist/types/src/session/GenericToolkit.d.ts.map +0 -1
  117. package/src/extraction/extraction-llm-function/extraction-llm-function.test.ts +0 -84
  118. package/src/extraction/extraction-llm-function/extraction-llm-function.ts +0 -62
  119. package/src/extraction/extraction-llm-function/index.ts +0 -5
  120. package/src/extraction/extraction-ner-function/extraction-ner-function.test.ts +0 -116
  121. package/src/extraction/extraction-ner-function/extraction-ner-function.ts +0 -56
  122. package/src/session/GenericToolkit.ts +0 -131
  123. /package/dist/lib/{browser → neutral}/chunk-J5LGTIGS.mjs +0 -0
  124. /package/dist/lib/{browser → neutral}/chunk-J5LGTIGS.mjs.map +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/assistant",
3
- "version": "0.8.4-main.937b3ca",
3
+ "version": "0.8.4-main.9be5663bfe",
4
4
  "description": "Natural language interface to DXOS",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -16,20 +16,17 @@
16
16
  ".": {
17
17
  "source": "./src/index.ts",
18
18
  "types": "./dist/types/src/index.d.ts",
19
- "browser": "./dist/lib/browser/index.mjs",
20
- "node": "./dist/lib/node-esm/index.mjs"
19
+ "default": "./dist/lib/neutral/index.mjs"
21
20
  },
22
21
  "./extraction": {
23
22
  "source": "./src/extraction/index.ts",
24
23
  "types": "./dist/types/src/extraction/index.d.ts",
25
- "browser": "./dist/lib/browser/extraction/index.mjs",
26
- "node": "./dist/lib/node-esm/extraction/index.mjs"
24
+ "default": "./dist/lib/neutral/extraction/index.mjs"
27
25
  },
28
26
  "./testing": {
29
27
  "source": "./src/testing/index.ts",
30
28
  "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"
29
+ "default": "./dist/lib/neutral/testing/index.mjs"
33
30
  }
34
31
  },
35
32
  "types": "dist/types/src/index.d.ts",
@@ -49,39 +46,42 @@
49
46
  ],
50
47
  "dependencies": {
51
48
  "@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",
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",
56
54
  "@xenova/transformers": "^2.17.2",
57
55
  "lodash.defaultsdeep": "^4.6.1",
58
- "@dxos/ai": "0.8.4-main.937b3ca",
59
- "@dxos/context": "0.8.4-main.937b3ca",
60
- "@dxos/blueprints": "0.8.4-main.937b3ca",
61
- "@dxos/async": "0.8.4-main.937b3ca",
62
- "@dxos/debug": "0.8.4-main.937b3ca",
63
- "@dxos/echo": "0.8.4-main.937b3ca",
64
- "@dxos/echo-db": "0.8.4-main.937b3ca",
65
- "@dxos/echo-protocol": "0.8.4-main.937b3ca",
66
- "@dxos/effect": "0.8.4-main.937b3ca",
67
- "@dxos/functions": "0.8.4-main.937b3ca",
68
- "@dxos/graph": "0.8.4-main.937b3ca",
69
- "@dxos/functions-runtime": "0.8.4-main.937b3ca",
70
- "@dxos/keys": "0.8.4-main.937b3ca",
71
- "@dxos/invariant": "0.8.4-main.937b3ca",
72
- "@dxos/node-std": "0.8.4-main.937b3ca",
73
- "@dxos/log": "0.8.4-main.937b3ca",
74
- "@dxos/schema": "0.8.4-main.937b3ca",
75
- "@dxos/util": "0.8.4-main.937b3ca",
76
- "@dxos/errors": "0.8.4-main.937b3ca",
77
- "@dxos/types": "0.8.4-main.937b3ca"
56
+ "@dxos/ai": "0.8.4-main.9be5663bfe",
57
+ "@dxos/async": "0.8.4-main.9be5663bfe",
58
+ "@dxos/debug": "0.8.4-main.9be5663bfe",
59
+ "@dxos/echo-db": "0.8.4-main.9be5663bfe",
60
+ "@dxos/context": "0.8.4-main.9be5663bfe",
61
+ "@dxos/echo-protocol": "0.8.4-main.9be5663bfe",
62
+ "@dxos/echo": "0.8.4-main.9be5663bfe",
63
+ "@dxos/effect": "0.8.4-main.9be5663bfe",
64
+ "@dxos/functions": "0.8.4-main.9be5663bfe",
65
+ "@dxos/errors": "0.8.4-main.9be5663bfe",
66
+ "@dxos/functions-runtime": "0.8.4-main.9be5663bfe",
67
+ "@dxos/blueprints": "0.8.4-main.9be5663bfe",
68
+ "@dxos/invariant": "0.8.4-main.9be5663bfe",
69
+ "@dxos/graph": "0.8.4-main.9be5663bfe",
70
+ "@dxos/keys": "0.8.4-main.9be5663bfe",
71
+ "@dxos/log": "0.8.4-main.9be5663bfe",
72
+ "@dxos/node-std": "0.8.4-main.9be5663bfe",
73
+ "@dxos/mcp-client": "0.8.4-main.9be5663bfe",
74
+ "@dxos/operation": "0.8.4-main.9be5663bfe",
75
+ "@dxos/schema": "0.8.4-main.9be5663bfe",
76
+ "@dxos/types": "0.8.4-main.9be5663bfe",
77
+ "@dxos/util": "0.8.4-main.9be5663bfe"
78
78
  },
79
79
  "devDependencies": {
80
80
  "@types/lodash.defaultsdeep": "^4.6.6",
81
- "effect": "3.19.11"
81
+ "effect": "3.20.0"
82
82
  },
83
83
  "peerDependencies": {
84
- "effect": "3.19.11"
84
+ "effect": "3.20.0"
85
85
  },
86
86
  "publishConfig": {
87
87
  "access": "public"
@@ -2,35 +2,41 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
+ import * as Context from 'effect/Context';
6
+ import * as Effect from 'effect/Effect';
7
+ import * as Layer from 'effect/Layer';
8
+ import * as Runtime from 'effect/Runtime';
5
9
  import * as Schema from 'effect/Schema';
6
10
  import { describe, it } from 'vitest';
7
11
 
8
- import { Obj, Ref, Type } from '@dxos/echo';
9
- import { type Queue } from '@dxos/echo-db';
12
+ import { Feed, Obj, Ref, Type } from '@dxos/echo';
10
13
 
11
14
  import { AiContextBinder } from './context';
12
15
 
13
- // TODO(burdon): Create mock queue?
14
- const createMockQueue = (): Queue =>
15
- ({
16
+ const createMockFeedRuntime = (): Runtime.Runtime<Feed.FeedService> => {
17
+ const mockFeedService: Context.Tag.Service<Feed.FeedService> = {
18
+ append: async () => {},
19
+ remove: async () => {},
16
20
  query: () =>
17
21
  ({
18
- subscribe: () => ({ unsubscribe: () => {} }),
22
+ subscribe: () => () => {},
19
23
  results: [],
24
+ run: async () => [],
20
25
  }) as any,
21
- append: async () => {
22
- return [] as any;
23
- },
24
- }) as any;
26
+ };
27
+ const layer = Layer.succeed(Feed.FeedService, mockFeedService);
28
+ return Effect.runSync(Effect.runtime<Feed.FeedService>().pipe(Effect.provide(layer)));
29
+ };
25
30
 
26
31
  describe('AiContextBinder', () => {
27
32
  it('should handle bind with Ref', async () => {
28
- const queue = createMockQueue();
29
- const binder = new AiContextBinder({ queue });
33
+ const feed = Feed.make();
34
+ const runtime = createMockFeedRuntime();
35
+ const binder = new AiContextBinder({ feed, runtime });
30
36
 
31
37
  const TestSchema = Schema.Struct({}).pipe(
32
38
  Type.object({
33
- typename: 'dxos.org/type/Example',
39
+ typename: 'org.dxos.type.example',
34
40
  version: '0.1.0',
35
41
  }),
36
42
  );
@@ -5,15 +5,17 @@
5
5
  import { Atom, Registry } from '@effect-atom/atom-react';
6
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';
10
+ import * as Runtime from 'effect/Runtime';
9
11
  import * as Schema from 'effect/Schema';
10
12
 
11
13
  import { Blueprint } from '@dxos/blueprints';
12
14
  import { Resource } from '@dxos/context';
13
- import { DXN, Obj, Query, type Ref, Type } from '@dxos/echo';
14
- import { type Queue } from '@dxos/echo-db';
15
+ import { DXN, Feed, Obj, type QueryResult, Query, Ref, Type } from '@dxos/echo';
16
+ import { assertArgument } from '@dxos/invariant';
15
17
  import { log } from '@dxos/log';
16
- import { ComplexSet, isTruthy } from '@dxos/util';
18
+ import { ComplexSet, isNonNullable } from '@dxos/util';
17
19
 
18
20
  /**
19
21
  * Thread message that binds or unbinds contextual objects to a conversation.
@@ -21,17 +23,17 @@ import { ComplexSet, isTruthy } from '@dxos/util';
21
23
  // TODO(burdon): Move to @dxos/schema ContentBlock?
22
24
  export const ContextBinding = Schema.Struct({
23
25
  blueprints: Schema.Struct({
24
- added: Schema.Array(Type.Ref(Blueprint.Blueprint)),
25
- removed: Schema.Array(Type.Ref(Blueprint.Blueprint)),
26
+ added: Schema.Array(Ref.Ref(Blueprint.Blueprint)),
27
+ removed: Schema.Array(Ref.Ref(Blueprint.Blueprint)),
26
28
  }),
27
29
 
28
30
  objects: Schema.Struct({
29
- added: Schema.Array(Type.Ref(Type.Obj)),
30
- removed: Schema.Array(Type.Ref(Type.Obj)),
31
+ added: Schema.Array(Ref.Ref(Obj.Unknown)),
32
+ removed: Schema.Array(Ref.Ref(Obj.Unknown)),
31
33
  }),
32
34
  }).pipe(
33
35
  Type.object({
34
- typename: 'dxos.org/type/ContextBinding',
36
+ typename: 'org.dxos.type.contextBinding',
35
37
  version: '0.1.0',
36
38
  }),
37
39
  );
@@ -58,7 +60,8 @@ export class Bindings {
58
60
  }
59
61
 
60
62
  export type AiContextBinderOptions = {
61
- queue: Queue;
63
+ feed: Feed.Feed;
64
+ runtime: Runtime.Runtime<Feed.FeedService>;
62
65
  registry?: Registry.Registry;
63
66
  };
64
67
 
@@ -70,11 +73,16 @@ export class AiContextBinder extends Resource {
70
73
  private readonly _blueprints = Atom.make<Blueprint.Blueprint[]>([]).pipe(Atom.keepAlive);
71
74
  private readonly _objects = Atom.make<Obj.Unknown[]>([]).pipe(Atom.keepAlive);
72
75
  private readonly _registry: Registry.Registry;
73
- private readonly _queue: Queue;
76
+ private readonly _feed: Feed.Feed;
77
+ private readonly _runtime: Runtime.Runtime<Feed.FeedService>;
78
+ #bindingsQuery: QueryResult.QueryResult<ContextBinding> | undefined;
74
79
 
75
80
  constructor(options: AiContextBinderOptions) {
76
81
  super();
77
- this._queue = options.queue;
82
+ assertArgument(options.feed, 'options.feed', 'Feed is required');
83
+ assertArgument(options.runtime, 'options.runtime', 'Feed runtime is required');
84
+ this._feed = options.feed;
85
+ this._runtime = options.runtime;
78
86
  this._registry = options.registry ?? Registry.make();
79
87
  }
80
88
 
@@ -121,20 +129,35 @@ export class AiContextBinder extends Resource {
121
129
  }
122
130
 
123
131
  protected override async _open(): Promise<void> {
124
- const query = this._queue.query(Query.type(ContextBinding));
132
+ this.#bindingsQuery = await Runtime.runPromise(this._runtime)(Feed.query(this._feed, Query.type(ContextBinding)));
125
133
 
126
134
  // Process initial state before returning.
127
- const initialResults = await query.run();
135
+ const initialResults = await this.#bindingsQuery.run();
128
136
  await this._updateBindings(initialResults);
129
137
 
130
138
  // Subscribe to future changes.
131
139
  this._ctx.onDispose(
132
- query.subscribe(async () => {
133
- await this._updateBindings(query.results);
140
+ this.#bindingsQuery.subscribe(async () => {
141
+ await this._updateBindings(this.#bindingsQuery!.results);
134
142
  }),
135
143
  );
136
144
  }
137
145
 
146
+ /**
147
+ * Re-reads bindings from the feed to pick up changes made by other processes.
148
+ */
149
+ async sync(): Promise<void> {
150
+ if (this.#bindingsQuery) {
151
+ const results = await this.#bindingsQuery.run();
152
+ log('sync', { bindingItems: results.length });
153
+ await this._updateBindings(results);
154
+ log('sync complete', {
155
+ blueprints: this._registry.get(this._blueprints).length,
156
+ blueprintKeys: this._registry.get(this._blueprints).map((bp) => (bp as any).key),
157
+ });
158
+ }
159
+ }
160
+
138
161
  private async _updateBindings(items: ContextBinding[]): Promise<void> {
139
162
  // Skip update if no items - preserve existing state set by bind().
140
163
  if (items.length === 0) {
@@ -143,19 +166,43 @@ export class AiContextBinder extends Resource {
143
166
 
144
167
  const bindings = this._reduce(items);
145
168
 
169
+ log('_updateBindings', {
170
+ items: items.length,
171
+ blueprintRefs: [...bindings.blueprints].map((ref) => ({ dxn: ref.dxn.toString(), available: ref.isAvailable })),
172
+ });
173
+
146
174
  // Resolve references (loading them first if needed).
147
175
  const currentBlueprints = this._registry.get(this._blueprints);
148
176
  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);
177
+ const resolvedBlueprints = await this._resolve(bindings.blueprints, currentBlueprints);
178
+ const resolvedObjects = await this._resolve(bindings.objects, currentObjects);
151
179
 
152
- // Atomic updates - subscribers notified automatically.
153
- this._registry.set(this._blueprints, newBlueprints);
154
- this._registry.set(this._objects, newObjects);
180
+ log('_updateBindings resolved', {
181
+ resolvedBlueprints: resolvedBlueprints.length,
182
+ resolvedBlueprintKeys: resolvedBlueprints.map((bp) => (bp as any).key),
183
+ });
184
+
185
+ // Filter current state to only items still in the reduced binding set,
186
+ // then merge in newly resolved items. This ensures unbind events are respected.
187
+ const reducedBlueprintDxns = new ComplexSet<DXN>(
188
+ DXN.hash,
189
+ [...bindings.blueprints].map((ref) => ref.dxn),
190
+ );
191
+ const reducedObjectDxns = new ComplexSet<DXN>(
192
+ DXN.hash,
193
+ [...bindings.objects].map((ref) => ref.dxn),
194
+ );
195
+ const filteredBlueprints = currentBlueprints.filter((obj) => reducedBlueprintDxns.has(Obj.getDXN(obj)));
196
+ const filteredObjects = currentObjects.filter((obj) => reducedObjectDxns.has(Obj.getDXN(obj)));
197
+ const mergedBlueprints = this._mergeInto(filteredBlueprints, resolvedBlueprints);
198
+ const mergedObjects = this._mergeInto(filteredObjects, resolvedObjects);
199
+
200
+ this._registry.set(this._blueprints, mergedBlueprints);
201
+ this._registry.set(this._objects, mergedObjects);
155
202
 
156
203
  log('updated bindings', {
157
- blueprints: newBlueprints.length,
158
- objects: newObjects.length,
204
+ blueprints: mergedBlueprints.length,
205
+ objects: mergedObjects.length,
159
206
  });
160
207
  }
161
208
 
@@ -180,18 +227,20 @@ export class AiContextBinder extends Resource {
180
227
  this._registry.set(this._objects, nextObjects);
181
228
 
182
229
  log('bind', { blueprints: addedBlueprints.length, objects: addedObjects.length });
183
- await this._queue.append([
184
- Obj.make(ContextBinding, {
185
- blueprints: {
186
- added: addedBlueprints,
187
- removed: [],
188
- },
189
- objects: {
190
- added: addedObjects,
191
- removed: [],
192
- },
193
- }),
194
- ]);
230
+ await Runtime.runPromise(this._runtime)(
231
+ Feed.append(this._feed, [
232
+ Obj.make(ContextBinding, {
233
+ blueprints: {
234
+ added: addedBlueprints,
235
+ removed: [],
236
+ },
237
+ objects: {
238
+ added: addedObjects,
239
+ removed: [],
240
+ },
241
+ }),
242
+ ]),
243
+ );
195
244
  }
196
245
 
197
246
  async unbind({ blueprints, objects }: BindingProps): Promise<void> {
@@ -199,19 +248,39 @@ export class AiContextBinder extends Resource {
199
248
  return;
200
249
  }
201
250
 
251
+ // Immediately update atom state so removals are reflected before the queue round-trips.
252
+ const removedBlueprintDxns = (blueprints ?? []).map((ref) => ref.dxn);
253
+ const removedObjectDxns = (objects ?? []).map((ref) => ref.dxn);
254
+ if (removedBlueprintDxns.length > 0) {
255
+ const current = this._registry.get(this._blueprints);
256
+ this._registry.set(
257
+ this._blueprints,
258
+ current.filter((obj) => !removedBlueprintDxns.some((dxn) => DXN.equalsEchoId(Obj.getDXN(obj), dxn))),
259
+ );
260
+ }
261
+ if (removedObjectDxns.length > 0) {
262
+ const current = this._registry.get(this._objects);
263
+ this._registry.set(
264
+ this._objects,
265
+ current.filter((obj) => !removedObjectDxns.some((dxn) => DXN.equalsEchoId(Obj.getDXN(obj), dxn))),
266
+ );
267
+ }
268
+
202
269
  log('unbind', { blueprints: blueprints?.length, objects: objects?.length });
203
- await this._queue.append([
204
- Obj.make(ContextBinding, {
205
- blueprints: {
206
- added: [],
207
- removed: blueprints ?? [],
208
- },
209
- objects: {
210
- added: [],
211
- removed: objects ?? [],
212
- },
213
- }),
214
- ]);
270
+ await Runtime.runPromise(this._runtime)(
271
+ Feed.append(this._feed, [
272
+ Obj.make(ContextBinding, {
273
+ blueprints: {
274
+ added: [],
275
+ removed: blueprints ?? [],
276
+ },
277
+ objects: {
278
+ added: [],
279
+ removed: objects ?? [],
280
+ },
281
+ }),
282
+ ]),
283
+ );
215
284
  }
216
285
 
217
286
  /**
@@ -271,14 +340,30 @@ export class AiContextBinder extends Resource {
271
340
  );
272
341
  }
273
342
 
343
+ /**
344
+ * Merge resolved items into the current set, adding only items not already present (by DXN).
345
+ */
346
+ private _mergeInto<T extends Obj.Unknown>(current: T[], resolved: T[]): T[] {
347
+ const seen = new Set(current.map((obj) => Obj.getDXN(obj).toString()));
348
+ const merged = [...current];
349
+ for (const obj of resolved) {
350
+ const dxn = Obj.getDXN(obj).toString();
351
+ if (!seen.has(dxn)) {
352
+ seen.add(dxn);
353
+ merged.push(obj);
354
+ }
355
+ }
356
+ return merged;
357
+ }
358
+
274
359
  /**
275
360
  * Resolve references to objects, loading them first if needed and falling back to existing objects.
276
361
  */
277
- private async _resolve<T>(refs: Iterable<Ref.Ref<T>>, current: T[]): Promise<T[]> {
362
+ private async _resolve<T extends Obj.Unknown>(refs: Iterable<Ref.Ref<T>>, current: T[]): Promise<T[]> {
278
363
  const refArray = [...refs];
279
364
 
280
365
  // Load all refs that need loading.
281
- await Promise.all(refArray.map((ref) => ref.load()));
366
+ await Promise.all(refArray.map((ref) => ref.tryLoad()));
282
367
 
283
368
  return refArray
284
369
  .map((ref) => {
@@ -291,7 +376,7 @@ export class AiContextBinder extends Resource {
291
376
  // Fallback to existing object.
292
377
  return target ?? current.find((obj) => Obj.getDXN(obj as any).toString() === ref.dxn.toString());
293
378
  })
294
- .filter(isTruthy);
379
+ .filter(isNonNullable);
295
380
  }
296
381
  }
297
382
 
@@ -300,4 +385,19 @@ export class AiContextService extends Context.Tag('@dxos/assistant/AiContextServ
300
385
  {
301
386
  binder: AiContextBinder;
302
387
  }
303
- >() {}
388
+ >() {
389
+ static bindContext = ({ blueprints, objects }: BindingProps): Effect.Effect<void, never, AiContextService> =>
390
+ Effect.gen(function* () {
391
+ const { binder } = yield* AiContextService;
392
+ yield* Effect.promise(() => binder.bind({ blueprints, objects }));
393
+ });
394
+
395
+ static findObjects = <T extends Type.AnyEntity>(
396
+ type: T,
397
+ ): Effect.Effect<Schema.Schema.Type<T>[], never, AiContextService> => {
398
+ return Effect.gen(function* () {
399
+ const { binder } = yield* AiContextService;
400
+ return binder.getObjects().filter(Obj.instanceOf(type));
401
+ });
402
+ };
403
+ }
@@ -4,32 +4,26 @@
4
4
 
5
5
  import { it } from '@effect/vitest';
6
6
  import * as Effect from 'effect/Effect';
7
- import { afterEach, beforeEach, describe, expect } from 'vitest';
7
+ import { describe, expect } from 'vitest';
8
8
 
9
9
  import { Blueprint } from '@dxos/blueprints';
10
- import { Obj, Ref } from '@dxos/echo';
11
- import { EchoTestBuilder } from '@dxos/echo-db/testing';
10
+ import { Database, Feed, Obj, Ref } from '@dxos/echo';
11
+ import { TestDatabaseLayer } from '@dxos/functions-runtime/testing';
12
12
 
13
13
  import { ContextBinding } from './context';
14
14
  import { AiConversation } from './conversation';
15
15
 
16
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();
17
+ const TestLayer = TestDatabaseLayer({
18
+ types: [Blueprint.Blueprint, ContextBinding],
25
19
  });
26
20
 
27
21
  it.effect('loads blueprints on open', () =>
28
22
  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>();
23
+ const { db } = yield* Database.Service;
24
+
25
+ // Create feed.
26
+ const feed = db.add(Feed.make());
33
27
 
34
28
  // Create blueprint.
35
29
  const blueprint = db.add(
@@ -39,27 +33,26 @@ describe('AiConversation', () => {
39
33
  }),
40
34
  );
41
35
 
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
- );
36
+ // Add blueprint to feed via binding.
37
+ yield* Feed.append(feed, [
38
+ Obj.make(ContextBinding, {
39
+ blueprints: {
40
+ added: [Ref.make(blueprint)],
41
+ removed: [],
42
+ },
43
+ objects: {
44
+ added: [],
45
+ removed: [],
46
+ },
47
+ }),
48
+ ]);
57
49
 
58
- const conversation = new AiConversation({ queue });
50
+ const runtime = yield* Effect.runtime<Feed.FeedService>();
51
+ const conversation = new AiConversation({ feed, runtime });
59
52
  yield* Effect.promise(() => conversation.open());
60
53
 
61
54
  expect(conversation.context.getBlueprints()).toHaveLength(1);
62
55
  expect(conversation.context.getObjects()).toHaveLength(0);
63
- }),
56
+ }).pipe(Effect.provide(TestLayer)),
64
57
  );
65
58
  });