@dxos/assistant 0.8.4-main.b97322e → 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 (211) hide show
  1. package/README.md +1 -0
  2. package/dist/lib/neutral/chunk-J5LGTIGS.mjs +10 -0
  3. package/dist/lib/neutral/chunk-J5LGTIGS.mjs.map +7 -0
  4. package/dist/lib/neutral/chunk-KVYJFFLF.mjs +1805 -0
  5. package/dist/lib/neutral/chunk-KVYJFFLF.mjs.map +7 -0
  6. package/dist/lib/neutral/extraction/index.mjs +219 -0
  7. package/dist/lib/neutral/extraction/index.mjs.map +7 -0
  8. package/dist/lib/neutral/index.mjs +551 -0
  9. package/dist/lib/neutral/index.mjs.map +7 -0
  10. package/dist/lib/neutral/meta.json +1 -0
  11. package/dist/lib/neutral/testing/index.mjs +112 -0
  12. package/dist/lib/neutral/testing/index.mjs.map +7 -0
  13. package/dist/types/src/conversation/context.d.ts +148 -0
  14. package/dist/types/src/conversation/context.d.ts.map +1 -0
  15. package/dist/types/src/conversation/context.test.d.ts +2 -0
  16. package/dist/types/src/conversation/context.test.d.ts.map +1 -0
  17. package/dist/types/src/conversation/conversation.d.ts +53 -25
  18. package/dist/types/src/conversation/conversation.d.ts.map +1 -1
  19. package/dist/types/src/conversation/conversation.test.d.ts +2 -0
  20. package/dist/types/src/conversation/conversation.test.d.ts.map +1 -0
  21. package/dist/types/src/conversation/index.d.ts +1 -0
  22. package/dist/types/src/conversation/index.d.ts.map +1 -1
  23. package/dist/types/src/diff.d.ts +17 -0
  24. package/dist/types/src/diff.d.ts.map +1 -0
  25. package/dist/types/src/diff.test.d.ts +2 -0
  26. package/dist/types/src/diff.test.d.ts.map +1 -0
  27. package/dist/types/src/errors.d.ts +47 -18
  28. package/dist/types/src/errors.d.ts.map +1 -1
  29. package/dist/types/src/execution-graph.d.ts +85 -0
  30. package/dist/types/src/execution-graph.d.ts.map +1 -0
  31. package/dist/types/src/execution-graph.test.d.ts +2 -0
  32. package/dist/types/src/execution-graph.test.d.ts.map +1 -0
  33. package/dist/types/src/extraction/extraction-ner-function/index.d.ts +0 -1
  34. package/dist/types/src/extraction/extraction-ner-function/index.d.ts.map +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 +1060 -273
  37. package/dist/types/src/extraction/extraction.d.ts.map +1 -1
  38. package/dist/types/src/extraction/index.d.ts +0 -1
  39. package/dist/types/src/extraction/index.d.ts.map +1 -1
  40. package/dist/types/src/extraction/quotes.d.ts +3 -3
  41. package/dist/types/src/extraction/quotes.d.ts.map +1 -1
  42. package/dist/types/src/functions/services.d.ts +37 -4
  43. package/dist/types/src/functions/services.d.ts.map +1 -1
  44. package/dist/types/src/functions.test.d.ts +2 -0
  45. package/dist/types/src/functions.test.d.ts.map +1 -0
  46. package/dist/types/src/index.d.ts +5 -3
  47. package/dist/types/src/index.d.ts.map +1 -1
  48. package/dist/types/src/service/AgentService.d.ts +62 -0
  49. package/dist/types/src/service/AgentService.d.ts.map +1 -0
  50. package/dist/types/src/service/AgentService.test.d.ts +2 -0
  51. package/dist/types/src/service/AgentService.test.d.ts.map +1 -0
  52. package/dist/types/src/service/agent-process.d.ts +17 -0
  53. package/dist/types/src/service/agent-process.d.ts.map +1 -0
  54. package/dist/types/src/service/index.d.ts +3 -0
  55. package/dist/types/src/service/index.d.ts.map +1 -0
  56. package/dist/types/src/session/artifact-diff.d.ts +27 -0
  57. package/dist/types/src/session/artifact-diff.d.ts.map +1 -0
  58. package/dist/types/src/session/error-handling.d.ts +4 -0
  59. package/dist/types/src/session/error-handling.d.ts.map +1 -0
  60. package/dist/types/src/session/format.d.ts +12 -0
  61. package/dist/types/src/session/format.d.ts.map +1 -0
  62. package/dist/types/src/session/format.test.d.ts +2 -0
  63. package/dist/types/src/session/format.test.d.ts.map +1 -0
  64. package/dist/types/src/session/index.d.ts +4 -0
  65. package/dist/types/src/session/index.d.ts.map +1 -1
  66. package/dist/types/src/session/observer.d.ts +23 -0
  67. package/dist/types/src/session/observer.d.ts.map +1 -0
  68. package/dist/types/src/session/session.d.ts +87 -235
  69. package/dist/types/src/session/session.d.ts.map +1 -1
  70. package/dist/types/src/session/toolkit.d.ts +18 -0
  71. package/dist/types/src/session/toolkit.d.ts.map +1 -0
  72. package/dist/types/src/session/version-pin.d.ts +4 -4
  73. package/dist/types/src/session/version-pin.d.ts.map +1 -1
  74. package/dist/types/src/templates/index.d.ts +1 -1
  75. package/dist/types/src/templates/system.d.ts +17 -0
  76. package/dist/types/src/templates/system.d.ts.map +1 -0
  77. package/dist/types/src/testing/index.d.ts +1 -2
  78. package/dist/types/src/testing/index.d.ts.map +1 -1
  79. package/dist/types/src/testing/layer.d.ts +35 -0
  80. package/dist/types/src/testing/layer.d.ts.map +1 -0
  81. package/dist/types/src/tracing.d.ts +264 -0
  82. package/dist/types/src/tracing.d.ts.map +1 -0
  83. package/dist/types/src/types/artifact.d.ts +28 -0
  84. package/dist/types/src/types/artifact.d.ts.map +1 -0
  85. package/dist/types/src/types/index.d.ts +2 -0
  86. package/dist/types/src/types/index.d.ts.map +1 -0
  87. package/dist/types/tsconfig.tsbuildinfo +1 -1
  88. package/package.json +58 -35
  89. package/src/conversation/context.test.ts +46 -0
  90. package/src/conversation/context.ts +369 -0
  91. package/src/conversation/conversation.test.ts +65 -0
  92. package/src/conversation/conversation.ts +232 -62
  93. package/src/conversation/index.ts +1 -0
  94. package/src/diff.test.ts +74 -0
  95. package/src/diff.ts +93 -0
  96. package/src/errors.ts +6 -1
  97. package/src/execution-graph.test.ts +740 -0
  98. package/src/execution-graph.ts +509 -0
  99. package/src/extraction/extraction-ner-function/index.ts +0 -1
  100. package/src/extraction/extraction-ner-function/named-entity-recognition.test.ts +12 -6
  101. package/src/extraction/extraction-ner-function/named-entity-recognition.ts +2 -0
  102. package/src/extraction/extraction.ts +44 -15
  103. package/src/extraction/index.ts +0 -1
  104. package/src/extraction/quotes.test.ts +3 -3
  105. package/src/extraction/quotes.ts +6 -7
  106. package/src/functions/services.ts +286 -96
  107. package/src/functions.conversations.json +1 -0
  108. package/src/functions.test.ts +82 -0
  109. package/src/index.ts +5 -3
  110. package/src/service/AgentService.conversations.json +1 -0
  111. package/src/service/AgentService.test.ts +169 -0
  112. package/src/service/AgentService.ts +171 -0
  113. package/src/service/agent-process.ts +360 -0
  114. package/src/service/index.ts +6 -0
  115. package/src/session/artifact-diff.ts +41 -0
  116. package/src/session/error-handling.ts +41 -0
  117. package/src/session/format.test.ts +133 -0
  118. package/src/session/format.ts +143 -0
  119. package/src/session/index.ts +4 -0
  120. package/src/session/observer.ts +62 -0
  121. package/src/session/session.conversations.json +1 -0
  122. package/src/session/session.test.ts +82 -201
  123. package/src/session/session.ts +282 -270
  124. package/src/session/toolkit.ts +44 -0
  125. package/src/session/version-pin.ts +8 -8
  126. package/src/templates/index.ts +1 -1
  127. package/src/templates/instructions/format.tpl +57 -0
  128. package/src/templates/instructions/system.tpl +57 -0
  129. package/src/templates/system.ts +42 -0
  130. package/src/testing/index.ts +2 -8
  131. package/src/testing/layer.ts +263 -0
  132. package/src/tracing.ts +112 -0
  133. package/src/types/artifact.ts +89 -0
  134. package/src/types/index.ts +5 -0
  135. package/src/typings.d.ts +5 -0
  136. package/dist/lib/browser/index.mjs +0 -1831
  137. package/dist/lib/browser/index.mjs.map +0 -7
  138. package/dist/lib/browser/meta.json +0 -1
  139. package/dist/lib/node-esm/index.mjs +0 -1833
  140. package/dist/lib/node-esm/index.mjs.map +0 -7
  141. package/dist/lib/node-esm/meta.json +0 -1
  142. package/dist/types/bin/chat.d.ts +0 -2
  143. package/dist/types/bin/chat.d.ts.map +0 -1
  144. package/dist/types/src/context/binder.d.ts +0 -34
  145. package/dist/types/src/context/binder.d.ts.map +0 -1
  146. package/dist/types/src/context/binding.d.ts +0 -56
  147. package/dist/types/src/context/binding.d.ts.map +0 -1
  148. package/dist/types/src/context/index.d.ts +0 -3
  149. package/dist/types/src/context/index.d.ts.map +0 -1
  150. package/dist/types/src/extraction/extraction-llm-function/extraction-llm-function.d.ts +0 -4
  151. package/dist/types/src/extraction/extraction-llm-function/extraction-llm-function.d.ts.map +0 -1
  152. package/dist/types/src/extraction/extraction-llm-function/extraction-llm-function.test.d.ts +0 -2
  153. package/dist/types/src/extraction/extraction-llm-function/extraction-llm-function.test.d.ts.map +0 -1
  154. package/dist/types/src/extraction/extraction-llm-function/index.d.ts +0 -2
  155. package/dist/types/src/extraction/extraction-llm-function/index.d.ts.map +0 -1
  156. package/dist/types/src/extraction/extraction-ner-function/extraction-ner-function.d.ts +0 -302
  157. package/dist/types/src/extraction/extraction-ner-function/extraction-ner-function.d.ts.map +0 -1
  158. package/dist/types/src/extraction/extraction-ner-function/extraction-ner-function.test.d.ts +0 -2
  159. package/dist/types/src/extraction/extraction-ner-function/extraction-ner-function.test.d.ts.map +0 -1
  160. package/dist/types/src/research/exa.d.ts +0 -12
  161. package/dist/types/src/research/exa.d.ts.map +0 -1
  162. package/dist/types/src/research/graph.d.ts +0 -51
  163. package/dist/types/src/research/graph.d.ts.map +0 -1
  164. package/dist/types/src/research/graph.test.d.ts +0 -2
  165. package/dist/types/src/research/graph.test.d.ts.map +0 -1
  166. package/dist/types/src/research/index.d.ts +0 -4
  167. package/dist/types/src/research/index.d.ts.map +0 -1
  168. package/dist/types/src/research/research.d.ts +0 -10
  169. package/dist/types/src/research/research.d.ts.map +0 -1
  170. package/dist/types/src/research/research.test.d.ts +0 -2
  171. package/dist/types/src/research/research.test.d.ts.map +0 -1
  172. package/dist/types/src/session/version-pin.test.d.ts +0 -2
  173. package/dist/types/src/session/version-pin.test.d.ts.map +0 -1
  174. package/dist/types/src/summarization/summarization.test.d.ts +0 -2
  175. package/dist/types/src/summarization/summarization.test.d.ts.map +0 -1
  176. package/dist/types/src/templates/prompt.d.ts +0 -30
  177. package/dist/types/src/templates/prompt.d.ts.map +0 -1
  178. package/dist/types/src/testing/exa-search-1748337321991.d.ts +0 -38
  179. package/dist/types/src/testing/exa-search-1748337321991.d.ts.map +0 -1
  180. package/dist/types/src/testing/exa-search-1748337331526.d.ts +0 -37
  181. package/dist/types/src/testing/exa-search-1748337331526.d.ts.map +0 -1
  182. package/dist/types/src/testing/exa-search-1748337344119.d.ts +0 -58
  183. package/dist/types/src/testing/exa-search-1748337344119.d.ts.map +0 -1
  184. package/dist/types/src/types.d.ts +0 -13
  185. package/dist/types/src/types.d.ts.map +0 -1
  186. package/src/context/binder.ts +0 -103
  187. package/src/context/binding.ts +0 -32
  188. package/src/context/index.ts +0 -6
  189. package/src/extraction/extraction-llm-function/extraction-llm-function.test.ts +0 -83
  190. package/src/extraction/extraction-llm-function/extraction-llm-function.ts +0 -70
  191. package/src/extraction/extraction-llm-function/index.ts +0 -5
  192. package/src/extraction/extraction-ner-function/extraction-ner-function.test.ts +0 -115
  193. package/src/extraction/extraction-ner-function/extraction-ner-function.ts +0 -57
  194. package/src/research/exa.ts +0 -97
  195. package/src/research/graph.test.ts +0 -64
  196. package/src/research/graph.ts +0 -369
  197. package/src/research/index.ts +0 -7
  198. package/src/research/instructions-research.tpl +0 -98
  199. package/src/research/research.test.ts +0 -190
  200. package/src/research/research.ts +0 -87
  201. package/src/session/__snapshots__/session.test.ts.snap +0 -32
  202. package/src/session/version-pin.test.ts +0 -7
  203. package/src/summarization/summarization.test.ts +0 -208
  204. package/src/templates/assistant.tpl +0 -111
  205. package/src/templates/prompt.ts +0 -45
  206. package/src/testing/exa-search-1748337321991.ts +0 -131
  207. package/src/testing/exa-search-1748337331526.ts +0 -144
  208. package/src/testing/exa-search-1748337344119.ts +0 -133
  209. package/src/typedefs.d.ts +0 -8
  210. package/src/types.ts +0 -47
  211. /package/src/extraction/extraction-llm-function/{instructions.tpl → transcription-instructions.tpl} +0 -0
@@ -2,95 +2,265 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { type AiError, type AiLanguageModel, type AiTool, type AiToolkit } from '@effect/ai';
6
- import { Effect } from 'effect';
5
+ import type * as Tool from '@effect/ai/Tool';
6
+ import type * as Toolkit from '@effect/ai/Toolkit';
7
+ import type { Registry } from '@effect-atom/atom-react';
8
+ import * as Array from 'effect/Array';
9
+ import * as Context from 'effect/Context';
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';
7
14
 
8
- import {
9
- type ToolExecutionService,
10
- type ToolResolverService,
11
- type AiInputPreprocessingError,
12
- type AiToolNotFoundError,
13
- } from '@dxos/ai';
14
- import { Event } from '@dxos/async';
15
+ import { type ToolExecutionService, type ToolResolverService } from '@dxos/ai';
16
+ import { type GenericToolkit } from '@dxos/ai';
17
+ import { type Blueprint } from '@dxos/blueprints';
18
+ import { Resource } from '@dxos/context';
15
19
  import { Obj } from '@dxos/echo';
16
20
  import { type Queue } from '@dxos/echo-db';
17
- import { DatabaseService } from '@dxos/functions';
18
- import { DataType } from '@dxos/schema';
21
+ import { acquireReleaseResource } from '@dxos/effect';
22
+ import { QueueService } from '@dxos/functions';
23
+ import { invariant } from '@dxos/invariant';
24
+ import { log } from '@dxos/log';
25
+ import { McpToolkit } from '@dxos/mcp-client';
26
+ import { Message } from '@dxos/types';
27
+ import * as Record from 'effect/Record';
19
28
 
20
- import { ContextBinder, type ContextBinding } from '../context';
21
- import type { AiAssistantError } from '../errors';
22
- import { AiSession } from '../session';
29
+ import {
30
+ AiSession,
31
+ type AiSessionRunError,
32
+ type AiSessionRunRequirements,
33
+ type GenerationObserver,
34
+ createToolkit,
35
+ formatSystemPrompt,
36
+ } from '../session';
23
37
 
24
- export interface ConversationRunOptions<Tools extends AiTool.Any> {
25
- systemPrompt?: string;
26
- prompt: string;
38
+ import { AiContextBinder, AiContextService, type ContextBinding } from './context';
27
39
 
28
- toolkit?: AiToolkit.AiToolkit<Tools>;
40
+ export interface AiConversationRunProps<Tools extends Record<string, Tool.Any>> {
41
+ prompt: string;
42
+ system?: string;
43
+ observer?: GenerationObserver;
44
+ toolkit?: Toolkit.Toolkit<Tools>;
29
45
  }
30
46
 
31
- export type ConversationOptions = {
32
- queue: Queue<DataType.Message | ContextBinding>;
47
+ export type AiConversationOptions = {
48
+ queue: Queue<Message.Message | ContextBinding>;
49
+ registry?: Registry.Registry;
33
50
  };
34
51
 
52
+ // TODO(dmaretskyi): Take from model characteristics. opus has 200k max tokens.
35
53
  /**
36
- * Persistent conversation state.
37
- * Context + history + artifacts.
38
- * Backed by a Queue.
54
+ * Summarization threshold in tokens.
39
55
  */
40
- export class Conversation {
41
- private readonly _queue: Queue<DataType.Message | ContextBinding>;
42
-
43
- /**
44
- * Fired when the execution loop begins.
45
- * This is called before the first message is sent.
46
- */
47
- public readonly onBegin = new Event<AiSession>();
56
+ const SUMMARY_THRESHOLD = 80_000;
48
57
 
58
+ /**
59
+ * Durable conversation state (initiated by users and agents) backed by a Queue.
60
+ * Executes tools based on AI responses and supports cancellation of in-progress requests.
61
+ */
62
+ export class AiConversation extends Resource {
49
63
  /**
50
- * Blueprints bound to the conversation.
64
+ * Blueprints and objects bound to the conversation.
51
65
  */
52
- public readonly context: ContextBinder;
66
+ private readonly _binder: AiContextBinder;
67
+ private readonly _queue: Queue<Message.Message | ContextBinding>;
68
+ private readonly _toolkit?: Toolkit.Any;
53
69
 
54
- constructor(options: ConversationOptions) {
70
+ public constructor(options: AiConversationOptions) {
71
+ super();
55
72
  this._queue = options.queue;
56
- this.context = new ContextBinder(this._queue);
73
+ invariant(this._queue);
74
+ this._binder = new AiContextBinder({ queue: this._queue, registry: options.registry });
57
75
  }
58
76
 
59
- async getHistory(): Promise<DataType.Message[]> {
60
- const queueItems = await this._queue.queryObjects();
61
- return queueItems.filter(Obj.instanceOf(DataType.Message));
77
+ protected override async _open(): Promise<void> {
78
+ await this._binder.open(this._ctx);
62
79
  }
63
80
 
64
- run = <Tools extends AiTool.Any>(
65
- options: ConversationRunOptions<Tools>,
66
- ): Effect.Effect<
67
- DataType.Message[],
68
- AiAssistantError | AiInputPreprocessingError | AiError.AiError | AiToolNotFoundError,
69
- AiLanguageModel.AiLanguageModel | ToolResolverService | ToolExecutionService | AiTool.ToHandler<Tools>
70
- > =>
71
- Effect.gen(this, function* () {
72
- const session = new AiSession();
73
- this.onBegin.emit(session);
81
+ public get queue() {
82
+ return this._queue;
83
+ }
74
84
 
85
+ public get context() {
86
+ return this._binder;
87
+ }
88
+
89
+ public get toolkit() {
90
+ return this._toolkit;
91
+ }
92
+
93
+ public async getHistory(): Promise<Message.Message[]> {
94
+ const queueItems = await this._queue.queryObjects(); // TODO(burdon): Update.
95
+ return queueItems.filter(Obj.instanceOf(Message.Message));
96
+ }
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
+
106
+ /**
107
+ * Creates a new cancelable request effect.
108
+ */
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* () {
75
113
  const history = yield* Effect.promise(() => this.getHistory());
76
- const context = yield* Effect.promise(() => this.context.query());
77
- const blueprints = yield* Effect.forEach(context.blueprints.values(), DatabaseService.loadRef);
114
+ const blueprints = this.context.getBlueprints();
115
+ const objects = this.context.getObjects();
78
116
 
79
- const contextObjects = yield* Effect.forEach(context.objects.values(), DatabaseService.loadRef);
117
+ log('run', {
118
+ history: history.length,
119
+ blueprints: blueprints.length,
120
+ objects: objects.length,
121
+ });
80
122
 
81
- const systemPrompt =
82
- (options.systemPrompt ?? '') +
83
- '\n\n' +
84
- contextObjects.map((obj) => `<object>${Obj.getDXN(obj)}</object>`).join('\n');
123
+ const session = new AiSession({
124
+ summarizationThreshold: SUMMARY_THRESHOLD,
125
+ observer: params.observer,
126
+ onOutput: (message) => Effect.promise(() => this._queue.append([message])),
127
+ });
85
128
 
86
- const messages = yield* session.run({
87
- prompt: options.prompt,
129
+ yield* session.begin({
88
130
  history,
89
- systemPrompt,
90
- toolkit: options.toolkit,
91
131
  blueprints,
132
+ objects,
133
+ prompt: params.prompt,
134
+ system: params.system,
92
135
  });
93
- yield* Effect.promise(() => this._queue.append(messages));
94
- return messages;
95
- }).pipe(Effect.withSpan('Conversation.run'));
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);
164
+
165
+ log('result', {
166
+ messages: session.pending.length,
167
+ duration: session.duration,
168
+ toolCalls: session.toolCalls,
169
+ });
170
+
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
+ );
182
+ }
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
+ });
96
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
+ );
@@ -2,4 +2,5 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
+ export * from './context';
5
6
  export * from './conversation';
@@ -0,0 +1,74 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
6
+
7
+ import { createDocAccessor } from '@dxos/echo-db';
8
+ import { EchoTestBuilder } from '@dxos/echo-db/testing';
9
+ import { Text } from '@dxos/schema';
10
+ import { trim } from '@dxos/util';
11
+
12
+ import { applyDiffs } from './diff';
13
+
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
+
24
+ it('should apply diffs', async () => {
25
+ const { db } = await builder.createDatabase({ types: [Text.Text] });
26
+
27
+ const document = trim`
28
+ # Hello World
29
+ There'z a typo in this sentence.
30
+ And another oon here.
31
+ But not this one.
32
+ `;
33
+
34
+ const text = db.add(Text.make({ content: document }));
35
+ const accessor = createDocAccessor(text, ['content']);
36
+ const result = applyDiffs(accessor, [
37
+ `- There'z a typo in this sentence.`,
38
+ `+ There is a typo in this sentence.`,
39
+ `- And another oon here.`,
40
+ `+ And another one here.`,
41
+ ]);
42
+
43
+ expect(result).toBe(trim`
44
+ # Hello World
45
+ There is a typo in this sentence.
46
+ And another one here.
47
+ But not this one.
48
+ `);
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
+ });
74
+ });
package/src/diff.ts ADDED
@@ -0,0 +1,93 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { next as A, type Doc } from '@automerge/automerge';
6
+
7
+ import { DocAccessor, toCursorRange } from '@dxos/echo-db';
8
+ import { log } from '@dxos/log';
9
+ import { isNonNullable } from '@dxos/util';
10
+
11
+ // Relevant info:
12
+ // https://github.com/Minahil-official/GPT-4.1-Promting-Guide?tab=readme-ov-file#-apply-patch-tool-format-v4a-diff
13
+
14
+ // TODO(burdon): Anthropic only.
15
+ // https://docs.anthropic.com/en/docs/agents-and-tools/tool-use/text-editor-tool#example-str-replace-command
16
+ // AI_TOOL_NOT_FOUND: str_replace_based_edit_tool
17
+ // 'str_replace_based_edit_tool',
18
+
19
+ // TOOD(burdon): Support append to end (no "-").
20
+
21
+ export type Diff = {
22
+ match: string;
23
+ replace: string;
24
+ };
25
+
26
+ /**
27
+ * Convert a list of diffs into a list of operations.
28
+ */
29
+ export const reduceDiffs = (diffs: readonly string[]): Diff[] => {
30
+ return diffs.reduce<Diff[]>((acc, diff) => {
31
+ const current = acc[acc.length - 1];
32
+ const [op, ...rest] = diff.split(' ');
33
+ switch (op) {
34
+ case '-': {
35
+ if (current && !current.match) {
36
+ current.match += rest.join(' ');
37
+ } else {
38
+ acc.push({ match: rest.join(' '), replace: '' });
39
+ }
40
+ return acc;
41
+ }
42
+ case '+': {
43
+ if (current && !current.replace) {
44
+ current.replace += rest.join(' ');
45
+ } else {
46
+ acc.push({ match: '', replace: rest.join(' ') });
47
+ }
48
+ return acc;
49
+ }
50
+ default: {
51
+ throw new Error(`Invalid diff format: each line must start with "- " or "+ ", got: ${JSON.stringify(diff)}`);
52
+ }
53
+ }
54
+ }, []);
55
+ };
56
+
57
+ export const computeDiffsWithCursors = <T>(accessor: DocAccessor<T>, diffs: readonly string[]) => {
58
+ return reduceDiffs(diffs)
59
+ .map((diff) => {
60
+ const text = DocAccessor.getValue<string>(accessor);
61
+ const idx = text.indexOf(diff.match);
62
+ if (idx !== -1) {
63
+ return { cursor: toCursorRange(accessor, idx, idx + diff.match.length), text: diff.replace };
64
+ }
65
+ })
66
+ .filter(isNonNullable);
67
+ };
68
+
69
+ export const applyDiffs = <T>(
70
+ accessor: DocAccessor<T>,
71
+ diffs: readonly string[],
72
+ { errorOnNotFound = false }: { errorOnNotFound?: boolean } = {},
73
+ ): string => {
74
+ for (const diff of reduceDiffs(diffs)) {
75
+ accessor.handle.change((doc: Doc<T>) => {
76
+ const text = DocAccessor.getValue<string>(accessor);
77
+ const idx = text.indexOf(diff.match);
78
+ if (idx !== -1) {
79
+ // TODO(burdon): Replace smallest substring.
80
+ // NOTE: This only replaces the first match.
81
+ A.splice(doc, accessor.path as A.Prop[], idx, diff.match.length, diff.replace);
82
+ } else {
83
+ if (errorOnNotFound) {
84
+ throw new Error(`Diff not found: ${diff.match}`);
85
+ } else {
86
+ log.warn('diff not found', diff);
87
+ }
88
+ }
89
+ });
90
+ }
91
+
92
+ return DocAccessor.getValue<string>(accessor);
93
+ };
package/src/errors.ts CHANGED
@@ -4,7 +4,12 @@
4
4
 
5
5
  import { BaseError } from '@dxos/errors';
6
6
 
7
+ /**
8
+ * Generic error from AI model.
9
+ */
10
+ export class AiModelError extends BaseError.extend('AiModelError', 'AI model error') {}
11
+
7
12
  /**
8
13
  * Generic error for AI agent execution.
9
14
  */
10
- export class AiAssistantError extends BaseError.extend('AI_ASSISTANT_ERROR') {}
15
+ export class AiAssistantError extends BaseError.extend('AiAssistantError', 'AI assistant error') {}