@dxos/assistant 0.8.3 → 0.8.4-main.28f8d3d

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 (152) hide show
  1. package/dist/lib/browser/chunk-6WRGDEMH.mjs +89 -0
  2. package/dist/lib/browser/chunk-6WRGDEMH.mjs.map +7 -0
  3. package/dist/lib/browser/chunk-NZGGCEF3.mjs +341 -0
  4. package/dist/lib/browser/chunk-NZGGCEF3.mjs.map +7 -0
  5. package/dist/lib/browser/extraction/index.mjs +269 -0
  6. package/dist/lib/browser/extraction/index.mjs.map +7 -0
  7. package/dist/lib/browser/index.mjs +306 -2110
  8. package/dist/lib/browser/index.mjs.map +4 -4
  9. package/dist/lib/browser/meta.json +1 -1
  10. package/dist/lib/browser/named-entity-recognition-O46UTCLR.mjs +13 -0
  11. package/dist/lib/browser/named-entity-recognition-O46UTCLR.mjs.map +7 -0
  12. package/dist/lib/node-esm/chunk-3UU5V4FS.mjs +91 -0
  13. package/dist/lib/node-esm/chunk-3UU5V4FS.mjs.map +7 -0
  14. package/dist/lib/node-esm/chunk-XOOPGSGW.mjs +343 -0
  15. package/dist/lib/node-esm/chunk-XOOPGSGW.mjs.map +7 -0
  16. package/dist/lib/node-esm/extraction/index.mjs +270 -0
  17. package/dist/lib/node-esm/extraction/index.mjs.map +7 -0
  18. package/dist/lib/node-esm/index.mjs +305 -2110
  19. package/dist/lib/node-esm/index.mjs.map +4 -4
  20. package/dist/lib/node-esm/meta.json +1 -1
  21. package/dist/lib/node-esm/named-entity-recognition-W4VMRCH6.mjs +14 -0
  22. package/dist/lib/node-esm/named-entity-recognition-W4VMRCH6.mjs.map +7 -0
  23. package/dist/types/src/conversation/context.d.ts +90 -0
  24. package/dist/types/src/conversation/context.d.ts.map +1 -0
  25. package/dist/types/src/conversation/conversation.d.ts +50 -0
  26. package/dist/types/src/conversation/conversation.d.ts.map +1 -0
  27. package/dist/types/src/conversation/index.d.ts +3 -0
  28. package/dist/types/src/conversation/index.d.ts.map +1 -0
  29. package/dist/types/src/errors.d.ts +39 -0
  30. package/dist/types/src/errors.d.ts.map +1 -0
  31. package/dist/types/src/extraction/extraction-llm-function/extraction-llm-function.d.ts.map +1 -1
  32. package/dist/types/src/extraction/extraction-ner-function/extraction-ner-function.d.ts +151 -53
  33. package/dist/types/src/extraction/extraction-ner-function/extraction-ner-function.d.ts.map +1 -1
  34. package/dist/types/src/extraction/extraction-ner-function/named-entity-recognition.d.ts +1 -1
  35. package/dist/types/src/extraction/extraction-ner-function/named-entity-recognition.d.ts.map +1 -1
  36. package/dist/types/src/extraction/extraction.d.ts +106 -44
  37. package/dist/types/src/extraction/extraction.d.ts.map +1 -1
  38. package/dist/types/src/functions/index.d.ts +2 -0
  39. package/dist/types/src/functions/index.d.ts.map +1 -0
  40. package/dist/types/src/functions/services.d.ts +7 -0
  41. package/dist/types/src/functions/services.d.ts.map +1 -0
  42. package/dist/types/src/index.d.ts +4 -3
  43. package/dist/types/src/index.d.ts.map +1 -1
  44. package/dist/types/src/session/artifact-diff.d.ts +27 -0
  45. package/dist/types/src/session/artifact-diff.d.ts.map +1 -0
  46. package/dist/types/src/session/format.d.ts +168 -0
  47. package/dist/types/src/session/format.d.ts.map +1 -0
  48. package/dist/types/src/session/format.test.d.ts +2 -0
  49. package/dist/types/src/session/format.test.d.ts.map +1 -0
  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/session.d.ts +125 -219
  53. package/dist/types/src/session/session.d.ts.map +1 -1
  54. package/dist/types/src/session/version-pin.d.ts +4 -4
  55. package/dist/types/src/session/version-pin.d.ts.map +1 -1
  56. package/dist/types/src/templates/index.d.ts +2 -0
  57. package/dist/types/src/templates/index.d.ts.map +1 -0
  58. package/dist/types/src/templates/system.d.ts +22 -0
  59. package/dist/types/src/templates/system.d.ts.map +1 -0
  60. package/dist/types/src/types.d.ts +13 -0
  61. package/dist/types/src/types.d.ts.map +1 -1
  62. package/dist/types/tsconfig.tsbuildinfo +1 -1
  63. package/package.json +46 -31
  64. package/src/conversation/context.ts +133 -0
  65. package/src/conversation/conversation.ts +125 -0
  66. package/src/conversation/index.ts +6 -0
  67. package/src/errors.ts +10 -0
  68. package/src/extraction/extraction-llm-function/extraction-llm-function.test.ts +7 -18
  69. package/src/extraction/extraction-llm-function/extraction-llm-function.ts +23 -17
  70. package/src/extraction/extraction-ner-function/extraction-ner-function.test.ts +4 -3
  71. package/src/extraction/extraction-ner-function/extraction-ner-function.ts +7 -4
  72. package/src/extraction/extraction-ner-function/named-entity-recognition.test.ts +2 -2
  73. package/src/extraction/extraction-ner-function/named-entity-recognition.ts +1 -1
  74. package/src/extraction/quotes.test.ts +1 -1
  75. package/src/extraction/quotes.ts +1 -1
  76. package/src/functions/index.ts +5 -0
  77. package/src/functions/services.ts +114 -0
  78. package/src/index.ts +4 -3
  79. package/src/session/__snapshots__/session.test.ts.snap +32 -0
  80. package/src/session/artifact-diff.ts +37 -0
  81. package/src/session/format.test.ts +60 -0
  82. package/src/session/format.ts +139 -0
  83. package/src/session/index.ts +1 -0
  84. package/src/session/session.test.ts +95 -19
  85. package/src/session/session.ts +224 -494
  86. package/src/session/version-pin.test.ts +2 -21
  87. package/src/session/version-pin.ts +9 -9
  88. package/src/summarization/summarization.test.ts +82 -106
  89. package/src/templates/format.tpl +55 -0
  90. package/src/templates/index.ts +5 -0
  91. package/src/templates/system.tpl +47 -0
  92. package/src/templates/system.ts +32 -0
  93. package/src/types.ts +45 -1
  94. package/src/typings.d.ts +5 -0
  95. package/dist/lib/node/index.cjs +0 -2178
  96. package/dist/lib/node/index.cjs.map +0 -7
  97. package/dist/lib/node/meta.json +0 -1
  98. package/dist/types/bin/chat.d.ts +0 -2
  99. package/dist/types/bin/chat.d.ts.map +0 -1
  100. package/dist/types/src/blueprint/ai.test.d.ts +0 -2
  101. package/dist/types/src/blueprint/ai.test.d.ts.map +0 -1
  102. package/dist/types/src/blueprint/blueprint.d.ts +0 -55
  103. package/dist/types/src/blueprint/blueprint.d.ts.map +0 -1
  104. package/dist/types/src/blueprint/blueprint.test.d.ts +0 -2
  105. package/dist/types/src/blueprint/blueprint.test.d.ts.map +0 -1
  106. package/dist/types/src/blueprint/index.d.ts +0 -4
  107. package/dist/types/src/blueprint/index.d.ts.map +0 -1
  108. package/dist/types/src/blueprint/logger.d.ts +0 -25
  109. package/dist/types/src/blueprint/logger.d.ts.map +0 -1
  110. package/dist/types/src/blueprint/machine.d.ts +0 -143
  111. package/dist/types/src/blueprint/machine.d.ts.map +0 -1
  112. package/dist/types/src/blueprint/test-data.d.ts +0 -3
  113. package/dist/types/src/blueprint/test-data.d.ts.map +0 -1
  114. package/dist/types/src/research/exa.d.ts +0 -7
  115. package/dist/types/src/research/exa.d.ts.map +0 -1
  116. package/dist/types/src/research/graph.d.ts +0 -41
  117. package/dist/types/src/research/graph.d.ts.map +0 -1
  118. package/dist/types/src/research/graph.test.d.ts +0 -2
  119. package/dist/types/src/research/graph.test.d.ts.map +0 -1
  120. package/dist/types/src/research/index.d.ts +0 -4
  121. package/dist/types/src/research/index.d.ts.map +0 -1
  122. package/dist/types/src/research/research.d.ts +0 -10
  123. package/dist/types/src/research/research.d.ts.map +0 -1
  124. package/dist/types/src/research/research.test.d.ts +0 -2
  125. package/dist/types/src/research/research.test.d.ts.map +0 -1
  126. package/dist/types/src/testing/exa-search-1748337321991.d.ts +0 -38
  127. package/dist/types/src/testing/exa-search-1748337321991.d.ts.map +0 -1
  128. package/dist/types/src/testing/exa-search-1748337331526.d.ts +0 -37
  129. package/dist/types/src/testing/exa-search-1748337331526.d.ts.map +0 -1
  130. package/dist/types/src/testing/exa-search-1748337344119.d.ts +0 -58
  131. package/dist/types/src/testing/exa-search-1748337344119.d.ts.map +0 -1
  132. package/dist/types/src/testing/index.d.ts +0 -3
  133. package/dist/types/src/testing/index.d.ts.map +0 -1
  134. package/src/blueprint/ai.test.ts +0 -156
  135. package/src/blueprint/blueprint.test.ts +0 -151
  136. package/src/blueprint/blueprint.ts +0 -75
  137. package/src/blueprint/index.ts +0 -7
  138. package/src/blueprint/logger.ts +0 -155
  139. package/src/blueprint/machine.ts +0 -251
  140. package/src/blueprint/test-data.ts +0 -55
  141. package/src/research/exa.ts +0 -100
  142. package/src/research/graph.test.ts +0 -15
  143. package/src/research/graph.ts +0 -337
  144. package/src/research/index.ts +0 -7
  145. package/src/research/instructions-research.tpl +0 -97
  146. package/src/research/research.test.ts +0 -190
  147. package/src/research/research.ts +0 -89
  148. package/src/testing/exa-search-1748337321991.ts +0 -131
  149. package/src/testing/exa-search-1748337331526.ts +0 -144
  150. package/src/testing/exa-search-1748337344119.ts +0 -133
  151. package/src/testing/index.ts +0 -11
  152. package/src/typedefs.d.ts +0 -8
@@ -0,0 +1,114 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { AiTool, type AiToolkit } from '@effect/ai';
6
+ import { Context, Effect, Layer, Match, Predicate, Record, Schema } from 'effect';
7
+
8
+ import { AiToolNotFoundError, ToolExecutionService, ToolResolverService } from '@dxos/ai';
9
+ import { type FunctionDefinition, LocalFunctionExecutionService } from '@dxos/functions';
10
+ import { invariant } from '@dxos/invariant';
11
+
12
+ export const makeToolResolverFromFunctions = (
13
+ functions: FunctionDefinition<any, any>[],
14
+ toolkit: AiToolkit.Any,
15
+ ): Layer.Layer<ToolResolverService> => {
16
+ return Layer.succeed(ToolResolverService, {
17
+ resolve: Effect.fn('resolveTool')(function* (id) {
18
+ const tool = toolkit.tools[id];
19
+ if (tool) {
20
+ return tool;
21
+ }
22
+
23
+ const fn = functions.find((f) => f.name === id);
24
+ if (!fn) {
25
+ return yield* Effect.fail(new AiToolNotFoundError(id));
26
+ }
27
+
28
+ return projectFunctionToTool(fn);
29
+ }),
30
+ });
31
+ };
32
+
33
+ export const makeToolExecutionServiceFromFunctions = (
34
+ functions: FunctionDefinition<any, any>[],
35
+ toolkit: AiToolkit.AiToolkit<AiTool.Any>,
36
+ handlersLayer: Layer.Layer<AiTool.ToHandler<AiTool.AiTool<any>>, never, never>,
37
+ ): Layer.Layer<ToolExecutionService, never, LocalFunctionExecutionService> => {
38
+ return Layer.effect(
39
+ ToolExecutionService,
40
+ Effect.gen(function* () {
41
+ const toolkitHandler = yield* toolkit.pipe(Effect.provide(handlersLayer));
42
+
43
+ const localFunctionExecutionService = yield* LocalFunctionExecutionService;
44
+ return {
45
+ handlersFor: (toolkit) => {
46
+ const makeHandler = (tool: AiTool.Any): ((params: unknown) => Effect.Effect<unknown, any, any>) => {
47
+ return Effect.fn('toolFunctionHandler')(function* (input: any) {
48
+ if (toolkitHandler.tools.find((t: AiTool.Any) => t.name === tool.name)) {
49
+ // TODO(wittjosiah): Everything is `never` here.
50
+ return yield* (toolkitHandler.handle as any)(tool.name, input);
51
+ }
52
+
53
+ const { functionName } = Context.get(FunctionToolAnnotation)(tool.annotations as any);
54
+ const fnDef = functions.find((f) => f.name === functionName);
55
+ if (!fnDef) {
56
+ return yield* Effect.fail(new AiToolNotFoundError(tool.name));
57
+ }
58
+
59
+ return yield* localFunctionExecutionService
60
+ .invokeFunction(fnDef, input)
61
+ .pipe(Effect.catchAllDefect((defect) => Effect.fail(defect)));
62
+ });
63
+ };
64
+
65
+ return toolkit.of(Record.map(toolkit.tools, (tool, _name) => makeHandler(tool)) as any) as any;
66
+ },
67
+ };
68
+ }),
69
+ );
70
+ };
71
+
72
+ /**
73
+ * Tools that are projected from functions have this annotation.
74
+ */
75
+ class FunctionToolAnnotation extends Context.Tag('@dxos/assisatnt/FunctionToolAnnotation')<
76
+ FunctionToolAnnotation,
77
+ {
78
+ functionName: string;
79
+ }
80
+ >() {}
81
+
82
+ const toolCache = new WeakMap<FunctionDefinition<any, any>, AiTool.Any>();
83
+ const projectFunctionToTool = (fn: FunctionDefinition<any, any>): AiTool.Any => {
84
+ if (toolCache.has(fn)) {
85
+ return toolCache.get(fn)!;
86
+ }
87
+ const tool = AiTool.make(makeToolName(fn.name), {
88
+ description: fn.description,
89
+ parameters: createStructFieldsFromSchema(fn.inputSchema),
90
+ // TODO(dmaretskyi): Include output schema.
91
+ failure: Schema.Any, // TODO(dmaretskyi): Better type for the failure?
92
+ }).annotate(FunctionToolAnnotation, { functionName: fn.name });
93
+ toolCache.set(fn, tool);
94
+ return tool;
95
+ };
96
+
97
+ /**
98
+ * @returns Tool name produced from function name by escaping invalid characters.
99
+ */
100
+ const makeToolName = (name: string) => {
101
+ const toolName = name.replace(/[^a-zA-Z0-9]/g, '_');
102
+ invariant(toolName.match(/^[a-zA-Z_][a-zA-Z0-9-_]*$/));
103
+ return toolName;
104
+ };
105
+
106
+ // TODO(dmaretskyi): Factor out.
107
+ const createStructFieldsFromSchema = (schema: Schema.Schema<any, any>): Record<string, Schema.Schema<any, any>> => {
108
+ return Match.value(schema.ast).pipe(
109
+ Match.when(Predicate.isTagged('TypeLiteral'), (ast) => {
110
+ return Object.fromEntries(ast.propertySignatures.map((prop) => [prop.name, Schema.make(prop.type)]));
111
+ }),
112
+ Match.orElseAbsurd,
113
+ );
114
+ };
package/src/index.ts CHANGED
@@ -2,8 +2,9 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- export * from './extraction';
6
- export * from './research';
5
+ export * from './conversation';
6
+ export * from './functions';
7
7
  export * from './session';
8
+ export * from './templates';
8
9
  export * from './types';
9
- export * from './blueprint';
10
+ export * from './errors';
@@ -0,0 +1,32 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`AiSession > no tools 1`] = `
4
+ [
5
+ {
6
+ "blocks": ReactiveArray [
7
+ {
8
+ "_tag": "text",
9
+ "text": "Hello world!",
10
+ },
11
+ ],
12
+ "created": "Sun, 20 Jul 2025 10:54:08 GMT",
13
+ "id": "01K0KQGFTFV3GBKGEWZW67K8HK",
14
+ "sender": {
15
+ "role": "user",
16
+ },
17
+ },
18
+ {
19
+ "blocks": ReactiveArray [
20
+ {
21
+ "_tag": "text",
22
+ "text": "Hi there! How can I help you today?",
23
+ },
24
+ ],
25
+ "created": "2025-07-20T10:54:10.801Z",
26
+ "id": "01K0KQGHVHJ37D4YG45WG090WV",
27
+ "sender": {
28
+ "role": "assistant",
29
+ },
30
+ },
31
+ ]
32
+ `;
@@ -0,0 +1,37 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { Context } from 'effect';
6
+
7
+ import { type ObjectVersion } from '@dxos/echo-db';
8
+ import { type ObjectId } from '@dxos/echo-schema';
9
+
10
+ /**
11
+ * Resolves artifact ids to their versions.
12
+ * Used to give the model a sense of the changes to the artifacts made by users during the conversation.
13
+ * The artifacts versions are pinned in the history, and whenever the artifact changes in-between assistant's steps,
14
+ * a diff is inserted into the conversation.
15
+ *
16
+ * Can be optionally provided to the session run call.
17
+ */
18
+ // TODO(burdon): Move.
19
+ // TODO(dmaretskyi): Convert to Context.Reference
20
+ export class ArtifactDiffResolver extends Context.Tag('@dxos/assistant/ArtifactDiffResolver')<
21
+ ArtifactDiffResolver,
22
+ ArtifactDiffResolver.Service
23
+ >() {}
24
+
25
+ export namespace ArtifactDiffResolver {
26
+ export type Service = {
27
+ resolve: (artifacts: { id: ObjectId; lastVersion: ObjectVersion }[]) => Promise<
28
+ Map<
29
+ ObjectId,
30
+ {
31
+ version: ObjectVersion;
32
+ diff?: string;
33
+ }
34
+ >
35
+ >;
36
+ };
37
+ }
@@ -0,0 +1,60 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { beforeAll, describe, it } from '@effect/vitest';
6
+ import { Effect } from 'effect';
7
+
8
+ import { Blueprint, Template } from '@dxos/blueprints';
9
+ import { Obj } from '@dxos/echo';
10
+ import { type EchoDatabase } from '@dxos/echo-db';
11
+ import { EchoTestBuilder } from '@dxos/echo-db/testing';
12
+ import { DataType } from '@dxos/schema';
13
+ import { trim } from '@dxos/util';
14
+
15
+ import { createSystemPrompt } from '../templates/system';
16
+
17
+ import { formatSystemPrompt } from './format';
18
+
19
+ describe('format', () => {
20
+ let db: EchoDatabase;
21
+ beforeAll(async () => {
22
+ const builder = await new EchoTestBuilder().open();
23
+ ({ db } = await builder.createDatabase({
24
+ types: [DataType.Text, DataType.Organization, Blueprint.Blueprint],
25
+ }));
26
+ });
27
+
28
+ it.effect(
29
+ 'should format',
30
+ Effect.fn(function* () {
31
+ const object = db.add(
32
+ Obj.make(DataType.Organization, {
33
+ name: 'Test',
34
+ website: 'https://www.test.com',
35
+ }),
36
+ );
37
+
38
+ const blueprint = db.add(
39
+ Blueprint.make({
40
+ key: 'example.com/blueprint/test',
41
+ name: 'Test',
42
+ instructions: Template.make({
43
+ source: trim`
44
+ Test
45
+ This is the test blueprint.
46
+ `,
47
+ }),
48
+ }),
49
+ );
50
+
51
+ const output = yield* formatSystemPrompt({
52
+ system: createSystemPrompt({}),
53
+ blueprints: [blueprint, blueprint],
54
+ objects: [object, object],
55
+ });
56
+
57
+ console.log(output);
58
+ }),
59
+ );
60
+ });
@@ -0,0 +1,139 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { Array, Effect, Option, pipe } from 'effect';
6
+
7
+ import { Template } from '@dxos/blueprints';
8
+ import { Obj } from '@dxos/echo';
9
+ import { ObjectVersion } from '@dxos/echo-db';
10
+ import { type ObjectId } from '@dxos/echo-schema';
11
+ import { DatabaseService } from '@dxos/functions';
12
+ import { log } from '@dxos/log';
13
+ import { type ContentBlock, DataType } from '@dxos/schema';
14
+ import { trim } from '@dxos/util';
15
+
16
+ import { AiAssistantError } from '../errors';
17
+
18
+ import { ArtifactDiffResolver } from './artifact-diff';
19
+ import { type SessionRunParams } from './session';
20
+
21
+ /**
22
+ * Formats the system prompt.
23
+ */
24
+ // TODO(burdon): Move to AiPreprocessor.
25
+ export const formatSystemPrompt = ({
26
+ system,
27
+ blueprints = [],
28
+ objects = [],
29
+ }: Pick<SessionRunParams<any>, 'system' | 'blueprints' | 'objects'>) =>
30
+ Effect.gen(function* () {
31
+ // TOOD(burdon): Should process templates.
32
+ const blueprintDefs = yield* pipe(
33
+ blueprints,
34
+ Effect.forEach((blueprint) => Effect.succeed(blueprint.instructions)),
35
+ Effect.flatMap(Effect.forEach((template) => DatabaseService.load(template.source))),
36
+ Effect.map(
37
+ Array.map(
38
+ (template) => trim`
39
+ <blueprint>
40
+ ${Template.process(template.content)}
41
+ </blueprint>
42
+ `,
43
+ ),
44
+ ),
45
+ Effect.map((blueprints) =>
46
+ blueprints.length > 0 ? ['## Blueprints Definitions', ...blueprints].join('\n\n') : undefined,
47
+ ),
48
+ );
49
+
50
+ const objectDefs = yield* pipe(
51
+ objects,
52
+ Effect.forEach((object) =>
53
+ Effect.succeed(trim`
54
+ <object>
55
+ <dxn>${Obj.getDXN(object)}</dxn>
56
+ <typename>${Obj.getTypename(object)}</typename>
57
+ </object>
58
+ `),
59
+ ),
60
+ Effect.map((objects) => (objects.length > 0 ? ['## Context Objects', ...objects].join('\n\n') : undefined)),
61
+ );
62
+
63
+ return yield* pipe(
64
+ Effect.succeed([system, blueprintDefs, objectDefs].filter((def): def is string => def !== undefined)),
65
+ Effect.map((parts) => parts.join('\n\n')),
66
+ );
67
+ });
68
+
69
+ /**
70
+ * Formats the user prompt.
71
+ */
72
+ // TODO(burdon): Move to AiPreprocessor.
73
+ // TODO(burdon): Convert util below to `Effect.fn` (to preserve stack info)
74
+ export const formatUserPrompt = ({ prompt, history = [] }: Pick<SessionRunParams<any>, 'prompt' | 'history'>) =>
75
+ Effect.gen(function* () {
76
+ const prelude: ContentBlock.Any[] = [];
77
+
78
+ // TODO(dmaretskyi): Evaluate other approaches as `serviceOption` isn't represented in the type system.
79
+ const artifactDiffResolver = yield* Effect.serviceOption(ArtifactDiffResolver);
80
+ if (Option.isSome(artifactDiffResolver)) {
81
+ const versions = gatherObjectVersions(history);
82
+ const artifactDiff = yield* Effect.tryPromise({
83
+ try: () =>
84
+ artifactDiffResolver.value.resolve(
85
+ [...versions.entries()].map(([id, version]) => ({ id, lastVersion: version })),
86
+ ),
87
+ catch: AiAssistantError.wrap('Artifact diff resolution error'),
88
+ });
89
+
90
+ log.info('version', { artifactDiff, versions });
91
+ for (const [id, { version }] of [...artifactDiff.entries()]) {
92
+ if (ObjectVersion.equals(version, versions.get(id)!)) {
93
+ artifactDiff.delete(id);
94
+ continue;
95
+ }
96
+
97
+ prelude.push({ _tag: 'anchor', objectId: id, version });
98
+ }
99
+
100
+ if (artifactDiff.size > 0) {
101
+ prelude.push(createArtifactUpdateBlock(artifactDiff));
102
+ }
103
+ }
104
+
105
+ return Obj.make(DataType.Message, {
106
+ created: new Date().toISOString(),
107
+ sender: { role: 'user' },
108
+ blocks: [...prelude, { _tag: 'text', text: prompt }],
109
+ });
110
+ });
111
+
112
+ const gatherObjectVersions = (messages: DataType.Message[]): Map<ObjectId, ObjectVersion> => {
113
+ const artifactIds = new Map<ObjectId, ObjectVersion>();
114
+ for (const message of messages) {
115
+ for (const block of message.blocks) {
116
+ if (block._tag === 'anchor') {
117
+ artifactIds.set(block.objectId, block.version as ObjectVersion);
118
+ }
119
+ }
120
+ }
121
+
122
+ return artifactIds;
123
+ };
124
+
125
+ const createArtifactUpdateBlock = (
126
+ artifactDiff: Map<ObjectId, { version: ObjectVersion; diff?: string }>,
127
+ ): ContentBlock.Any => {
128
+ return {
129
+ _tag: 'text',
130
+ // TODO(dmaretskyi): Does this need to be a special content-block?
131
+ disposition: 'artifact-update',
132
+ text: trim`
133
+ The following artifacts have been updated since the last message:
134
+ ${[...artifactDiff.entries()]
135
+ .map(([id, { diff }]) => `<changed-artifact id="${id}">${diff ? `\n${diff}` : ''}</changed-artifact>`)
136
+ .join('\n')}
137
+ `,
138
+ };
139
+ };
@@ -2,5 +2,6 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
+ export * from './artifact-diff';
5
6
  export * from './session';
6
7
  export * from './version-pin';
@@ -2,18 +2,16 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { Schema } from 'effect';
6
- import { describe, test } from 'vitest';
7
-
8
- import { EdgeAiServiceClient, ConsolePrinter, createTool, ToolResult } from '@dxos/ai';
9
- import { AI_SERVICE_ENDPOINT } from '@dxos/ai/testing';
10
- import { ArtifactId, defineArtifact } from '@dxos/artifact';
11
- import { Type, Obj } from '@dxos/echo';
12
- import { ObjectId } from '@dxos/echo-schema';
13
- import { DXN } from '@dxos/keys';
5
+ import { AiTool, AiToolkit } from '@effect/ai';
6
+ import { describe, it } from '@effect/vitest';
7
+ import { Effect, Layer, Schema } from 'effect';
8
+
9
+ import { AiService, ToolExecutionService, ToolResolverService } from '@dxos/ai';
10
+ import { AiServiceTestingPreset } from '@dxos/ai/testing';
11
+ import { Obj, Type } from '@dxos/echo';
14
12
  import { log } from '@dxos/log';
15
13
 
16
- import { AISession } from './session';
14
+ import { AiSession } from './session';
17
15
 
18
16
  // Define a calendar event artifact schema.
19
17
  const CalendarEventSchema = Schema.Struct({
@@ -30,15 +28,90 @@ const CalendarEventSchema = Schema.Struct({
30
28
 
31
29
  type CalendarEvent = Schema.Schema.Type<typeof CalendarEventSchema>;
32
30
 
33
- // TODO(burdon): Flaky.
34
- describe.skip('AISession with Ollama', () => {
35
- test('create calendar itinerary', { timeout: 60_000 }, async () => {
36
- const aiClient = new EdgeAiServiceClient({ endpoint: AI_SERVICE_ENDPOINT.REMOTE });
37
- // const aiClient = new OllamaAiServiceClient({
38
- // overrides: { model: 'llama3.1:8b' },
39
- // });
40
- const session = new AISession({ operationModel: 'configured' });
31
+ class TestToolkit extends AiToolkit.make(
32
+ AiTool.make('Calculator', {
33
+ description: 'Basic calculator tool',
34
+ parameters: {
35
+ input: Schema.String.annotations({
36
+ description: 'The calculation to perform.',
37
+ }),
38
+ },
39
+ success: Schema.Struct({
40
+ result: Schema.Number,
41
+ }),
42
+ failure: Schema.Never,
43
+ }),
44
+ ) {}
45
+
46
+ // Tool handlers.
47
+ const toolkitLayer = TestToolkit.toLayer({
48
+ Calculator: ({ input }) =>
49
+ Effect.gen(function* () {
50
+ const result = (() => {
51
+ // Restrict to basic arithmetic operations for safety.
52
+ const sanitizedInput = input.replace(/[^0-9+\-*/().\s]/g, '');
53
+ log.info('calculate', { sanitizedInput });
54
+
55
+ // eslint-disable-next-line @typescript-eslint/no-implied-eval
56
+ return Function(`"use strict"; return (${sanitizedInput})`)();
57
+ })();
58
+
59
+ return { result };
60
+ }),
61
+ });
62
+
63
+ describe.runIf(process.env.DX_RUN_SLOW_TESTS)('AiSession', () => {
64
+ it.effect(
65
+ 'no tools',
66
+ Effect.fn(
67
+ function* ({ expect: _ }) {
68
+ const session = new AiSession({ operationModel: 'configured' });
69
+ const response = yield* session.run({
70
+ prompt: 'Hello world!',
71
+ history: [],
72
+ });
73
+ log.info('response', { response });
74
+ },
75
+ Effect.provide(
76
+ AiService.model('@anthropic/claude-3-5-sonnet-20241022').pipe(
77
+ Layer.provideMerge(ToolResolverService.layerEmpty),
78
+ Layer.provideMerge(ToolExecutionService.layerEmpty),
79
+ Layer.provideMerge(AiServiceTestingPreset('direct')),
80
+ ),
81
+ ),
82
+ ),
83
+ );
41
84
 
85
+ it.effect(
86
+ 'calculator',
87
+ Effect.fn(
88
+ function* ({ expect: _ }) {
89
+ const session = new AiSession({ operationModel: 'configured' });
90
+ const response = yield* session.run({
91
+ prompt: 'What is 10 + 20?',
92
+ history: [],
93
+ toolkit: TestToolkit,
94
+ });
95
+ log.info('response', { response });
96
+ },
97
+ Effect.provide(
98
+ Layer.mergeAll(
99
+ toolkitLayer,
100
+ ToolResolverService.layerEmpty,
101
+ ToolExecutionService.layerEmpty,
102
+ AiService.model('@anthropic/claude-3-5-sonnet-20241022').pipe(
103
+ Layer.provideMerge(AiServiceTestingPreset('direct')),
104
+ ),
105
+ ),
106
+ ),
107
+ ),
108
+ );
109
+
110
+ // TODO(dmaretskyi): Revive test.
111
+ /*
112
+ it.skip('create calendar itinerary', { timeout: 60_000 }, async () => {
113
+ // overrides: { model: 'llama3.1:8b' },
114
+ const session = new AiSession({ operationModel: 'configured' });
42
115
  const objects = new Set<string>();
43
116
 
44
117
  // Define calendar artifact.
@@ -145,6 +218,7 @@ describe.skip('AISession with Ollama', () => {
145
218
  model: '@anthropic/claude-3-5-haiku-20241022',
146
219
  },
147
220
  prompt: 'create a table and map for a travel itinerary based on events in my calendar',
221
+ toolResolver: new ToolRegistry([]),
148
222
  });
149
223
 
150
224
  log('result', {
@@ -152,10 +226,12 @@ describe.skip('AISession with Ollama', () => {
152
226
  finalMessage: response.at(-1),
153
227
  });
154
228
  });
229
+ */
155
230
  });
156
231
 
157
232
  // Travel to rome, florence, livorno, siena, madrid for conferences
158
- const CALENDAR_EVENTS: CalendarEvent[] = [
233
+
234
+ const _CALENDAR_EVENTS: CalendarEvent[] = [
159
235
  Obj.make(CalendarEventSchema, {
160
236
  title: 'Exploring Ancient Ruins in Rome',
161
237
  startTime: '2024-01-01T10:00:00Z',