@dxos/assistant 0.8.4-main.fffef41 → 0.9.0

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 (174) hide show
  1. package/LICENSE +102 -5
  2. package/README.md +2 -1
  3. package/dist/lib/{browser → neutral}/extraction/index.mjs +42 -59
  4. package/dist/lib/neutral/extraction/index.mjs.map +7 -0
  5. package/dist/lib/neutral/index.mjs +1886 -0
  6. package/dist/lib/neutral/index.mjs.map +7 -0
  7. package/dist/lib/neutral/meta.json +1 -0
  8. package/dist/types/src/extraction/extraction-ner-function/index.d.ts +0 -1
  9. package/dist/types/src/extraction/extraction-ner-function/index.d.ts.map +1 -1
  10. package/dist/types/src/extraction/extraction-ner-function/named-entity-recognition.d.ts.map +1 -1
  11. package/dist/types/src/extraction/extraction.d.ts +1649 -317
  12. package/dist/types/src/extraction/extraction.d.ts.map +1 -1
  13. package/dist/types/src/extraction/index.d.ts +0 -1
  14. package/dist/types/src/extraction/index.d.ts.map +1 -1
  15. package/dist/types/src/extraction/quotes.d.ts +2 -2
  16. package/dist/types/src/extraction/quotes.d.ts.map +1 -1
  17. package/dist/types/src/functions/services.d.ts +14 -16
  18. package/dist/types/src/functions/services.d.ts.map +1 -1
  19. package/dist/types/src/index.d.ts +2 -5
  20. package/dist/types/src/index.d.ts.map +1 -1
  21. package/dist/types/src/request/AiRequest.d.ts +308 -0
  22. package/dist/types/src/request/AiRequest.d.ts.map +1 -0
  23. package/dist/types/src/{session → request}/artifact-diff.d.ts +4 -4
  24. package/dist/types/src/request/artifact-diff.d.ts.map +1 -0
  25. package/dist/types/src/request/format.d.ts +15 -0
  26. package/dist/types/src/request/format.d.ts.map +1 -0
  27. package/dist/types/src/request/index.d.ts +4 -0
  28. package/dist/types/src/request/index.d.ts.map +1 -0
  29. package/dist/types/src/{session → request}/observer.d.ts +4 -3
  30. package/dist/types/src/request/observer.d.ts.map +1 -0
  31. package/dist/types/src/{session → request}/version-pin.d.ts +4 -4
  32. package/dist/types/src/request/version-pin.d.ts.map +1 -0
  33. package/dist/types/src/session/AiContext.d.ts +178 -0
  34. package/dist/types/src/session/AiContext.d.ts.map +1 -0
  35. package/dist/types/src/session/AiContext.test.d.ts +2 -0
  36. package/dist/types/src/session/AiContext.test.d.ts.map +1 -0
  37. package/dist/types/src/session/AiSession.d.ts +79 -0
  38. package/dist/types/src/session/AiSession.d.ts.map +1 -0
  39. package/dist/types/src/session/AiSession.test.d.ts +2 -0
  40. package/dist/types/src/session/AiSession.test.d.ts.map +1 -0
  41. package/dist/types/src/session/SessionLink.d.ts +17 -0
  42. package/dist/types/src/session/SessionLink.d.ts.map +1 -0
  43. package/dist/types/src/session/SessionLoader.d.ts +15 -0
  44. package/dist/types/src/session/SessionLoader.d.ts.map +1 -0
  45. package/dist/types/src/session/SessionLoader.test.d.ts +2 -0
  46. package/dist/types/src/session/SessionLoader.test.d.ts.map +1 -0
  47. package/dist/types/src/session/index.d.ts +4 -5
  48. package/dist/types/src/session/index.d.ts.map +1 -1
  49. package/dist/types/src/session/toolkit.d.ts +10 -9
  50. package/dist/types/src/session/toolkit.d.ts.map +1 -1
  51. package/dist/types/src/templates/system.d.ts +12 -3
  52. package/dist/types/src/templates/system.d.ts.map +1 -1
  53. package/dist/types/src/util/artifact.d.ts +18 -0
  54. package/dist/types/src/util/artifact.d.ts.map +1 -0
  55. package/dist/types/src/{diff.d.ts → util/diff.d.ts} +4 -2
  56. package/dist/types/src/util/diff.d.ts.map +1 -0
  57. package/dist/types/src/{diff.test.d.ts.map → util/diff.test.d.ts.map} +1 -1
  58. package/dist/types/src/util/error-handling.d.ts.map +1 -0
  59. package/dist/types/src/{errors.d.ts → util/errors.d.ts} +24 -28
  60. package/dist/types/src/util/errors.d.ts.map +1 -0
  61. package/dist/types/src/{execution-graph.d.ts → util/execution-graph.d.ts} +2 -2
  62. package/dist/types/src/util/execution-graph.d.ts.map +1 -0
  63. package/dist/types/src/util/execution-graph.test.d.ts.map +1 -0
  64. package/dist/types/src/util/index.d.ts +7 -0
  65. package/dist/types/src/util/index.d.ts.map +1 -0
  66. package/dist/types/src/util/tracing.d.ts +296 -0
  67. package/dist/types/src/util/tracing.d.ts.map +1 -0
  68. package/dist/types/tsconfig.tsbuildinfo +1 -1
  69. package/package.json +36 -44
  70. package/src/extraction/extraction-ner-function/index.ts +0 -1
  71. package/src/extraction/extraction.ts +47 -18
  72. package/src/extraction/index.ts +0 -1
  73. package/src/extraction/quotes.test.ts +2 -2
  74. package/src/extraction/quotes.ts +6 -8
  75. package/src/functions/services.ts +129 -63
  76. package/src/index.ts +2 -5
  77. package/src/request/AiRequest.ts +387 -0
  78. package/src/{session → request}/artifact-diff.ts +4 -4
  79. package/src/{session → request}/format.ts +28 -25
  80. package/src/request/index.ts +8 -0
  81. package/src/{session → request}/observer.ts +4 -1
  82. package/src/{session → request}/version-pin.ts +6 -5
  83. package/src/session/AiContext.test.ts +49 -0
  84. package/src/session/AiContext.ts +420 -0
  85. package/src/session/AiSession.test.ts +111 -0
  86. package/src/session/AiSession.ts +328 -0
  87. package/src/session/SessionLink.ts +23 -0
  88. package/src/session/SessionLoader.test.ts +115 -0
  89. package/src/session/SessionLoader.ts +55 -0
  90. package/src/session/index.ts +4 -5
  91. package/src/session/toolkit.ts +27 -28
  92. package/src/templates/instructions/format.tpl +0 -4
  93. package/src/templates/instructions/system.tpl +50 -2
  94. package/src/templates/system.ts +7 -6
  95. package/src/util/artifact.ts +96 -0
  96. package/src/util/diff.test.ts +74 -0
  97. package/src/{diff.ts → util/diff.ts} +14 -5
  98. package/src/{session → util}/error-handling.ts +3 -2
  99. package/src/util/errors.ts +15 -0
  100. package/src/{execution-graph.test.ts → util/execution-graph.test.ts} +4 -4
  101. package/src/{execution-graph.ts → util/execution-graph.ts} +30 -19
  102. package/src/util/index.ts +10 -0
  103. package/src/util/tracing.ts +60 -0
  104. package/dist/lib/browser/extraction/index.mjs.map +0 -7
  105. package/dist/lib/browser/index.mjs +0 -1290
  106. package/dist/lib/browser/index.mjs.map +0 -7
  107. package/dist/lib/browser/meta.json +0 -1
  108. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs +0 -11
  109. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs.map +0 -7
  110. package/dist/lib/node-esm/extraction/index.mjs +0 -226
  111. package/dist/lib/node-esm/extraction/index.mjs.map +0 -7
  112. package/dist/lib/node-esm/index.mjs +0 -1291
  113. package/dist/lib/node-esm/index.mjs.map +0 -7
  114. package/dist/lib/node-esm/meta.json +0 -1
  115. package/dist/types/src/conversation/context.d.ts +0 -103
  116. package/dist/types/src/conversation/context.d.ts.map +0 -1
  117. package/dist/types/src/conversation/conversation.d.ts +0 -41
  118. package/dist/types/src/conversation/conversation.d.ts.map +0 -1
  119. package/dist/types/src/conversation/index.d.ts +0 -3
  120. package/dist/types/src/conversation/index.d.ts.map +0 -1
  121. package/dist/types/src/diff.d.ts.map +0 -1
  122. package/dist/types/src/errors.d.ts.map +0 -1
  123. package/dist/types/src/execution-graph.d.ts.map +0 -1
  124. package/dist/types/src/execution-graph.test.d.ts.map +0 -1
  125. package/dist/types/src/extraction/extraction-llm-function/extraction-llm-function.d.ts +0 -4
  126. package/dist/types/src/extraction/extraction-llm-function/extraction-llm-function.d.ts.map +0 -1
  127. package/dist/types/src/extraction/extraction-llm-function/extraction-llm-function.test.d.ts +0 -2
  128. package/dist/types/src/extraction/extraction-llm-function/extraction-llm-function.test.d.ts.map +0 -1
  129. package/dist/types/src/extraction/extraction-llm-function/index.d.ts +0 -2
  130. package/dist/types/src/extraction/extraction-llm-function/index.d.ts.map +0 -1
  131. package/dist/types/src/extraction/extraction-ner-function/extraction-ner-function.d.ts +0 -345
  132. package/dist/types/src/extraction/extraction-ner-function/extraction-ner-function.d.ts.map +0 -1
  133. package/dist/types/src/extraction/extraction-ner-function/extraction-ner-function.test.d.ts +0 -2
  134. package/dist/types/src/extraction/extraction-ner-function/extraction-ner-function.test.d.ts.map +0 -1
  135. package/dist/types/src/session/GenericToolkit.d.ts +0 -84
  136. package/dist/types/src/session/GenericToolkit.d.ts.map +0 -1
  137. package/dist/types/src/session/artifact-diff.d.ts.map +0 -1
  138. package/dist/types/src/session/error-handling.d.ts.map +0 -1
  139. package/dist/types/src/session/format.d.ts +0 -12
  140. package/dist/types/src/session/format.d.ts.map +0 -1
  141. package/dist/types/src/session/format.test.d.ts +0 -2
  142. package/dist/types/src/session/format.test.d.ts.map +0 -1
  143. package/dist/types/src/session/observer.d.ts.map +0 -1
  144. package/dist/types/src/session/session.d.ts +0 -51
  145. package/dist/types/src/session/session.d.ts.map +0 -1
  146. package/dist/types/src/session/session.test.d.ts +0 -2
  147. package/dist/types/src/session/session.test.d.ts.map +0 -1
  148. package/dist/types/src/session/version-pin.d.ts.map +0 -1
  149. package/dist/types/src/types/artifact.d.ts +0 -20
  150. package/dist/types/src/types/artifact.d.ts.map +0 -1
  151. package/dist/types/src/types/index.d.ts +0 -2
  152. package/dist/types/src/types/index.d.ts.map +0 -1
  153. package/src/conversation/context.ts +0 -186
  154. package/src/conversation/conversation.ts +0 -128
  155. package/src/conversation/index.ts +0 -6
  156. package/src/diff.test.ts +0 -43
  157. package/src/errors.ts +0 -15
  158. package/src/extraction/extraction-llm-function/extraction-llm-function.test.ts +0 -84
  159. package/src/extraction/extraction-llm-function/extraction-llm-function.ts +0 -62
  160. package/src/extraction/extraction-llm-function/index.ts +0 -5
  161. package/src/extraction/extraction-ner-function/extraction-ner-function.test.ts +0 -116
  162. package/src/extraction/extraction-ner-function/extraction-ner-function.ts +0 -56
  163. package/src/session/GenericToolkit.ts +0 -130
  164. package/src/session/format.test.ts +0 -61
  165. package/src/session/session.conversations.json +0 -1
  166. package/src/session/session.test.ts +0 -149
  167. package/src/session/session.ts +0 -213
  168. package/src/types/artifact.ts +0 -71
  169. package/src/types/index.ts +0 -5
  170. /package/dist/lib/{browser → neutral}/chunk-J5LGTIGS.mjs +0 -0
  171. /package/dist/lib/{browser → neutral}/chunk-J5LGTIGS.mjs.map +0 -0
  172. /package/dist/types/src/{diff.test.d.ts → util/diff.test.d.ts} +0 -0
  173. /package/dist/types/src/{session → util}/error-handling.d.ts +0 -0
  174. /package/dist/types/src/{execution-graph.test.d.ts → util/execution-graph.test.d.ts} +0 -0
@@ -0,0 +1,328 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ // @import-as-namespace
6
+
7
+ import { type Registry as AtomRegistry } 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';
14
+ import * as Record from 'effect/Record';
15
+ import * as Runtime from 'effect/Runtime';
16
+
17
+ import { type OpaqueToolkit, type ToolExecutionService, type ToolResolverService } from '@dxos/ai';
18
+ import { type Blueprint, McpServer, Operation, Trace } from '@dxos/compute';
19
+ import { Resource } from '@dxos/context';
20
+ import { Database, Feed, Filter, Obj, Registry } from '@dxos/echo';
21
+ import { EffectEx } from '@dxos/effect';
22
+ import { invariant } from '@dxos/invariant';
23
+ import { log } from '@dxos/log';
24
+ import { McpToolkit } from '@dxos/mcp-client';
25
+ import { Message, type ContentBlock } from '@dxos/types';
26
+
27
+ import { ToolExecutionServices } from '../functions';
28
+ import { AiRequest, type GenerationObserver, formatSystemPrompt } from '../request';
29
+ import { McpServerError } from '../util';
30
+ import * as AiContext from './AiContext';
31
+ import { SessionLoader } from './SessionLoader';
32
+ import { createToolkit } from './toolkit';
33
+
34
+ export type RunProps<R = never> = {
35
+ prompt: string | ContentBlock.Any[];
36
+ system?: string;
37
+ observer?: GenerationObserver;
38
+ toolkit?: OpaqueToolkit.OpaqueToolkit<R>;
39
+
40
+ /**
41
+ * Space-level MCP servers to connect alongside blueprint-defined ones.
42
+ */
43
+ mcpServers?: readonly McpServer.McpServer[];
44
+ };
45
+
46
+ export type Options = {
47
+ feed: Feed.Feed;
48
+ runtime: Runtime.Runtime<Feed.FeedService>;
49
+ /** @effect-atom/atom-react Registry for reactive state. */
50
+ registry?: AtomRegistry.Registry;
51
+ };
52
+
53
+ /**
54
+ * Summarization threshold in tokens.
55
+ */
56
+ // TODO(dmaretskyi): Take from model characteristics. opus has 200k max tokens.
57
+ const SUMMARY_THRESHOLD = 80_000;
58
+
59
+ /**
60
+ * Durable conversation state (initiated by users and agents) backed by a Feed.
61
+ * Executes tools based on AI responses and supports cancellation of in-progress requests.
62
+ */
63
+ export class Session extends Resource {
64
+ /**
65
+ * Blueprints and objects bound to the session.
66
+ */
67
+ private readonly _binder: AiContext.Binder;
68
+ private readonly _feed: Feed.Feed;
69
+ private readonly _runtime: Runtime.Runtime<Feed.FeedService>;
70
+ private readonly _sessionLoader = new SessionLoader();
71
+
72
+ public constructor(options: Options) {
73
+ super();
74
+ this._feed = options.feed;
75
+ this._runtime = options.runtime;
76
+ invariant(this._feed);
77
+ invariant(this._runtime);
78
+ this._binder = new AiContext.Binder({
79
+ feed: this._feed,
80
+ runtime: this._runtime,
81
+ registry: options.registry,
82
+ });
83
+ }
84
+
85
+ protected override async _open(): Promise<void> {
86
+ await this._binder.open(this._ctx);
87
+ }
88
+
89
+ public get feed() {
90
+ return this._feed;
91
+ }
92
+
93
+ public get context() {
94
+ return this._binder;
95
+ }
96
+
97
+ public async getHistory(): Promise<Message.Message[]> {
98
+ const queryResult = await Runtime.runPromise(this._runtime)(Feed.query(this._feed, Filter.type(Message.Message)));
99
+ const items = await queryResult.run();
100
+ const messages = items.filter(Obj.instanceOf(Message.Message));
101
+ return Runtime.runPromise(this._runtime)(this._sessionLoader.reifyHistory(this._feed, messages));
102
+ }
103
+
104
+ getTools(): Effect.Effect<
105
+ Record<string, import('@effect/ai/Tool').Any>,
106
+ never,
107
+ ToolExecutionService | ToolResolverService
108
+ > {
109
+ return Effect.gen(this, function* () {
110
+ const toolkit = yield* createToolkit({ blueprints: this.context.getBlueprints() });
111
+ return toolkit.toolkit.tools;
112
+ }).pipe(Effect.orDie);
113
+ }
114
+
115
+ /**
116
+ * Not provided by default, since users might want to override them.
117
+ */
118
+ makeToolExecutionServices(): Layer.Layer<
119
+ ToolExecutionService | ToolResolverService,
120
+ never,
121
+ OpaqueToolkit.OpaqueToolkitProvider | Operation.Service | Registry.Service
122
+ > {
123
+ return ToolExecutionServices.pipe(
124
+ Layer.provide(Operation.withInvocationOptions({ conversation: Obj.getURI(this._feed) })),
125
+ );
126
+ }
127
+
128
+ /**
129
+ * Creates a new cancelable request effect.
130
+ */
131
+ public createRequest<R = never>(
132
+ params: RunProps<R>,
133
+ ): Effect.Effect<Message.Message[], AiRequest.RunError, AiRequest.RunRequirements | R> {
134
+ return Effect.gen(this, function* () {
135
+ const history = yield* Effect.promise(() => this.getHistory());
136
+ const blueprints = this.context.getBlueprints();
137
+ const objects = this.context.getObjects();
138
+
139
+ log('run', {
140
+ history: history.length,
141
+ blueprints: blueprints.length,
142
+ objects: objects.length,
143
+ });
144
+
145
+ const request = new AiRequest.Request({
146
+ summarizationThreshold: SUMMARY_THRESHOLD,
147
+ observer: params.observer,
148
+ onOutput: (message) =>
149
+ Effect.promise(() => Runtime.runPromise(this._runtime)(Feed.append(this._feed, [message]))),
150
+ });
151
+
152
+ yield* request.begin({
153
+ history,
154
+ blueprints,
155
+ objects,
156
+ prompt: params.prompt,
157
+ system: params.system,
158
+ });
159
+
160
+ // Turn loop: recompute toolkit and system prompt between turns to pick up dynamically enabled blueprints.
161
+ do {
162
+ yield* Effect.promise(() => this.context.sync());
163
+ const currentBlueprints = this.context.getBlueprints();
164
+ const mcps = yield* connectMcpServers(currentBlueprints, params.mcpServers);
165
+ const toolkit = yield* createToolkit({
166
+ toolkit: params.toolkit,
167
+ blueprints: currentBlueprints,
168
+ opaqueToolkits: mcps,
169
+ });
170
+
171
+ log('toolkit', { tools: Record.keys(toolkit.toolkit.tools) });
172
+ const system = yield* formatSystemPrompt({
173
+ system: params.system,
174
+ blueprints: currentBlueprints,
175
+ objects: this.context.getObjects(),
176
+ }).pipe(Effect.orDie);
177
+
178
+ const { done } = yield* request.runAgentTurn({ system, toolkit });
179
+ if (done) {
180
+ break;
181
+ }
182
+
183
+ yield* request.runTools({ toolkit });
184
+ } while (true);
185
+
186
+ log('result', {
187
+ messages: request.pending.length,
188
+ duration: request.duration,
189
+ toolCalls: request.toolCalls,
190
+ });
191
+
192
+ return [...request.pending];
193
+ }).pipe(
194
+ Effect.provide(
195
+ Layer.mergeAll(
196
+ Layer.succeed(AiContext.Service, {
197
+ binder: this.context,
198
+ }),
199
+ Layer.succeed(Service, this),
200
+ Operation.withInvocationOptions({ conversation: Obj.getURI(this._feed) }),
201
+ ),
202
+ ),
203
+ Effect.withSpan('AiSession.createRequest'),
204
+ );
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Gives access to the ai session.
210
+ */
211
+ export class Service extends Context.Tag('@dxos/assistant/AiSessionService')<Service, Session>() {
212
+ /**
213
+ * Create a new session layer from options.
214
+ */
215
+ static layer = (options: Options): Layer.Layer<Service | AiContext.Service> =>
216
+ aiContextFromSession.pipe(
217
+ Layer.provideMerge(
218
+ Layer.scoped(
219
+ Service,
220
+ Effect.gen(function* () {
221
+ const session = yield* EffectEx.acquireReleaseResource(() => new Session(options));
222
+ return session;
223
+ }),
224
+ ),
225
+ ),
226
+ );
227
+
228
+ /**
229
+ * Create a new session with a new feed.
230
+ */
231
+ static layerNewFeed = (
232
+ options?: Omit<Options, 'feed' | 'runtime'>,
233
+ ): Layer.Layer<Service | AiContext.Service, never, Database.Service | Feed.FeedService> =>
234
+ Layer.unwrapScoped(
235
+ Effect.gen(function* () {
236
+ const feed = yield* Database.add(Feed.make());
237
+ const runtime = yield* Effect.runtime<Feed.FeedService>();
238
+ return Service.layer({ ...options, feed, runtime });
239
+ }),
240
+ );
241
+
242
+ /**
243
+ * Run a prompt in the current session.
244
+ */
245
+ static run = <R = never>(
246
+ params: RunProps<R>,
247
+ ): Effect.Effect<Message.Message[], AiRequest.RunError, AiRequest.RunRequirements | Service | R> =>
248
+ Effect.gen(function* () {
249
+ const session = yield* Service;
250
+ return yield* session.createRequest(params);
251
+ });
252
+ }
253
+
254
+ const aiContextFromSession = Layer.effect(
255
+ AiContext.Service,
256
+ Effect.gen(function* () {
257
+ const session = yield* Service;
258
+ return {
259
+ binder: session.context,
260
+ };
261
+ }),
262
+ );
263
+
264
+ const connectMcpServers = (
265
+ blueprints: readonly Blueprint.Blueprint[],
266
+ spaceMcpServers: readonly McpServer.McpServer[] = [],
267
+ ): Effect.Effect<OpaqueToolkit.OpaqueToolkit[], never, Trace.TraceService> => {
268
+ const blueprintServers: McpToolkit.McpToolkitOptions[] = pipe(
269
+ blueprints,
270
+ Array.flatMap((_) => _.mcpServers ?? []),
271
+ Array.map(({ url, protocol, apiKey }) => ({ url, protocol, apiKey })),
272
+ );
273
+ const spaceServers: McpToolkit.McpToolkitOptions[] = spaceMcpServers.map(({ url, protocol, apiKey }) => ({
274
+ url,
275
+ protocol,
276
+ apiKey,
277
+ }));
278
+ const allServers = [...blueprintServers, ...spaceServers];
279
+
280
+ return pipe(
281
+ allServers,
282
+ Effect.forEach((options) =>
283
+ McpToolkit.make(options).pipe(
284
+ // NOTE: Type-inference fails here without explicit void return.
285
+ Effect.tap((toolkit): void =>
286
+ log.info('Connected to MCP server', { url: options.url, tools: Object.keys(toolkit.toolkit.tools).length }),
287
+ ),
288
+ // Surface typed connection failures via ephemeral trace + warn log, then drop the server.
289
+ Effect.tapError((error) =>
290
+ Effect.gen(function* () {
291
+ log.warn('Failed to connect to MCP server', {
292
+ url: error.url,
293
+ protocol: error.protocol,
294
+ message: error.message,
295
+ });
296
+ yield* Trace.write(McpServerError, {
297
+ url: error.url,
298
+ protocol: error.protocol,
299
+ message: error.message,
300
+ });
301
+ }),
302
+ ),
303
+ // Catch unexpected defects too (e.g. malformed tool schemas) so a single broken
304
+ // server can never abort the whole turn — surface them through the same channel.
305
+ Effect.catchAllDefect((defect) =>
306
+ Effect.gen(function* () {
307
+ const message = defect instanceof Error ? defect.message : String(defect);
308
+ log.warn('Unexpected MCP defect', { url: options.url, message });
309
+ yield* Trace.write(McpServerError, {
310
+ url: options.url,
311
+ protocol: options.protocol,
312
+ message: `Unexpected MCP failure: ${message}`,
313
+ });
314
+ return yield* Effect.fail(
315
+ new McpToolkit.McpConnectionError({
316
+ url: options.url,
317
+ protocol: options.protocol,
318
+ message,
319
+ }),
320
+ );
321
+ }),
322
+ ),
323
+ Effect.either,
324
+ ),
325
+ ),
326
+ Effect.map(Array.filterMap((_) => Either.getRight(_))),
327
+ );
328
+ };
@@ -0,0 +1,23 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ // @import-as-namespace
6
+
7
+ import * as Schema from 'effect/Schema';
8
+
9
+ import { DXN, Feed, Obj, Ref, Type } from '@dxos/echo';
10
+
11
+ /**
12
+ * Feed-level record that links this session to a point in another session's history.
13
+ * When present in a feed, the messages from the referenced feed up to and including
14
+ * `messageId` are prepended to the conversation history before each turn.
15
+ */
16
+ export const SessionLink = Schema.Struct({
17
+ /** Reference to the feed of the source session. */
18
+ feedRef: Ref.Ref(Feed.Feed),
19
+ /** Messages from the source feed up to and including this message ID are injected. */
20
+ messageId: Obj.ID,
21
+ }).pipe(Type.makeObject(DXN.make('org.dxos.type.sessionLink', '0.1.0')));
22
+
23
+ export type SessionLink = Type.InstanceType<typeof SessionLink>;
@@ -0,0 +1,115 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { describe, expect, it } from '@effect/vitest';
6
+ import * as Effect from 'effect/Effect';
7
+
8
+ import { Database, Feed, Obj, Ref } from '@dxos/echo';
9
+ import { TestDatabaseLayer } from '@dxos/echo-client/testing';
10
+ import { Message } from '@dxos/types';
11
+
12
+ import * as SessionLink from './SessionLink';
13
+ import { SessionLoader } from './SessionLoader';
14
+
15
+ // Monotonic timestamps so chronological sorting in SessionLoader is deterministic.
16
+ let clock = 0;
17
+ const makeMessage = (text: string, sender: 'user' | 'assistant' = 'user') =>
18
+ Message.make({ created: new Date(clock++).toISOString(), sender, blocks: [{ _tag: 'text', text }] });
19
+
20
+ describe('SessionLoader', () => {
21
+ const TestLayer = TestDatabaseLayer({ types: [Feed.Feed, Message.Message, SessionLink.SessionLink] });
22
+
23
+ it.effect('returns original messages when no SessionLink exists in feed', () =>
24
+ Effect.gen(function* () {
25
+ const { db } = yield* Database.Service;
26
+ const feed = db.add(Feed.make());
27
+ const message = makeMessage('hello');
28
+ yield* Feed.append(feed, [message]);
29
+
30
+ const result = yield* new SessionLoader().reifyHistory(feed, [message]);
31
+
32
+ expect(result.map((msg) => msg.id)).toEqual([message.id]);
33
+ }).pipe(Effect.provide(TestLayer)),
34
+ );
35
+
36
+ it.effect('prepends source history when SessionLink resolves to a valid message', () =>
37
+ Effect.gen(function* () {
38
+ const { db } = yield* Database.Service;
39
+ const sourceFeed = db.add(Feed.make());
40
+ const forkFeed = db.add(Feed.make());
41
+
42
+ const msg1 = makeMessage('first');
43
+ const msg2 = makeMessage('second', 'assistant');
44
+ const forkMsg = makeMessage('new turn');
45
+ yield* Feed.append(sourceFeed, [msg1, msg2]);
46
+
47
+ yield* Feed.append(forkFeed, [
48
+ Obj.make(SessionLink.SessionLink, { feedRef: Ref.make(sourceFeed), messageId: msg2.id }),
49
+ ]);
50
+
51
+ const result = yield* new SessionLoader().reifyHistory(forkFeed, [forkMsg]);
52
+
53
+ expect(result.map((msg) => msg.id)).toEqual([msg1.id, msg2.id, forkMsg.id]);
54
+ }).pipe(Effect.provide(TestLayer)),
55
+ );
56
+
57
+ it.effect('respects the fork cutoff — excludes messages after messageId', () =>
58
+ Effect.gen(function* () {
59
+ const { db } = yield* Database.Service;
60
+ const sourceFeed = db.add(Feed.make());
61
+ const forkFeed = db.add(Feed.make());
62
+
63
+ const msg1 = makeMessage('a');
64
+ const msg2 = makeMessage('b', 'assistant');
65
+ const msg3 = makeMessage('c — after fork');
66
+ yield* Feed.append(sourceFeed, [msg1, msg2, msg3]);
67
+
68
+ yield* Feed.append(forkFeed, [
69
+ // Fork at msg2; msg3 should be excluded.
70
+ Obj.make(SessionLink.SessionLink, { feedRef: Ref.make(sourceFeed), messageId: msg2.id }),
71
+ ]);
72
+
73
+ const result = yield* new SessionLoader().reifyHistory(forkFeed, []);
74
+
75
+ expect(result.map((msg) => msg.id)).toEqual([msg1.id, msg2.id]);
76
+ }).pipe(Effect.provide(TestLayer)),
77
+ );
78
+
79
+ it.effect('returns original messages when messageId is not found in source feed (fail-closed)', () =>
80
+ Effect.gen(function* () {
81
+ const { db } = yield* Database.Service;
82
+ const sourceFeed = db.add(Feed.make());
83
+ const forkFeed = db.add(Feed.make());
84
+
85
+ const msg1 = makeMessage('existing');
86
+ const currentMsg = makeMessage('new turn');
87
+ yield* Feed.append(sourceFeed, [msg1]);
88
+
89
+ yield* Feed.append(forkFeed, [
90
+ Obj.make(SessionLink.SessionLink, { feedRef: Ref.make(sourceFeed), messageId: Obj.ID.random() }),
91
+ ]);
92
+
93
+ const result = yield* new SessionLoader().reifyHistory(forkFeed, [currentMsg]);
94
+
95
+ expect(result.map((msg) => msg.id)).toEqual([currentMsg.id]);
96
+ }).pipe(Effect.provide(TestLayer)),
97
+ );
98
+
99
+ it.effect('returns original messages when source feed reference is unresolvable', () =>
100
+ Effect.gen(function* () {
101
+ const { db } = yield* Database.Service;
102
+ const forkFeed = db.add(Feed.make());
103
+ const unresolvedFeed = Feed.make(); // Intentionally not added to the database.
104
+
105
+ yield* Feed.append(forkFeed, [
106
+ Obj.make(SessionLink.SessionLink, { feedRef: Ref.make(unresolvedFeed), messageId: Obj.ID.random() }),
107
+ ]);
108
+
109
+ const currentMsg = makeMessage('hello');
110
+ const result = yield* new SessionLoader().reifyHistory(forkFeed, [currentMsg]);
111
+
112
+ expect(result.map((msg) => msg.id)).toEqual([currentMsg.id]);
113
+ }).pipe(Effect.provide(TestLayer)),
114
+ );
115
+ });
@@ -0,0 +1,55 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import * as Effect from 'effect/Effect';
6
+
7
+ import { Feed, Filter, Obj } from '@dxos/echo';
8
+ import { Message } from '@dxos/types';
9
+
10
+ import * as SessionLink from './SessionLink';
11
+
12
+ /**
13
+ * Loads and reifies conversation history by resolving SessionLink feed records.
14
+ */
15
+ export class SessionLoader {
16
+ /**
17
+ * Prepends linked history to `messages` when the feed contains a SessionLink.
18
+ * Queries the feed for any SessionLink record; if found, loads messages from
19
+ * the referenced feed up to and including `messageId` and prepends them.
20
+ */
21
+ reifyHistory(
22
+ feed: Feed.Feed,
23
+ messages: Message.Message[],
24
+ ): Effect.Effect<Message.Message[], never, Feed.FeedService> {
25
+ return Effect.gen(function* () {
26
+ const links = yield* Feed.runQuery(feed, Filter.type(SessionLink.SessionLink));
27
+ const sessionLinks = links.filter(Obj.instanceOf(SessionLink.SessionLink));
28
+
29
+ const link = sessionLinks[0];
30
+ if (!link) {
31
+ return messages;
32
+ }
33
+
34
+ const sourceFeed = link.feedRef.target;
35
+ if (!sourceFeed) {
36
+ return messages;
37
+ }
38
+
39
+ const sourceMessages = yield* Feed.runQuery(sourceFeed, Filter.type(Message.Message));
40
+ const filtered = sourceMessages.filter(Obj.instanceOf(Message.Message));
41
+
42
+ // Sort by creation timestamp so history is in chronological order.
43
+ const sorted = [...filtered].sort((a, b) => a.created.localeCompare(b.created));
44
+
45
+ // Include messages up to and including the fork point.
46
+ const cutoffIndex = sorted.findIndex((m) => m.id === link.messageId);
47
+ if (cutoffIndex < 0) {
48
+ // Fork point not found; return original messages unmodified to avoid injecting unexpected history.
49
+ return messages;
50
+ }
51
+
52
+ return [...sorted.slice(0, cutoffIndex + 1), ...messages];
53
+ });
54
+ }
55
+ }
@@ -2,10 +2,9 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- export * from './artifact-diff';
6
- export * from './observer';
7
- export * from './session';
8
5
  export * from './toolkit';
9
- export * from './version-pin';
10
6
 
11
- export * as GenericToolkit from './GenericToolkit';
7
+ export * as AiContext from './AiContext';
8
+ export * as AiSession from './AiSession';
9
+ export { SessionLoader } from './SessionLoader';
10
+ export * as SessionLink from './SessionLink';
@@ -2,47 +2,46 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import type * as Tool from '@effect/ai/Tool';
6
5
  import * as Toolkit from '@effect/ai/Toolkit';
7
- import type * as Context from 'effect/Context';
8
6
  import * as Effect from 'effect/Effect';
7
+ import * as Layer from 'effect/Layer';
9
8
 
10
- import { type AiToolNotFoundError, ToolExecutionService, type ToolId, ToolResolverService } from '@dxos/ai';
11
- import { type Blueprint } from '@dxos/blueprints';
9
+ import { type AiToolNotFoundError, OpaqueToolkit, ToolExecutionService, ToolResolverService } from '@dxos/ai';
10
+ import { type Blueprint } from '@dxos/compute';
12
11
  import { isTruthy } from '@dxos/util';
13
12
 
14
- export type ToolkitParams<Tools extends Record<string, Tool.Any>> = {
15
- toolkit?: Toolkit.Toolkit<Tools>;
16
- toolIds?: ToolId[];
17
- blueprints?: Blueprint.Blueprint[];
13
+ export type CreateToolkitProps = {
14
+ toolkit?: OpaqueToolkit.Any;
15
+ blueprints?: readonly Blueprint.Blueprint[];
16
+ /**
17
+ * Self-contained with handlers toolkits.
18
+ */
19
+ opaqueToolkits?: readonly OpaqueToolkit.Any[];
18
20
  };
19
21
 
20
22
  /**
21
23
  * Build a combined toolkit from the blueprint tools and the provided toolkit.
22
24
  */
23
- export const createToolkit = <Tools extends Record<string, Tool.Any> = {}>({
24
- toolkit: toolkitParam,
25
- toolIds = [],
25
+ export const createToolkit = ({
26
+ toolkit: toolkitProp,
26
27
  blueprints = [],
27
- }: ToolkitParams<Tools>): Effect.Effect<
28
- Toolkit.WithHandler<any>,
28
+ opaqueToolkits = [],
29
+ }: CreateToolkitProps): Effect.Effect<
30
+ OpaqueToolkit.OpaqueToolkit,
29
31
  AiToolNotFoundError,
30
- ToolResolverService | ToolExecutionService | Tool.HandlersFor<Tools>
32
+ ToolResolverService | ToolExecutionService
31
33
  > =>
32
34
  Effect.gen(function* () {
33
- const blueprintToolkit = yield* ToolResolverService.resolveToolkit([
34
- ...blueprints.flatMap(({ tools }) => tools),
35
- ...toolIds,
36
- ]);
35
+ const blueprintToolkit = yield* ToolResolverService.resolveToolkit(blueprints.flatMap(({ tools }) => tools));
36
+ const blueprintToolHandler = yield* blueprintToolkit.toContext(ToolExecutionService.handlersFor(blueprintToolkit));
37
+ const opaqueToolkit = OpaqueToolkit.merge(...opaqueToolkits);
37
38
 
38
- const blueprintToolkitHandler: Context.Context<Tool.HandlersFor<any>> = yield* blueprintToolkit.toContext(
39
- ToolExecutionService.handlersFor(blueprintToolkit),
39
+ const toolkitDefs = [toolkitProp?.toolkit, blueprintToolkit, opaqueToolkit.toolkit].filter(isTruthy);
40
+ const mergedToolkit = Toolkit.merge(...toolkitDefs);
41
+ const combinedHandlerLayer = Layer.mergeAll(
42
+ Layer.succeedContext(blueprintToolHandler),
43
+ toolkitProp?.layer ?? OpaqueToolkit.empty.layer,
44
+ opaqueToolkit.layer,
40
45
  );
41
-
42
- const toolkit = Toolkit.merge(...[toolkitParam, blueprintToolkit].filter(isTruthy));
43
- return yield* toolkit.pipe(Effect.provide(blueprintToolkitHandler)) as any as Effect.Effect<
44
- Toolkit.WithHandler<any>,
45
- never,
46
- Tool.HandlersFor<Tools>
47
- >;
48
- });
46
+ return OpaqueToolkit.make(mergedToolkit, combinedHandlerLayer as any) as OpaqueToolkit.OpaqueToolkit;
47
+ }) as Effect.Effect<OpaqueToolkit.OpaqueToolkit, AiToolNotFoundError, ToolResolverService | ToolExecutionService>;
@@ -52,10 +52,6 @@ Do not use bullets when listing suggestions.
52
52
  You may ask multiple choice questions by wrapping consise options inside <option> tags inside a well formed <select> tag.
53
53
  Example: <select><option>Yes</option><option>No</option></select>
54
54
 
55
- ## Toolkit
56
-
57
- If the user asks for a list of tools, then just emit a single self-closing <toolkit/> tag instead of listing the tools.
58
-
59
55
  ## Context
60
56
 
61
57
  The current date and time is {{DATETIME}}.
@@ -43,6 +43,54 @@
43
43
  - Suggested actions could include actions that create artifacts.
44
44
  - After creating an artifact use a tool to add it to the chat context.
45
45
 
46
- ## Context
46
+ ## Navigation
47
47
 
48
- Today is {{DATETIME}}.
48
+ - When the user asks to open, go to, navigate to, or show a specific document, page, or object, use the navigation tools instead of loading content inline.
49
+ - First, call the Resolve navigation targets tool to find the navigation path for the requested item.
50
+ - Without arguments, it returns available pages that can be navigated to.
51
+ - With a DXN in the query, it resolves that specific object to a navigation path.
52
+ - If you know the object's DXN (e.g. from a database query or context objects), pass it in the query to get the exact navigation path.
53
+ - Then, call the Open tool with the resolved navigation path(s) in the subject array to navigate the user to that item.
54
+ - Do not respond with the document's content when the user asks to open it — open it in the main content area instead.
55
+ - If multiple targets match, present the options to the user and let them choose which to open.
56
+
57
+ ## Response format
58
+
59
+ - NEVER output narration text outside `<status>` tags or the final user-facing answer. Any progress update, transition sentence, or "let me…" lead-in MUST be wrapped in `<status>...</status>`. Violating this is an error.
60
+ - When updating the user about the progress of the work you are doing, put the update in a <status> XML tag.
61
+ - Only when you need to show the result to the user or ask a question use a text block without status tags.
62
+ - After a `</status>` tag, the next output must be either another `<status>` tag, a tool call, or the final user-facing answer — never a transition sentence.
63
+
64
+ <example>
65
+ // reasoning...
66
+ <status>Looking for emails from Depot</status>
67
+ // tool call..
68
+ <status>Found 2 emails from Depot.</status>
69
+ <status>Loading email details</status>
70
+ // tool call
71
+ <status>Located mailbox</status>
72
+ I found **2 emails from Depot**:
73
+ </example>
74
+
75
+ Do not do (no status tags):
76
+
77
+ <incorrect_example>
78
+ // reasoning...
79
+ // tool call..
80
+ I found 2 emails from Depot. Let me load their details:
81
+ // tool call
82
+ I found **2 emails from Depot**:
83
+ </incorrect_example>
84
+
85
+ Do not do (transition sentence after a status tag, before tool calls):
86
+
87
+ <incorrect_example>
88
+ <status>Doing X</status>
89
+ Let me start by doing X:
90
+ // tool call
91
+ </incorrect_example>
92
+
93
+ - NEVER write a lead-in sentence before a tool call. Use a `<status>` tag instead.
94
+ - Avoid using plain text to tell the user what you are about to do; instead, use status blocks.
95
+ - WRONG: Let me get...
96
+ - CORRECT: <status>Getting...</status>