@dxos/assistant 0.8.4-main.fd6878d → 0.8.4-staging.60fe92afc8
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.
- package/LICENSE +102 -5
- package/README.md +2 -1
- package/dist/lib/neutral/chunk-J5LGTIGS.mjs +10 -0
- package/dist/lib/neutral/extraction/index.mjs +208 -0
- package/dist/lib/neutral/extraction/index.mjs.map +7 -0
- package/dist/lib/neutral/index.mjs +1886 -0
- package/dist/lib/neutral/index.mjs.map +7 -0
- package/dist/lib/neutral/meta.json +1 -0
- package/dist/types/src/extraction/extraction-ner-function/index.d.ts +0 -1
- package/dist/types/src/extraction/extraction-ner-function/index.d.ts.map +1 -1
- package/dist/types/src/extraction/extraction-ner-function/named-entity-recognition.d.ts.map +1 -1
- package/dist/types/src/extraction/extraction.d.ts +1650 -281
- package/dist/types/src/extraction/extraction.d.ts.map +1 -1
- package/dist/types/src/extraction/index.d.ts +0 -1
- package/dist/types/src/extraction/index.d.ts.map +1 -1
- package/dist/types/src/extraction/quotes.d.ts +3 -3
- package/dist/types/src/extraction/quotes.d.ts.map +1 -1
- package/dist/types/src/functions/services.d.ts +16 -5
- package/dist/types/src/functions/services.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +2 -3
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/request/AiRequest.d.ts +308 -0
- package/dist/types/src/request/AiRequest.d.ts.map +1 -0
- package/dist/types/src/{session → request}/artifact-diff.d.ts +5 -5
- package/dist/types/src/request/artifact-diff.d.ts.map +1 -0
- package/dist/types/src/request/format.d.ts +15 -0
- package/dist/types/src/request/format.d.ts.map +1 -0
- package/dist/types/src/request/index.d.ts +4 -0
- package/dist/types/src/request/index.d.ts.map +1 -0
- package/dist/types/src/request/observer.d.ts +23 -0
- package/dist/types/src/request/observer.d.ts.map +1 -0
- package/dist/types/src/{session → request}/version-pin.d.ts +6 -6
- package/dist/types/src/request/version-pin.d.ts.map +1 -0
- package/dist/types/src/session/AiContext.d.ts +178 -0
- package/dist/types/src/session/AiContext.d.ts.map +1 -0
- package/dist/types/src/session/AiContext.test.d.ts +2 -0
- package/dist/types/src/session/AiContext.test.d.ts.map +1 -0
- package/dist/types/src/session/AiSession.d.ts +79 -0
- package/dist/types/src/session/AiSession.d.ts.map +1 -0
- package/dist/types/src/session/AiSession.test.d.ts +2 -0
- package/dist/types/src/session/AiSession.test.d.ts.map +1 -0
- package/dist/types/src/session/SessionLink.d.ts +17 -0
- package/dist/types/src/session/SessionLink.d.ts.map +1 -0
- package/dist/types/src/session/SessionLoader.d.ts +15 -0
- package/dist/types/src/session/SessionLoader.d.ts.map +1 -0
- package/dist/types/src/session/SessionLoader.test.d.ts +2 -0
- package/dist/types/src/session/SessionLoader.test.d.ts.map +1 -0
- package/dist/types/src/session/index.d.ts +5 -3
- package/dist/types/src/session/index.d.ts.map +1 -1
- package/dist/types/src/session/toolkit.d.ts +16 -0
- package/dist/types/src/session/toolkit.d.ts.map +1 -0
- package/dist/types/src/templates/system.d.ts +19 -14
- package/dist/types/src/templates/system.d.ts.map +1 -1
- package/dist/types/src/util/artifact.d.ts +18 -0
- package/dist/types/src/util/artifact.d.ts.map +1 -0
- package/dist/types/src/util/diff.d.ts +17 -0
- package/dist/types/src/util/diff.d.ts.map +1 -0
- package/dist/types/src/util/diff.test.d.ts +2 -0
- package/dist/types/src/util/diff.test.d.ts.map +1 -0
- package/dist/types/src/util/error-handling.d.ts +4 -0
- package/dist/types/src/util/error-handling.d.ts.map +1 -0
- package/dist/types/src/util/errors.d.ts +67 -0
- package/dist/types/src/util/errors.d.ts.map +1 -0
- package/dist/types/src/util/execution-graph.d.ts +85 -0
- package/dist/types/src/util/execution-graph.d.ts.map +1 -0
- package/dist/types/src/util/execution-graph.test.d.ts +2 -0
- package/dist/types/src/util/execution-graph.test.d.ts.map +1 -0
- package/dist/types/src/util/index.d.ts +7 -0
- package/dist/types/src/util/index.d.ts.map +1 -0
- package/dist/types/src/util/tracing.d.ts +296 -0
- package/dist/types/src/util/tracing.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +37 -47
- package/src/extraction/extraction-ner-function/index.ts +0 -1
- package/src/extraction/extraction-ner-function/named-entity-recognition.test.ts +12 -6
- package/src/extraction/extraction-ner-function/named-entity-recognition.ts +2 -0
- package/src/extraction/extraction.ts +49 -19
- package/src/extraction/index.ts +0 -1
- package/src/extraction/quotes.test.ts +2 -2
- package/src/extraction/quotes.ts +7 -9
- package/src/functions/services.ts +180 -66
- package/src/index.ts +2 -3
- package/src/request/AiRequest.ts +387 -0
- package/src/{session → request}/artifact-diff.ts +10 -6
- package/src/{session → request}/format.ts +45 -37
- package/src/request/index.ts +8 -0
- package/src/request/observer.ts +62 -0
- package/src/{session → request}/version-pin.ts +8 -8
- package/src/session/AiContext.test.ts +49 -0
- package/src/session/AiContext.ts +420 -0
- package/src/session/AiSession.test.ts +111 -0
- package/src/session/AiSession.ts +328 -0
- package/src/session/SessionLink.ts +23 -0
- package/src/session/SessionLoader.test.ts +115 -0
- package/src/session/SessionLoader.ts +55 -0
- package/src/session/index.ts +6 -3
- package/src/session/toolkit.ts +47 -0
- package/src/templates/{format.tpl → instructions/format.tpl} +6 -4
- package/src/templates/instructions/system.tpl +96 -0
- package/src/templates/system.ts +26 -16
- package/src/util/artifact.ts +96 -0
- package/src/util/diff.test.ts +74 -0
- package/src/util/diff.ts +93 -0
- package/src/util/error-handling.ts +42 -0
- package/src/util/errors.ts +15 -0
- package/src/util/execution-graph.test.ts +740 -0
- package/src/util/execution-graph.ts +520 -0
- package/src/util/index.ts +10 -0
- package/src/util/tracing.ts +60 -0
- package/dist/lib/browser/chunk-6WRGDEMH.mjs +0 -89
- package/dist/lib/browser/chunk-6WRGDEMH.mjs.map +0 -7
- package/dist/lib/browser/chunk-NZGGCEF3.mjs +0 -341
- package/dist/lib/browser/chunk-NZGGCEF3.mjs.map +0 -7
- package/dist/lib/browser/extraction/index.mjs +0 -269
- package/dist/lib/browser/extraction/index.mjs.map +0 -7
- package/dist/lib/browser/index.mjs +0 -354
- package/dist/lib/browser/index.mjs.map +0 -7
- package/dist/lib/browser/meta.json +0 -1
- package/dist/lib/browser/named-entity-recognition-O46UTCLR.mjs +0 -13
- package/dist/lib/node-esm/chunk-3UU5V4FS.mjs +0 -91
- package/dist/lib/node-esm/chunk-3UU5V4FS.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-XOOPGSGW.mjs +0 -343
- package/dist/lib/node-esm/chunk-XOOPGSGW.mjs.map +0 -7
- package/dist/lib/node-esm/extraction/index.mjs +0 -270
- package/dist/lib/node-esm/extraction/index.mjs.map +0 -7
- package/dist/lib/node-esm/index.mjs +0 -355
- package/dist/lib/node-esm/index.mjs.map +0 -7
- package/dist/lib/node-esm/meta.json +0 -1
- package/dist/lib/node-esm/named-entity-recognition-W4VMRCH6.mjs +0 -14
- package/dist/lib/node-esm/named-entity-recognition-W4VMRCH6.mjs.map +0 -7
- package/dist/types/src/conversation/context.d.ts +0 -90
- package/dist/types/src/conversation/context.d.ts.map +0 -1
- package/dist/types/src/conversation/conversation.d.ts +0 -50
- package/dist/types/src/conversation/conversation.d.ts.map +0 -1
- package/dist/types/src/conversation/index.d.ts +0 -3
- package/dist/types/src/conversation/index.d.ts.map +0 -1
- package/dist/types/src/errors.d.ts +0 -39
- package/dist/types/src/errors.d.ts.map +0 -1
- package/dist/types/src/extraction/extraction-llm-function/extraction-llm-function.d.ts +0 -4
- package/dist/types/src/extraction/extraction-llm-function/extraction-llm-function.d.ts.map +0 -1
- package/dist/types/src/extraction/extraction-llm-function/extraction-llm-function.test.d.ts +0 -2
- package/dist/types/src/extraction/extraction-llm-function/extraction-llm-function.test.d.ts.map +0 -1
- package/dist/types/src/extraction/extraction-llm-function/index.d.ts +0 -2
- package/dist/types/src/extraction/extraction-llm-function/index.d.ts.map +0 -1
- package/dist/types/src/extraction/extraction-ner-function/extraction-ner-function.d.ts +0 -306
- package/dist/types/src/extraction/extraction-ner-function/extraction-ner-function.d.ts.map +0 -1
- package/dist/types/src/extraction/extraction-ner-function/extraction-ner-function.test.d.ts +0 -2
- package/dist/types/src/extraction/extraction-ner-function/extraction-ner-function.test.d.ts.map +0 -1
- package/dist/types/src/session/artifact-diff.d.ts.map +0 -1
- package/dist/types/src/session/format.d.ts +0 -168
- package/dist/types/src/session/format.d.ts.map +0 -1
- package/dist/types/src/session/format.test.d.ts +0 -2
- package/dist/types/src/session/format.test.d.ts.map +0 -1
- package/dist/types/src/session/session.d.ts +0 -167
- package/dist/types/src/session/session.d.ts.map +0 -1
- package/dist/types/src/session/session.test.d.ts +0 -2
- package/dist/types/src/session/session.test.d.ts.map +0 -1
- package/dist/types/src/session/version-pin.d.ts.map +0 -1
- package/dist/types/src/session/version-pin.test.d.ts +0 -2
- package/dist/types/src/session/version-pin.test.d.ts.map +0 -1
- package/dist/types/src/summarization/summarization.test.d.ts +0 -2
- package/dist/types/src/summarization/summarization.test.d.ts.map +0 -1
- package/dist/types/src/types.d.ts +0 -16
- package/dist/types/src/types.d.ts.map +0 -1
- package/src/conversation/context.ts +0 -133
- package/src/conversation/conversation.ts +0 -125
- package/src/conversation/index.ts +0 -6
- package/src/errors.ts +0 -10
- package/src/extraction/extraction-llm-function/extraction-llm-function.test.ts +0 -84
- package/src/extraction/extraction-llm-function/extraction-llm-function.ts +0 -71
- package/src/extraction/extraction-llm-function/index.ts +0 -5
- package/src/extraction/extraction-ner-function/extraction-ner-function.test.ts +0 -116
- package/src/extraction/extraction-ner-function/extraction-ner-function.ts +0 -57
- package/src/session/__snapshots__/session.test.ts.snap +0 -32
- package/src/session/format.test.ts +0 -60
- package/src/session/session.test.ts +0 -305
- package/src/session/session.ts +0 -279
- package/src/session/version-pin.test.ts +0 -7
- package/src/summarization/summarization.test.ts +0 -197
- package/src/templates/system.tpl +0 -47
- package/src/types.ts +0 -52
- /package/dist/lib/{browser/named-entity-recognition-O46UTCLR.mjs.map → neutral/chunk-J5LGTIGS.mjs.map} +0 -0
- /package/src/extraction/extraction-llm-function/{instructions.tpl → transcription-instructions.tpl} +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
|
+
}
|
package/src/session/index.ts
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
export * from './
|
|
6
|
-
|
|
7
|
-
export * from './
|
|
5
|
+
export * from './toolkit';
|
|
6
|
+
|
|
7
|
+
export * as AiContext from './AiContext';
|
|
8
|
+
export * as AiSession from './AiSession';
|
|
9
|
+
export { SessionLoader } from './SessionLoader';
|
|
10
|
+
export * as SessionLink from './SessionLink';
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as Toolkit from '@effect/ai/Toolkit';
|
|
6
|
+
import * as Effect from 'effect/Effect';
|
|
7
|
+
import * as Layer from 'effect/Layer';
|
|
8
|
+
|
|
9
|
+
import { type AiToolNotFoundError, OpaqueToolkit, ToolExecutionService, ToolResolverService } from '@dxos/ai';
|
|
10
|
+
import { type Blueprint } from '@dxos/compute';
|
|
11
|
+
import { isTruthy } from '@dxos/util';
|
|
12
|
+
|
|
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[];
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Build a combined toolkit from the blueprint tools and the provided toolkit.
|
|
24
|
+
*/
|
|
25
|
+
export const createToolkit = ({
|
|
26
|
+
toolkit: toolkitProp,
|
|
27
|
+
blueprints = [],
|
|
28
|
+
opaqueToolkits = [],
|
|
29
|
+
}: CreateToolkitProps): Effect.Effect<
|
|
30
|
+
OpaqueToolkit.OpaqueToolkit,
|
|
31
|
+
AiToolNotFoundError,
|
|
32
|
+
ToolResolverService | ToolExecutionService
|
|
33
|
+
> =>
|
|
34
|
+
Effect.gen(function* () {
|
|
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);
|
|
38
|
+
|
|
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,
|
|
45
|
+
);
|
|
46
|
+
return OpaqueToolkit.make(mergedToolkit, combinedHandlerLayer as any) as OpaqueToolkit.OpaqueToolkit;
|
|
47
|
+
}) as Effect.Effect<OpaqueToolkit.OpaqueToolkit, AiToolNotFoundError, ToolResolverService | ToolExecutionService>;
|
|
@@ -23,6 +23,7 @@ The <typename> element is the type of the object.
|
|
|
23
23
|
It is very important to respond in the correct format.
|
|
24
24
|
You response must be valid Markdown.
|
|
25
25
|
In some cases you should respond with well-formed XML tags along with the content.
|
|
26
|
+
Code snippets should use 2-space indents.
|
|
26
27
|
|
|
27
28
|
{{#if cot}}
|
|
28
29
|
Before responding, explain your reasoning and include your detailed chain-of-thought enclosed in a <cot> tag.
|
|
@@ -42,14 +43,15 @@ You can reply with a block reference for objects if you have the DXN. For exampl
|
|
|
42
43
|
|
|
43
44
|
### Suggestions
|
|
44
45
|
|
|
45
|
-
Each suggestion must in the form of a proposed action enclosed in a <
|
|
46
|
-
Example: <
|
|
46
|
+
Each suggestion must in the form of a proposed action enclosed in a <suggestion> tag.
|
|
47
|
+
Example: <suggestion>Show on a map</suggestion>
|
|
48
|
+
Do not use bullets when listing suggestions.
|
|
47
49
|
|
|
48
50
|
### Multiple choice questions
|
|
49
51
|
|
|
50
52
|
You may ask multiple choice questions by wrapping consise options inside <option> tags inside a well formed <select> tag.
|
|
51
53
|
Example: <select><option>Yes</option><option>No</option></select>
|
|
52
54
|
|
|
53
|
-
##
|
|
55
|
+
## Context
|
|
54
56
|
|
|
55
|
-
|
|
57
|
+
The current date and time is {{DATETIME}}.
|