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

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 (121) hide show
  1. package/README.md +1 -0
  2. package/dist/lib/neutral/chunk-KVYJFFLF.mjs +1805 -0
  3. package/dist/lib/neutral/chunk-KVYJFFLF.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 +72 -371
  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 +112 -0
  10. package/dist/lib/neutral/testing/index.mjs.map +7 -0
  11. package/dist/types/src/conversation/context.d.ts +24 -5
  12. package/dist/types/src/conversation/context.d.ts.map +1 -1
  13. package/dist/types/src/conversation/conversation.d.ts +29 -4
  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 +729 -17
  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 +62 -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/index.d.ts +1 -1
  36. package/dist/types/src/session/index.d.ts.map +1 -1
  37. package/dist/types/src/session/observer.d.ts.map +1 -1
  38. package/dist/types/src/session/session.d.ts +64 -8
  39. package/dist/types/src/session/session.d.ts.map +1 -1
  40. package/dist/types/src/session/toolkit.d.ts +6 -2
  41. package/dist/types/src/session/toolkit.d.ts.map +1 -1
  42. package/dist/types/src/templates/system.d.ts +0 -1
  43. package/dist/types/src/templates/system.d.ts.map +1 -1
  44. package/dist/types/src/testing/layer.d.ts +23 -9
  45. package/dist/types/src/testing/layer.d.ts.map +1 -1
  46. package/dist/types/src/tracing.d.ts +264 -0
  47. package/dist/types/src/tracing.d.ts.map +1 -0
  48. package/dist/types/src/types/artifact.d.ts +5 -5
  49. package/dist/types/src/types/artifact.d.ts.map +1 -1
  50. package/dist/types/tsconfig.tsbuildinfo +1 -1
  51. package/package.json +33 -33
  52. package/src/conversation/context.test.ts +1 -1
  53. package/src/conversation/context.ts +84 -18
  54. package/src/conversation/conversation.ts +173 -32
  55. package/src/diff.test.ts +36 -5
  56. package/src/diff.ts +13 -4
  57. package/src/execution-graph.test.ts +2 -2
  58. package/src/execution-graph.ts +2 -2
  59. package/src/extraction/extraction-ner-function/index.ts +0 -1
  60. package/src/extraction/extraction.ts +38 -10
  61. package/src/extraction/index.ts +0 -1
  62. package/src/functions/services.ts +199 -50
  63. package/src/functions.conversations.json +1 -1
  64. package/src/functions.test.ts +28 -20
  65. package/src/index.ts +2 -0
  66. package/src/service/AgentService.conversations.json +1 -0
  67. package/src/service/AgentService.test.ts +169 -0
  68. package/src/service/AgentService.ts +171 -0
  69. package/src/service/agent-process.ts +360 -0
  70. package/src/service/index.ts +6 -0
  71. package/src/session/format.test.ts +23 -14
  72. package/src/session/index.ts +1 -2
  73. package/src/session/observer.ts +3 -2
  74. package/src/session/session.conversations.json +1 -1
  75. package/src/session/session.test.ts +52 -4
  76. package/src/session/session.ts +231 -117
  77. package/src/session/toolkit.ts +12 -7
  78. package/src/templates/instructions/format.tpl +0 -4
  79. package/src/templates/instructions/system.tpl +11 -2
  80. package/src/templates/system.ts +6 -5
  81. package/src/testing/layer.ts +209 -24
  82. package/src/tracing.ts +112 -0
  83. package/src/types/artifact.ts +17 -10
  84. package/dist/lib/browser/chunk-7CDDG6LL.mjs +0 -578
  85. package/dist/lib/browser/chunk-7CDDG6LL.mjs.map +0 -7
  86. package/dist/lib/browser/extraction/index.mjs.map +0 -7
  87. package/dist/lib/browser/index.mjs.map +0 -7
  88. package/dist/lib/browser/meta.json +0 -1
  89. package/dist/lib/browser/testing/index.mjs +0 -32
  90. package/dist/lib/browser/testing/index.mjs.map +0 -7
  91. package/dist/lib/node-esm/chunk-CC7ER6Q3.mjs +0 -579
  92. package/dist/lib/node-esm/chunk-CC7ER6Q3.mjs.map +0 -7
  93. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs +0 -11
  94. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs.map +0 -7
  95. package/dist/lib/node-esm/extraction/index.mjs +0 -226
  96. package/dist/lib/node-esm/extraction/index.mjs.map +0 -7
  97. package/dist/lib/node-esm/index.mjs +0 -851
  98. package/dist/lib/node-esm/index.mjs.map +0 -7
  99. package/dist/lib/node-esm/meta.json +0 -1
  100. package/dist/lib/node-esm/testing/index.mjs +0 -33
  101. package/dist/lib/node-esm/testing/index.mjs.map +0 -7
  102. package/dist/types/src/extraction/extraction-llm-function/extraction-llm-function.d.ts +0 -4
  103. package/dist/types/src/extraction/extraction-llm-function/extraction-llm-function.d.ts.map +0 -1
  104. package/dist/types/src/extraction/extraction-llm-function/extraction-llm-function.test.d.ts +0 -2
  105. package/dist/types/src/extraction/extraction-llm-function/extraction-llm-function.test.d.ts.map +0 -1
  106. package/dist/types/src/extraction/extraction-llm-function/index.d.ts +0 -2
  107. package/dist/types/src/extraction/extraction-llm-function/index.d.ts.map +0 -1
  108. package/dist/types/src/extraction/extraction-ner-function/extraction-ner-function.d.ts +0 -347
  109. package/dist/types/src/extraction/extraction-ner-function/extraction-ner-function.d.ts.map +0 -1
  110. package/dist/types/src/extraction/extraction-ner-function/extraction-ner-function.test.d.ts +0 -2
  111. package/dist/types/src/extraction/extraction-ner-function/extraction-ner-function.test.d.ts.map +0 -1
  112. package/dist/types/src/session/GenericToolkit.d.ts +0 -85
  113. package/dist/types/src/session/GenericToolkit.d.ts.map +0 -1
  114. package/src/extraction/extraction-llm-function/extraction-llm-function.test.ts +0 -84
  115. package/src/extraction/extraction-llm-function/extraction-llm-function.ts +0 -62
  116. package/src/extraction/extraction-llm-function/index.ts +0 -5
  117. package/src/extraction/extraction-ner-function/extraction-ner-function.test.ts +0 -116
  118. package/src/extraction/extraction-ner-function/extraction-ner-function.ts +0 -56
  119. package/src/session/GenericToolkit.ts +0 -131
  120. /package/dist/lib/{browser → neutral}/chunk-J5LGTIGS.mjs +0 -0
  121. /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.bc674ce",
3
+ "version": "0.8.4-main.bcb3aa67d6",
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.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"
56
+ "@dxos/ai": "0.8.4-main.bcb3aa67d6",
57
+ "@dxos/async": "0.8.4-main.bcb3aa67d6",
58
+ "@dxos/blueprints": "0.8.4-main.bcb3aa67d6",
59
+ "@dxos/echo": "0.8.4-main.bcb3aa67d6",
60
+ "@dxos/echo-db": "0.8.4-main.bcb3aa67d6",
61
+ "@dxos/debug": "0.8.4-main.bcb3aa67d6",
62
+ "@dxos/echo-protocol": "0.8.4-main.bcb3aa67d6",
63
+ "@dxos/errors": "0.8.4-main.bcb3aa67d6",
64
+ "@dxos/effect": "0.8.4-main.bcb3aa67d6",
65
+ "@dxos/functions-runtime": "0.8.4-main.bcb3aa67d6",
66
+ "@dxos/graph": "0.8.4-main.bcb3aa67d6",
67
+ "@dxos/functions": "0.8.4-main.bcb3aa67d6",
68
+ "@dxos/invariant": "0.8.4-main.bcb3aa67d6",
69
+ "@dxos/keys": "0.8.4-main.bcb3aa67d6",
70
+ "@dxos/node-std": "0.8.4-main.bcb3aa67d6",
71
+ "@dxos/mcp-client": "0.8.4-main.bcb3aa67d6",
72
+ "@dxos/log": "0.8.4-main.bcb3aa67d6",
73
+ "@dxos/operation": "0.8.4-main.bcb3aa67d6",
74
+ "@dxos/schema": "0.8.4-main.bcb3aa67d6",
75
+ "@dxos/context": "0.8.4-main.bcb3aa67d6",
76
+ "@dxos/types": "0.8.4-main.bcb3aa67d6",
77
+ "@dxos/util": "0.8.4-main.bcb3aa67d6"
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"
@@ -30,7 +30,7 @@ describe('AiContextBinder', () => {
30
30
 
31
31
  const TestSchema = Schema.Struct({}).pipe(
32
32
  Type.object({
33
- typename: 'dxos.org/type/Example',
33
+ typename: 'org.dxos.type.example',
34
34
  version: '0.1.0',
35
35
  }),
36
36
  );
@@ -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';
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, Obj, Query, type Ref, Type } from '@dxos/echo';
14
+ import { DXN, Obj, Query, Ref, Type } from '@dxos/echo';
14
15
  import { type Queue } from '@dxos/echo-db';
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
  );
@@ -74,6 +76,7 @@ export class AiContextBinder extends Resource {
74
76
 
75
77
  constructor(options: AiContextBinderOptions) {
76
78
  super();
79
+ assertArgument(options.queue, 'options.queue', 'Queue is required');
77
80
  this._queue = options.queue;
78
81
  this._registry = options.registry ?? Registry.make();
79
82
  }
@@ -146,16 +149,30 @@ export class AiContextBinder extends Resource {
146
149
  // Resolve references (loading them first if needed).
147
150
  const currentBlueprints = this._registry.get(this._blueprints);
148
151
  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);
152
+ const resolvedBlueprints = await this._resolve(bindings.blueprints, currentBlueprints);
153
+ const resolvedObjects = await this._resolve(bindings.objects, currentObjects);
154
+
155
+ // Filter current state to only items still in the reduced binding set,
156
+ // then merge in newly resolved items. This ensures unbind events are respected.
157
+ const reducedBlueprintDxns = new ComplexSet<DXN>(
158
+ DXN.hash,
159
+ [...bindings.blueprints].map((ref) => ref.dxn),
160
+ );
161
+ const reducedObjectDxns = new ComplexSet<DXN>(
162
+ DXN.hash,
163
+ [...bindings.objects].map((ref) => ref.dxn),
164
+ );
165
+ const filteredBlueprints = currentBlueprints.filter((obj) => reducedBlueprintDxns.has(Obj.getDXN(obj)));
166
+ const filteredObjects = currentObjects.filter((obj) => reducedObjectDxns.has(Obj.getDXN(obj)));
167
+ const mergedBlueprints = this._mergeInto(filteredBlueprints, resolvedBlueprints);
168
+ const mergedObjects = this._mergeInto(filteredObjects, resolvedObjects);
151
169
 
152
- // Atomic updates - subscribers notified automatically.
153
- this._registry.set(this._blueprints, newBlueprints);
154
- this._registry.set(this._objects, newObjects);
170
+ this._registry.set(this._blueprints, mergedBlueprints);
171
+ this._registry.set(this._objects, mergedObjects);
155
172
 
156
173
  log('updated bindings', {
157
- blueprints: newBlueprints.length,
158
- objects: newObjects.length,
174
+ blueprints: mergedBlueprints.length,
175
+ objects: mergedObjects.length,
159
176
  });
160
177
  }
161
178
 
@@ -199,6 +216,24 @@ export class AiContextBinder extends Resource {
199
216
  return;
200
217
  }
201
218
 
219
+ // Immediately update atom state so removals are reflected before the queue round-trips.
220
+ const removedBlueprintDxns = (blueprints ?? []).map((ref) => ref.dxn);
221
+ const removedObjectDxns = (objects ?? []).map((ref) => ref.dxn);
222
+ if (removedBlueprintDxns.length > 0) {
223
+ const current = this._registry.get(this._blueprints);
224
+ this._registry.set(
225
+ this._blueprints,
226
+ current.filter((obj) => !removedBlueprintDxns.some((dxn) => DXN.equalsEchoId(Obj.getDXN(obj), dxn))),
227
+ );
228
+ }
229
+ if (removedObjectDxns.length > 0) {
230
+ const current = this._registry.get(this._objects);
231
+ this._registry.set(
232
+ this._objects,
233
+ current.filter((obj) => !removedObjectDxns.some((dxn) => DXN.equalsEchoId(Obj.getDXN(obj), dxn))),
234
+ );
235
+ }
236
+
202
237
  log('unbind', { blueprints: blueprints?.length, objects: objects?.length });
203
238
  await this._queue.append([
204
239
  Obj.make(ContextBinding, {
@@ -271,14 +306,30 @@ export class AiContextBinder extends Resource {
271
306
  );
272
307
  }
273
308
 
309
+ /**
310
+ * Merge resolved items into the current set, adding only items not already present (by DXN).
311
+ */
312
+ private _mergeInto<T extends Obj.Unknown>(current: T[], resolved: T[]): T[] {
313
+ const seen = new Set(current.map((obj) => Obj.getDXN(obj).toString()));
314
+ const merged = [...current];
315
+ for (const obj of resolved) {
316
+ const dxn = Obj.getDXN(obj).toString();
317
+ if (!seen.has(dxn)) {
318
+ seen.add(dxn);
319
+ merged.push(obj);
320
+ }
321
+ }
322
+ return merged;
323
+ }
324
+
274
325
  /**
275
326
  * Resolve references to objects, loading them first if needed and falling back to existing objects.
276
327
  */
277
- private async _resolve<T>(refs: Iterable<Ref.Ref<T>>, current: T[]): Promise<T[]> {
328
+ private async _resolve<T extends Obj.Unknown>(refs: Iterable<Ref.Ref<T>>, current: T[]): Promise<T[]> {
278
329
  const refArray = [...refs];
279
330
 
280
331
  // Load all refs that need loading.
281
- await Promise.all(refArray.map((ref) => ref.load()));
332
+ await Promise.all(refArray.map((ref) => ref.tryLoad()));
282
333
 
283
334
  return refArray
284
335
  .map((ref) => {
@@ -291,7 +342,7 @@ export class AiContextBinder extends Resource {
291
342
  // Fallback to existing object.
292
343
  return target ?? current.find((obj) => Obj.getDXN(obj as any).toString() === ref.dxn.toString());
293
344
  })
294
- .filter(isTruthy);
345
+ .filter(isNonNullable);
295
346
  }
296
347
  }
297
348
 
@@ -300,4 +351,19 @@ export class AiContextService extends Context.Tag('@dxos/assistant/AiContextServ
300
351
  {
301
352
  binder: AiContextBinder;
302
353
  }
303
- >() {}
354
+ >() {
355
+ static bindContext = ({ blueprints, objects }: BindingProps): Effect.Effect<void, never, AiContextService> =>
356
+ Effect.gen(function* () {
357
+ const { binder } = yield* AiContextService;
358
+ yield* Effect.promise(() => binder.bind({ blueprints, objects }));
359
+ });
360
+
361
+ static findObjects = <T extends Type.AnyEntity>(
362
+ type: T,
363
+ ): Effect.Effect<Schema.Schema.Type<T>[], never, AiContextService> => {
364
+ return Effect.gen(function* () {
365
+ const { binder } = yield* AiContextService;
366
+ return binder.getObjects().filter(Obj.instanceOf(type));
367
+ });
368
+ };
369
+ }
@@ -2,16 +2,29 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
+ import type * as Tool from '@effect/ai/Tool';
5
6
  import type * as Toolkit from '@effect/ai/Toolkit';
6
7
  import type { Registry } from '@effect-atom/atom-react';
8
+ import * as Array from 'effect/Array';
9
+ import * as Context from 'effect/Context';
7
10
  import * as Effect from 'effect/Effect';
11
+ import * as Either from 'effect/Either';
12
+ import { pipe } from 'effect/Function';
13
+ import * as Layer from 'effect/Layer';
8
14
 
15
+ import { type ToolExecutionService, type ToolResolverService } from '@dxos/ai';
16
+ import { type GenericToolkit } from '@dxos/ai';
17
+ import { type Blueprint } from '@dxos/blueprints';
9
18
  import { Resource } from '@dxos/context';
10
19
  import { Obj } from '@dxos/echo';
11
20
  import { type Queue } from '@dxos/echo-db';
21
+ import { acquireReleaseResource } from '@dxos/effect';
22
+ import { QueueService } from '@dxos/functions';
12
23
  import { invariant } from '@dxos/invariant';
13
24
  import { log } from '@dxos/log';
25
+ import { McpToolkit } from '@dxos/mcp-client';
14
26
  import { Message } from '@dxos/types';
27
+ import * as Record from 'effect/Record';
15
28
 
16
29
  import {
17
30
  AiSession,
@@ -19,22 +32,29 @@ import {
19
32
  type AiSessionRunRequirements,
20
33
  type GenerationObserver,
21
34
  createToolkit,
35
+ formatSystemPrompt,
22
36
  } from '../session';
23
37
 
24
38
  import { AiContextBinder, AiContextService, type ContextBinding } from './context';
25
39
 
26
- export interface AiConversationRunProps {
40
+ export interface AiConversationRunProps<Tools extends Record<string, Tool.Any>> {
27
41
  prompt: string;
28
42
  system?: string;
29
43
  observer?: GenerationObserver;
44
+ toolkit?: Toolkit.Toolkit<Tools>;
30
45
  }
31
46
 
32
47
  export type AiConversationOptions = {
33
48
  queue: Queue<Message.Message | ContextBinding>;
34
- toolkit?: Toolkit.Any;
35
49
  registry?: Registry.Registry;
36
50
  };
37
51
 
52
+ // TODO(dmaretskyi): Take from model characteristics. opus has 200k max tokens.
53
+ /**
54
+ * Summarization threshold in tokens.
55
+ */
56
+ const SUMMARY_THRESHOLD = 80_000;
57
+
38
58
  /**
39
59
  * Durable conversation state (initiated by users and agents) backed by a Queue.
40
60
  * Executes tools based on AI responses and supports cancellation of in-progress requests.
@@ -50,7 +70,6 @@ export class AiConversation extends Resource {
50
70
  public constructor(options: AiConversationOptions) {
51
71
  super();
52
72
  this._queue = options.queue;
53
- this._toolkit = options.toolkit;
54
73
  invariant(this._queue);
55
74
  this._binder = new AiContextBinder({ queue: this._queue, registry: options.registry });
56
75
  }
@@ -76,50 +95,172 @@ export class AiConversation extends Resource {
76
95
  return queueItems.filter(Obj.instanceOf(Message.Message));
77
96
  }
78
97
 
98
+ getTools(): Effect.Effect<Record<string, Tool.Any>, never, ToolExecutionService | ToolResolverService> {
99
+ return Effect.gen(this, function* () {
100
+ const blueprints = this.context.getBlueprints();
101
+ const tookit = yield* createToolkit({ toolkit: this._toolkit, blueprints });
102
+ return tookit.tools;
103
+ }).pipe(Effect.orDie);
104
+ }
105
+
79
106
  /**
80
107
  * Creates a new cancelable request effect.
81
108
  */
82
- public createRequest(
83
- params: AiConversationRunProps,
84
- ): Effect.Effect<Message.Message[], AiSessionRunError, AiSessionRunRequirements> {
85
- const self = this;
86
- return Effect.gen(function* () {
87
- const history = yield* Effect.promise(() => self.getHistory());
88
-
89
- // Create toolkit.
90
- const blueprints = self.context.getBlueprints();
91
- const toolkit = yield* createToolkit({
92
- toolkit: self._toolkit,
93
- blueprints,
94
- });
95
-
96
- // Context objects.
97
- const objects = self.context.getObjects();
109
+ public createRequest<Tools extends Record<string, Tool.Any> = {}>(
110
+ params: AiConversationRunProps<Tools>,
111
+ ): Effect.Effect<Message.Message[], AiSessionRunError, AiSessionRunRequirements | Tool.HandlersFor<Tools>> {
112
+ return Effect.gen(this, function* () {
113
+ const history = yield* Effect.promise(() => this.getHistory());
114
+ const blueprints = this.context.getBlueprints();
115
+ const objects = this.context.getObjects();
98
116
 
99
117
  log('run', {
100
118
  history: history.length,
101
119
  blueprints: blueprints.length,
102
- tools: Object.keys(toolkit.tools).length,
103
120
  objects: objects.length,
104
121
  });
105
122
 
106
- // Process request.
107
- const session = new AiSession();
108
- const messages = yield* session.run({ history, blueprints, toolkit, objects, ...params }).pipe(
109
- Effect.provideService(AiContextService, {
110
- binder: self.context,
111
- }),
112
- );
123
+ const session = new AiSession({
124
+ summarizationThreshold: SUMMARY_THRESHOLD,
125
+ observer: params.observer,
126
+ onOutput: (message) => Effect.promise(() => this._queue.append([message])),
127
+ });
128
+
129
+ yield* session.begin({
130
+ history,
131
+ blueprints,
132
+ objects,
133
+ prompt: params.prompt,
134
+ system: params.system,
135
+ });
136
+
137
+ // Turn loop: recompute toolkit and system prompt between turns to pick up dynamically enabled blueprints.
138
+ do {
139
+ const currentBlueprints = this.context.getBlueprints();
140
+ const mcps = yield* connectMcpServers(currentBlueprints);
141
+ const toolkit = yield* createToolkit({
142
+ toolkit: params.toolkit,
143
+ blueprints: currentBlueprints,
144
+ genericToolkits: mcps,
145
+ });
146
+ log('toolkit', { tools: Record.keys(toolkit.tools) });
147
+ const system = yield* formatSystemPrompt({
148
+ system: params.system,
149
+ blueprints: currentBlueprints,
150
+ objects: this.context.getObjects(),
151
+ }).pipe(Effect.orDie);
152
+
153
+ const { done } = yield* session.runAgentTurn({
154
+ system,
155
+ toolkit,
156
+ });
157
+
158
+ if (done) {
159
+ break;
160
+ }
161
+
162
+ yield* session.runTools({ toolkit });
163
+ } while (true);
113
164
 
114
165
  log('result', {
115
- messages: messages.length,
166
+ messages: session.pending.length,
116
167
  duration: session.duration,
117
168
  toolCalls: session.toolCalls,
118
169
  });
119
170
 
120
- // Append to queue.
121
- yield* Effect.promise(() => self._queue.append(messages));
122
- return messages;
123
- });
171
+ return [...session.pending];
172
+ }).pipe(
173
+ Effect.provide(
174
+ Layer.mergeAll(
175
+ Layer.succeed(AiContextService, {
176
+ binder: this.context,
177
+ }),
178
+ Layer.succeed(AiConversationService, this),
179
+ ),
180
+ ),
181
+ );
124
182
  }
125
183
  }
184
+
185
+ /**
186
+ * Gives access to the ai conversation.
187
+ */
188
+ export class AiConversationService extends Context.Tag('@dxos/assistant/AiConversationService')<
189
+ AiConversationService,
190
+ AiConversation
191
+ >() {
192
+ /**
193
+ * Create a new conversation layer from options.
194
+ */
195
+ static layer = (options: AiConversationOptions): Layer.Layer<AiConversationService | AiContextService> =>
196
+ aiContextFromConversation.pipe(
197
+ Layer.provideMerge(
198
+ Layer.scoped(
199
+ AiConversationService,
200
+ Effect.gen(function* () {
201
+ const conversation = yield* acquireReleaseResource(() => new AiConversation(options));
202
+ return conversation;
203
+ }),
204
+ ),
205
+ ),
206
+ );
207
+
208
+ /**
209
+ * Create a new conversation with a new queue.
210
+ */
211
+ static layerNewQueue = (
212
+ options?: Omit<AiConversationOptions, 'queue'>,
213
+ ): Layer.Layer<AiConversationService | AiContextService, never, QueueService> =>
214
+ Layer.unwrapScoped(
215
+ Effect.gen(function* () {
216
+ return AiConversationService.layer({
217
+ ...options,
218
+ queue: yield* QueueService.createQueue<Message.Message | ContextBinding>(),
219
+ });
220
+ }),
221
+ );
222
+
223
+ /**
224
+ * Run a prompt in the current conversation.
225
+ */
226
+ static run = <Tools extends Record<string, Tool.Any> = {}>(
227
+ params: AiConversationRunProps<Tools>,
228
+ ): Effect.Effect<
229
+ Message.Message[],
230
+ AiSessionRunError,
231
+ AiSessionRunRequirements | Tool.HandlersFor<Tools> | AiConversationService
232
+ > =>
233
+ Effect.gen(function* () {
234
+ const conversation = yield* AiConversationService;
235
+ return yield* conversation.createRequest(params);
236
+ });
237
+ }
238
+
239
+ const aiContextFromConversation = Layer.effect(
240
+ AiContextService,
241
+ Effect.gen(function* () {
242
+ const conversation = yield* AiConversationService;
243
+ return {
244
+ binder: conversation.context,
245
+ };
246
+ }),
247
+ );
248
+
249
+ const connectMcpServers = (
250
+ blueprints: readonly Blueprint.Blueprint[],
251
+ ): Effect.Effect<GenericToolkit.GenericToolkit[]> =>
252
+ pipe(
253
+ blueprints,
254
+ Array.flatMap((_) => _.mcpServers ?? []),
255
+ Effect.forEach(({ url, protocol }) =>
256
+ McpToolkit.make({ url, kind: protocol }).pipe(
257
+ // NOTE: Type-inference fails here without explicit void return.
258
+ Effect.tap((toolkit): void =>
259
+ log.info('Connected to MCP server', { url, tools: Object.keys(toolkit.toolkit.tools).length }),
260
+ ),
261
+ Effect.tapDefect((error) => Effect.sync(() => log.warn('Failed to connect to MCP server', { error }))),
262
+ Effect.either,
263
+ ),
264
+ ),
265
+ Effect.map(Array.filterMap((_) => Either.getRight(_))),
266
+ );
package/src/diff.test.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { describe, expect, it } from 'vitest';
5
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
6
6
 
7
7
  import { createDocAccessor } from '@dxos/echo-db';
8
8
  import { EchoTestBuilder } from '@dxos/echo-db/testing';
@@ -12,10 +12,17 @@ import { trim } from '@dxos/util';
12
12
  import { applyDiffs } from './diff';
13
13
 
14
14
  describe('diff', () => {
15
+ let builder: EchoTestBuilder;
16
+ beforeEach(async () => {
17
+ builder = await new EchoTestBuilder().open();
18
+ });
19
+
20
+ afterEach(async () => {
21
+ await builder.close();
22
+ });
23
+
15
24
  it('should apply diffs', async () => {
16
- const builder = new EchoTestBuilder();
17
- const { db, graph } = await builder.createDatabase();
18
- await graph.schemaRegistry.register([Text.Text]);
25
+ const { db } = await builder.createDatabase({ types: [Text.Text] });
19
26
 
20
27
  const document = trim`
21
28
  # Hello World
@@ -24,7 +31,7 @@ describe('diff', () => {
24
31
  But not this one.
25
32
  `;
26
33
 
27
- const text = db.add(Text.make(document));
34
+ const text = db.add(Text.make({ content: document }));
28
35
  const accessor = createDocAccessor(text, ['content']);
29
36
  const result = applyDiffs(accessor, [
30
37
  `- There'z a typo in this sentence.`,
@@ -40,4 +47,28 @@ describe('diff', () => {
40
47
  But not this one.
41
48
  `);
42
49
  });
50
+
51
+ it('should apply diffs that append content at the end', async () => {
52
+ const { db } = await builder.createDatabase({ types: [Text.Text] });
53
+
54
+ const document = '# Shopping list';
55
+
56
+ const text = db.add(Text.make({ content: document }));
57
+ const accessor = createDocAccessor(text, ['content']);
58
+ const result = applyDiffs(accessor, ['- # Shopping list', '+ # Shopping list\n- Milk']);
59
+
60
+ expect(result).toBe('# Shopping list\n- Milk');
61
+ });
62
+
63
+ it('should append text to an empty document', async () => {
64
+ const builder = new EchoTestBuilder();
65
+ const { db, graph } = await builder.createDatabase();
66
+ await graph.schemaRegistry.register([Text.Text]);
67
+
68
+ const text = db.add(Text.make({ content: '' }));
69
+ const accessor = createDocAccessor(text, ['content']);
70
+ const result = applyDiffs(accessor, ['+ # Shopping list']);
71
+
72
+ expect(result).toBe('# Shopping list');
73
+ });
43
74
  });
package/src/diff.ts CHANGED
@@ -47,9 +47,10 @@ export const reduceDiffs = (diffs: readonly string[]): Diff[] => {
47
47
  }
48
48
  return acc;
49
49
  }
50
+ default: {
51
+ throw new Error(`Invalid diff format: each line must start with "- " or "+ ", got: ${JSON.stringify(diff)}`);
52
+ }
50
53
  }
51
-
52
- return acc;
53
54
  }, []);
54
55
  };
55
56
 
@@ -65,7 +66,11 @@ export const computeDiffsWithCursors = <T>(accessor: DocAccessor<T>, diffs: read
65
66
  .filter(isNonNullable);
66
67
  };
67
68
 
68
- export const applyDiffs = <T>(accessor: DocAccessor<T>, diffs: readonly string[]): string => {
69
+ export const applyDiffs = <T>(
70
+ accessor: DocAccessor<T>,
71
+ diffs: readonly string[],
72
+ { errorOnNotFound = false }: { errorOnNotFound?: boolean } = {},
73
+ ): string => {
69
74
  for (const diff of reduceDiffs(diffs)) {
70
75
  accessor.handle.change((doc: Doc<T>) => {
71
76
  const text = DocAccessor.getValue<string>(accessor);
@@ -75,7 +80,11 @@ export const applyDiffs = <T>(accessor: DocAccessor<T>, diffs: readonly string[]
75
80
  // NOTE: This only replaces the first match.
76
81
  A.splice(doc, accessor.path as A.Prop[], idx, diff.match.length, diff.replace);
77
82
  } else {
78
- log.warn('diff not found', diff);
83
+ if (errorOnNotFound) {
84
+ throw new Error(`Diff not found: ${diff.match}`);
85
+ } else {
86
+ log.warn('diff not found', diff);
87
+ }
79
88
  }
80
89
  });
81
90
  }