@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,42 +2,94 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { AiLanguageModel, AiToolkit, type AiError, type AiResponse, type AiTool } from '@effect/ai';
6
- import { Chunk, Context, Effect, Option, pipe, Stream, type Schema } from 'effect';
7
- import { Array, String } from 'effect';
8
-
5
+ import type * as AiError from '@effect/ai/AiError';
6
+ import * as LanguageModel from '@effect/ai/LanguageModel';
7
+ import type * as Tool from '@effect/ai/Tool';
8
+ import type * as Toolkit from '@effect/ai/Toolkit';
9
+ import * as Chunk from 'effect/Chunk';
10
+ import * as Effect from 'effect/Effect';
11
+ import * as Stream from 'effect/Stream';
12
+ import * as Array from 'effect/Array';
13
+ import * as Option from 'effect/Option';
9
14
  import {
10
15
  AiParser,
11
16
  AiPreprocessor,
12
- getToolCalls,
13
- runTool,
14
- ToolExecutionService,
15
- ToolResolverService,
16
- type AgentStatus,
17
- type AiInputPreprocessingError,
17
+ AiSummarizer,
18
18
  type AiToolNotFoundError,
19
- type GenerationStream,
19
+ callTool,
20
+ type PromptPreprocessingError,
21
+ type ToolExecutionService,
22
+ type ToolResolverService,
23
+ withoutToolCallParising,
20
24
  } from '@dxos/ai';
21
- import { Event } from '@dxos/async';
22
25
  import { type Blueprint } from '@dxos/blueprints';
23
- import { todo } from '@dxos/debug';
24
26
  import { Obj } from '@dxos/echo';
25
- import { ObjectVersion } from '@dxos/echo-db';
26
- import { type ObjectId } from '@dxos/echo-schema';
27
- import { DatabaseService } from '@dxos/functions';
27
+ import { type FunctionInvocationService, Trace, TracingService } from '@dxos/functions';
28
28
  import { log } from '@dxos/log';
29
- import { DataType, type ContentBlock } from '@dxos/schema';
29
+ import { ContentBlock, Message } from '@dxos/types';
30
+
31
+ import { type AiAssistantError } from '../errors';
30
32
 
31
- import { AiAssistantError } from '../errors';
33
+ import { formatSystemPrompt, formatUserPrompt } from './format';
34
+ import { GenerationObserver } from './observer';
35
+ import { pipe } from 'effect/Function';
36
+ import { CompleteBlock, PartialBlock } from '../tracing';
32
37
 
33
- export type AiSessionOptions = {};
38
+ export type AiSessionRunError = AiError.AiError | PromptPreprocessingError | AiToolNotFoundError | AiAssistantError;
34
39
 
35
- export type SessionRunOptions<Tools extends AiTool.Any> = {
40
+ export type AiSessionRunRequirements =
41
+ | LanguageModel.LanguageModel
42
+ | ToolExecutionService
43
+ | ToolResolverService
44
+ | FunctionInvocationService
45
+ | Trace.TraceService
46
+ /**
47
+ * @deprecated Retained for backward compatibility with tool handlers that use TracingService.emitStatus().
48
+ * New code should use Trace.TraceService instead.
49
+ */
50
+ | TracingService;
51
+
52
+ export type AiSessionOptions = {
53
+ /**
54
+ * Summarize before executing the prompt if the existing history exceeds this threshold.
55
+ */
56
+ summarizationThreshold?: number;
57
+
58
+ // TODO(dmaretskyi): Plan to phase out in favor of TracingService and the return type being a stream.
59
+ observer?: GenerationObserver;
60
+ /**
61
+ * Callback for when a message is received from the user, model, or tool.
62
+ * This is useful for streaming the output to a queue.
63
+ */
64
+ onOutput?: (message: Message.Message) => Effect.Effect<void, never, never>;
65
+ };
66
+
67
+ export type AiSessionRunProps<Tools extends Record<string, Tool.Any>> = {
36
68
  prompt: string;
37
- history: DataType.Message[];
38
- systemPrompt?: string;
39
- toolkit?: AiToolkit.AiToolkit<Tools>;
40
- blueprints?: Blueprint.Blueprint[];
69
+ // TODO(wittjosiah): Rename to systemPrompt.
70
+ system?: string;
71
+ history?: Message.Message[];
72
+ objects?: Obj.Unknown[];
73
+ blueprints?: readonly Blueprint.Blueprint[];
74
+ toolkit?: Toolkit.WithHandler<Tools>;
75
+ };
76
+
77
+ export type AiSessionBeginProps = {
78
+ prompt: string;
79
+ system?: string;
80
+ history?: Message.Message[];
81
+ objects?: Obj.Unknown[];
82
+ blueprints?: readonly Blueprint.Blueprint[];
83
+ };
84
+
85
+ export type AiSessionTurnProps<Tools extends Record<string, Tool.Any>> = {
86
+ system: string;
87
+ toolkit?: Toolkit.WithHandler<Tools>;
88
+ };
89
+
90
+ export type AiSessionTurnResult = {
91
+ messages: Message.Message[];
92
+ done: boolean;
41
93
  };
42
94
 
43
95
  /**
@@ -50,287 +102,247 @@ export type SessionRunOptions<Tools extends AiTool.Any> = {
50
102
  * Could be run locally in the app or remotely.
51
103
  * Could be personal or shared.
52
104
  */
105
+ // TODO(dmaretskyi): Rename AiSessionRequest
53
106
  export class AiSession {
54
- // TODO(burdon): Move to conversation.
107
+ /** Prevents concurrent execution of session. */
55
108
  private readonly _semaphore = Effect.runSync(Effect.makeSemaphore(1));
56
109
 
57
- /** Pending messages (incl. the current user request). */
58
- private _pending: DataType.Message[] = [];
110
+ private readonly _observer: GenerationObserver;
111
+ private readonly _onOutput: (message: Message.Message) => Effect.Effect<void, never, never>;
59
112
 
60
- /** Current streaming response. */
61
- private _stream: GenerationStream | undefined;
113
+ /** Message history from queue. */
114
+ // TODO(burdon): Evolve into supporting a git-like graph of messages.
115
+ private _history: Message.Message[] = [];
62
116
 
63
- /** Prior history from queue. */
64
- private _history: DataType.Message[] = [];
117
+ /** Pending messages for this session (incl. the current prompt). */
118
+ private _pending: Message.Message[] = [];
65
119
 
66
- /**
67
- * New message.
68
- */
69
- public readonly message = new Event<DataType.Message>();
120
+ private _started = 0;
121
+ private _ended = 0;
122
+ private _toolCalls = 0;
70
123
 
71
- /**
72
- * Complete block added to Message.
73
- */
74
- public readonly block = new Event<ContentBlock.Any>();
124
+ constructor(private readonly _options: AiSessionOptions = {}) {
125
+ this._observer = _options.observer ?? GenerationObserver.noop();
126
+ this._onOutput = _options.onOutput ?? (() => Effect.void);
127
+ }
75
128
 
76
- /**
77
- * Update partial block (while streaming).
78
- */
79
- public readonly update = new Event<ContentBlock.Any>();
129
+ get duration(): number {
130
+ return this._ended - this._started;
131
+ }
80
132
 
81
- /**
82
- * Unparsed events from the underlying generation stream.
83
- */
84
- public readonly streamEvent = new Event<AiResponse.Part>();
133
+ get toolCalls(): number {
134
+ return this._toolCalls;
135
+ }
85
136
 
86
- /**
87
- * User prompt or tool result.
88
- */
89
- public readonly userMessage = new Event<DataType.Message>();
137
+ get pending(): readonly Message.Message[] {
138
+ return this._pending;
139
+ }
90
140
 
91
- /**
92
- * Agent self-reporting its status.
93
- * Triggered by the model.
94
- */
95
- public readonly statusReport = new Event<AgentStatus>();
141
+ private _submitMessage = (message: Message.Message): Effect.Effect<Message.Message, never, Trace.TraceService> =>
142
+ Effect.gen(this, function* () {
143
+ this._pending.push(message);
144
+ yield* this._observer.onMessage(message);
145
+ for (const block of message.blocks) {
146
+ yield* Trace.write(CompleteBlock, {
147
+ messageId: message.id,
148
+ role: message.sender.role!,
149
+ block,
150
+ });
151
+ }
152
+ yield* this._onOutput(message);
153
+ return message;
154
+ });
155
+
156
+ getToolCalls = () =>
157
+ pipe(
158
+ [...this._history, ...this._pending],
159
+ Array.reverse,
160
+ Array.takeWhile((_) => _.sender.role === 'assistant'),
161
+ Array.flatMap((_) => _.blocks.filter(ContentBlock.is('toolCall')).map((block) => ({ block, message: _ }))),
162
+ Array.filter((_) => !_.block.providerExecuted),
163
+ );
96
164
 
97
165
  /**
98
- * Emits when the session is done.
166
+ * Initialize a session: set up history, perform summarization if needed, and submit the user prompt.
167
+ * Must be called before `runTurn()`.
99
168
  */
100
- public readonly done = new Event<void>();
169
+ begin = ({
170
+ prompt,
171
+ system,
172
+ history = [],
173
+ blueprints = [],
174
+ objects = [],
175
+ }: AiSessionBeginProps): Effect.Effect<void, AiSessionRunError, AiSessionRunRequirements> =>
176
+ Effect.gen(this, function* () {
177
+ this._started = Date.now();
178
+ this._history = [...history];
179
+ this._pending = [];
180
+
181
+ const systemPrompt = yield* formatSystemPrompt({ system, blueprints, objects }).pipe(Effect.orDie);
182
+
183
+ if (this._options.summarizationThreshold !== undefined) {
184
+ const tokenCount = yield* AiPreprocessor.estimateTokens(
185
+ yield* AiPreprocessor.preprocessPrompt([...this._history], {
186
+ system: systemPrompt,
187
+ }),
188
+ );
189
+ if (tokenCount > this._options.summarizationThreshold) {
190
+ const summary = yield* AiSummarizer.summarize([...this._history]);
191
+ yield* this._submitMessage(summary);
192
+ }
193
+ }
101
194
 
102
- constructor(private readonly _options: AiSessionOptions = {}) {}
195
+ yield* this._submitMessage(yield* formatUserPrompt({ prompt, history }));
196
+ });
103
197
 
104
198
  /**
105
- * Runs the AI model loop interacting with tools and artifacts.
106
- * @param options - The session options.
107
- * @returns The messages generated by the session, including the user's prompt.
199
+ * Execute a single turn: one LLM generation followed by tool execution.
200
+ * The toolkit and system prompt can be updated between turns to reflect context changes (e.g. dynamically enabled blueprints).
108
201
  */
109
- // TODO(dmaretskyi): Toolkit context doesn't get added to the effect type.
110
- run = <Tools extends AiTool.Any>(
111
- options: SessionRunOptions<Tools>,
112
- ): Effect.Effect<
113
- DataType.Message[],
114
- AiAssistantError | AiInputPreprocessingError | AiToolNotFoundError | AiError.AiError,
115
- AiLanguageModel.AiLanguageModel | ToolResolverService | ToolExecutionService | AiTool.ToHandler<Tools>
116
- > =>
202
+ runAgentTurn = <Tools extends Record<string, Tool.Any>>({
203
+ system,
204
+ toolkit,
205
+ }: AiSessionTurnProps<Tools>): Effect.Effect<AiSessionTurnResult, AiSessionRunError, AiSessionRunRequirements> =>
117
206
  Effect.gen(this, function* () {
118
- this._history = [...options.history];
119
-
120
- const promptMessages = yield* this._formatUserPrompt(options.prompt, options.history);
121
- this._pending = [promptMessages];
122
- this.userMessage.emit(promptMessages);
207
+ log('request', {
208
+ system: { snippet: createSnippet(system), length: system.length },
209
+ pending: this._pending.length,
210
+ history: this._history.length,
211
+ });
123
212
 
124
- // Potential tool use loop.
125
- do {
126
- log('request', {
127
- pending: this._pending.length,
128
- history: this._history.length,
129
- tools: (Object.values(options.toolkit?.tools ?? {}) as AiTool.Any[]).map((tool: AiTool.Any) => tool.name),
130
- });
213
+ const prompt = yield* AiPreprocessor.preprocessPrompt([...this._history, ...this._pending], {
214
+ system,
215
+ cacheControl: 'ephemeral',
216
+ });
131
217
 
132
- const prompt = yield* AiPreprocessor.preprocessAiInput([...this._history, ...this._pending]);
218
+ const observer = this._observer;
219
+ let currentMessageId: Obj.ID | null = null; // Consistent IDs for pending blocks preceding the complete one.
220
+ const messages = yield* LanguageModel.streamText({
221
+ prompt,
222
+ toolkit,
223
+ disableToolCallResolution: true,
224
+ }).pipe(
225
+ withoutToolCallParising,
226
+ AiParser.parseResponse({
227
+ emitPartial: true,
228
+ onBegin: () => observer.onBegin(),
229
+ onBlock: (block) => observer.onBlock(block),
230
+ onPart: (part) => observer.onPart(part as any),
231
+ onEnd: (summary) => observer.onEnd(summary),
232
+ }),
233
+ Stream.mapEffect(
234
+ (block) =>
235
+ Effect.gen(this, function* () {
236
+ if (block.pending) {
237
+ currentMessageId ??= Obj.ID.random();
238
+ log('emit ephemeral message', { id: currentMessageId, type: block._tag });
239
+ yield* Trace.write(PartialBlock, {
240
+ messageId: currentMessageId,
241
+ role: 'assistant',
242
+ block,
243
+ });
244
+ return Option.none();
245
+ } else {
246
+ currentMessageId ??= Obj.ID.random();
247
+ const id = currentMessageId;
248
+ currentMessageId = null;
249
+ log('emit complete message', { id, type: block._tag });
250
+ const message = Obj.make(Message.Message, {
251
+ id,
252
+ created: new Date().toISOString(),
253
+ sender: { role: 'assistant' },
254
+ blocks: [block],
255
+ });
256
+ return Option.some(yield* this._submitMessage(message));
257
+ }
258
+ }),
259
+ { concurrency: 1, unordered: false },
260
+ ),
261
+ Stream.filterMap((_) => _),
262
+ Stream.runCollect,
263
+ Effect.map(Chunk.toArray),
264
+ );
265
+
266
+ const toolCalls = this.getToolCalls();
267
+
268
+ if (toolCalls.length === 0) {
269
+ this._ended = Date.now();
270
+ return { messages, done: true };
271
+ } else if (!toolkit) {
272
+ throw new Error('No toolkit provided');
273
+ }
133
274
 
134
- const blueprints = options.blueprints ?? [];
275
+ return { messages, done: false };
276
+ });
135
277
 
136
- // Build system prompt from blueprint templates.
137
- // TODO(dmaretskyi): Loading BP from the Database should be done at the higher level. We need a type for the resolved blueprint.
138
- const systemPrompt = yield* pipe(
139
- blueprints,
140
- Effect.forEach((blueprint) => DatabaseService.loadRef(blueprint.instructions)),
141
- Effect.flatMap(Effect.forEach((template) => DatabaseService.loadRef(template.source))),
142
- Effect.map(Array.map((template) => `\n\n<blueprint>${template.content}</blueprint>`)),
143
- Effect.map(Array.reduce(options.systemPrompt ?? '', String.concat)),
278
+ runTools = <Tools extends Record<string, Tool.Any>>({ toolkit }: { toolkit?: Toolkit.WithHandler<Tools> }) =>
279
+ Effect.gen(this, function* () {
280
+ const toolCalls = this.getToolCalls();
281
+ // TODO(burdon): Retry errors? Write result when each completes individually?
282
+ const toolResults = yield* Effect.forEach(toolCalls, ({ block, message }) => {
283
+ if (!toolkit) {
284
+ throw new Error('No toolkit provided');
285
+ }
286
+ return callTool(toolkit, block).pipe(
287
+ Effect.provide(
288
+ TracingService.layerSubframe((context) => ({
289
+ ...context,
290
+ parentMessage: message.id,
291
+ toolCallId: block.toolCallId,
292
+ })),
293
+ ),
144
294
  );
295
+ });
145
296
 
146
- // Build a combined toolkit from the blueprint tools and the provided toolkit.
147
- const blueprintToolkit = yield* ToolResolverService.resolveToolkit(blueprints.flatMap((bp) => bp.tools));
148
- const blueprintToolkitHandler: Context.Context<AiTool.ToHandler<AiTool.Any>> =
149
- yield* blueprintToolkit.toContext(yield* ToolExecutionService.handlersFor(blueprintToolkit));
150
- const toolkit = options.toolkit != null ? AiToolkit.merge(options.toolkit, blueprintToolkit) : blueprintToolkit;
151
- const toolkitWithBlueprintHandlers = yield* toolkit.pipe(
152
- Effect.provide(blueprintToolkitHandler) as any,
153
- ) as Effect.Effect<AiToolkit.ToHandler<any>, never, AiTool.ToHandler<Tools>>;
154
-
155
- log.info('run', {
156
- systemPrompt: [systemPrompt.slice(0, 32), '...', systemPrompt.slice(-32), systemPrompt.length].join(' '),
157
- tools: Object.keys(blueprintToolkit.tools),
158
- });
297
+ // TODO(wittjosiah): Sometimes tool error results are added to the queue before the tool agent statuses.
298
+ // TODO(dmaretskyi): Stream tool results one by one.
299
+ yield* this._submitMessage(
300
+ Obj.make(Message.Message, {
301
+ created: new Date().toISOString(),
302
+ sender: { role: 'tool' },
303
+ blocks: toolResults,
304
+ }),
305
+ );
159
306
 
160
- const blocks = yield* AiLanguageModel.streamText({
161
- prompt,
162
- system: systemPrompt,
163
- toolkit: toolkitWithBlueprintHandlers,
164
- disableToolCallResolution: true,
165
- }).pipe(
166
- AiParser.parseGptStream({
167
- onBlock: (block) =>
168
- Effect.gen(this, function* () {
169
- if (block.pending) {
170
- this.update.emit(block);
171
- } else {
172
- this.block.emit(block);
173
- }
174
- }),
175
- onPart: (part) =>
176
- Effect.gen(this, function* () {
177
- this.streamEvent.emit(part);
178
- }),
179
- }),
180
- Stream.runCollect,
181
- Effect.map(Chunk.toArray),
182
- );
307
+ this._toolCalls += toolResults.length;
308
+ });
309
+ /**
310
+ * Run a full conversation turn loop. Equivalent to calling `begin()` then `runTurn()` in a loop.
311
+ */
312
+ run = <Tools extends Record<string, Tool.Any>>({
313
+ prompt,
314
+ system: systemTemplate,
315
+ history = [],
316
+ objects = [],
317
+ blueprints = [],
318
+ toolkit,
319
+ }: AiSessionRunProps<Tools>): Effect.Effect<Message.Message[], AiSessionRunError, AiSessionRunRequirements> =>
320
+ Effect.gen(this, function* () {
321
+ yield* this.begin({ prompt, system: systemTemplate, history, objects, blueprints });
183
322
 
184
- const response = Obj.make(DataType.Message, {
185
- created: new Date().toISOString(),
186
- sender: { role: 'assistant' },
187
- blocks,
188
- });
189
- this._pending.push(response);
190
- this.message.emit(response);
323
+ const system = yield* formatSystemPrompt({ system: systemTemplate, blueprints, objects }).pipe(Effect.orDie);
191
324
 
192
- const toolCalls = getToolCalls(response);
193
- if (toolCalls.length === 0) {
325
+ do {
326
+ const { done } = yield* this.runAgentTurn({ system, toolkit });
327
+ if (done) {
194
328
  break;
195
329
  }
196
-
197
- const toolResults = yield* runTools(toolCalls, toolkitWithBlueprintHandlers as any);
198
- this._pending.push(
199
- Obj.make(DataType.Message, {
200
- created: new Date().toISOString(),
201
- sender: { role: 'user' },
202
- blocks: toolResults,
203
- }),
204
- );
330
+ yield* this.runTools({ toolkit });
205
331
  } while (true);
206
332
 
333
+ log('done', { pending: this._pending.length, duration: this.duration, tools: this._toolCalls });
207
334
  return this._pending;
208
335
  }).pipe(this._semaphore.withPermits(1), Effect.withSpan('AiSession.run'));
209
-
210
- async runStructured<S extends Schema.Schema.AnyNoContext>(
211
- schema: S,
212
- options: SessionRunOptions<AiTool.Any>,
213
- ): Promise<Schema.Schema.Type<S>> {
214
- return todo();
215
- // const parser = structuredOutputParser(schema);
216
- // const result = await this.run({
217
- // ...options,
218
- // executableTools: [...(options.executableTools ?? []), parser.tool],
219
- // });
220
- // return parser.getResult(result);
221
- }
222
-
223
- private _formatUserPrompt = (prompt: string, history: DataType.Message[]) =>
224
- Effect.gen(function* () {
225
- const prelude: ContentBlock.Any[] = [];
226
-
227
- // TODO(dmaretskyi): Evaluate other approaches as `serviceOption` isn't represented in the type system.
228
- const artifactDiffResolver = yield* Effect.serviceOption(ArtifactDiffResolver);
229
- if (Option.isSome(artifactDiffResolver)) {
230
- const versions = gatherObjectVersions(history);
231
-
232
- const artifactDiff = yield* Effect.tryPromise({
233
- try: () =>
234
- artifactDiffResolver.value.resolve(
235
- [...versions.entries()].map(([id, version]) => ({ id, lastVersion: version })),
236
- ),
237
- catch: AiAssistantError.wrap('Artifact diff resolution error'),
238
- });
239
-
240
- log.info('vision', {
241
- artifactDiff,
242
- versions,
243
- });
244
-
245
- for (const [id, { version }] of [...artifactDiff.entries()]) {
246
- if (ObjectVersion.equals(version, versions.get(id)!)) {
247
- artifactDiff.delete(id);
248
- continue;
249
- }
250
-
251
- prelude.push({ _tag: 'anchor', objectId: id, version });
252
- }
253
- if (artifactDiff.size > 0) {
254
- prelude.push(createArtifactUpdateBlock(artifactDiff));
255
- }
256
- }
257
-
258
- return Obj.make(DataType.Message, {
259
- created: new Date().toISOString(),
260
- sender: { role: 'user' },
261
- blocks: [...prelude, { _tag: 'text', text: prompt }],
262
- });
263
- });
264
-
265
- abort(): void {
266
- this._stream?.abort();
267
- }
268
336
  }
269
337
 
270
- const gatherObjectVersions = (messages: DataType.Message[]): Map<ObjectId, ObjectVersion> => {
271
- const artifactIds = new Map<ObjectId, ObjectVersion>();
272
- for (const message of messages) {
273
- for (const block of message.blocks) {
274
- if (block._tag === 'anchor') {
275
- artifactIds.set(block.objectId, block.version as ObjectVersion);
276
- }
277
- }
278
- }
279
-
280
- return artifactIds;
281
- };
338
+ const createSnippet = (text: string, len = 32) =>
339
+ text.length <= len * 2 ? text : [text.slice(0, len), '...', text.slice(-len)].join('');
282
340
 
283
- const createArtifactUpdateBlock = (
284
- artifactDiff: Map<ObjectId, { version: ObjectVersion; diff?: string }>,
285
- ): ContentBlock.Any => {
286
- return {
287
- _tag: 'text',
288
- // TODO(dmaretskyi): Does this need to be a special content-block?
289
- disposition: 'artifact-update',
290
- text: `
291
- The following artifacts have been updated since the last message:
292
- ${[...artifactDiff.entries()]
293
- .map(([id, { diff }]) => `<changed-artifact id="${id}">${diff ? `\n${diff}` : ''}</changed-artifact>`)
294
- .join('\n')}
295
- `,
296
- };
341
+ // TODO(dmaretskyi): Extract as a general util.
342
+ const logDump = (message: string, data: unknown) => {
343
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
344
+ const { writeFileSync } = require('node:fs');
345
+ const path = `/tmp/log-data-${Date.now()}.json`;
346
+ writeFileSync(path, JSON.stringify(data, null, 2));
347
+ log.info(message, { path });
297
348
  };
298
-
299
- /**
300
- * Resolves artifact ids to their versions.
301
- * Used to give the model a sense of the changes to the artifacts made by users during the conversation.
302
- * The artifacts versions are pinned in the history, and whenever the artifact changes in-between assistant's steps,
303
- * a diff is inserted into the conversation.
304
- *
305
- * Can be optionally provided to the session run call.
306
- */
307
- // TODO(dmaretskyi): Convert to Context.Reference
308
- export class ArtifactDiffResolver extends Context.Tag('ArtifactDiffResolver')<
309
- ArtifactDiffResolver,
310
- ArtifactDiffResolver.Service
311
- >() {}
312
-
313
- export namespace ArtifactDiffResolver {
314
- export type Service = {
315
- resolve: (artifacts: { id: ObjectId; lastVersion: ObjectVersion }[]) => Promise<
316
- Map<
317
- ObjectId,
318
- {
319
- version: ObjectVersion;
320
- diff?: string;
321
- }
322
- >
323
- >;
324
- };
325
- }
326
-
327
- const runTools: <Tools extends AiTool.Any>(
328
- toolCalls: ContentBlock.ToolCall[],
329
- toolkit: AiToolkit.AiToolkit<Tools>,
330
- ) => Effect.Effect<ContentBlock.ToolResult[], AiError.AiError, AiTool.ToHandler<Tools>> = Effect.fn('runTools')(
331
- function* (toolCalls, toolkit) {
332
- const toolkitWithHandlers = Effect.isEffect(toolkit) ? yield* toolkit : toolkit;
333
-
334
- return yield* Effect.forEach(toolCalls, (toolCall) => runTool(toolkitWithHandlers, toolCall));
335
- },
336
- );
@@ -0,0 +1,44 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import type * as Tool from '@effect/ai/Tool';
6
+ import * as Toolkit from '@effect/ai/Toolkit';
7
+ import * as Effect from 'effect/Effect';
8
+
9
+ import { type AiToolNotFoundError, GenericToolkit, ToolExecutionService, ToolResolverService } from '@dxos/ai';
10
+ import { type Blueprint } from '@dxos/blueprints';
11
+ import { isTruthy } from '@dxos/util';
12
+
13
+ export type CreateToolkitProps = {
14
+ toolkit?: Toolkit.Any;
15
+ blueprints?: readonly Blueprint.Blueprint[];
16
+ /**
17
+ * Self-contained with handlers toolkits.
18
+ */
19
+ genericToolkits?: readonly GenericToolkit.GenericToolkit[];
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
+ genericToolkits = [],
29
+ }: CreateToolkitProps): Effect.Effect<
30
+ Toolkit.WithHandler<any>,
31
+ AiToolNotFoundError,
32
+ ToolResolverService | ToolExecutionService | Tool.HandlersFor<any>
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 genericToolkit = GenericToolkit.merge(...genericToolkits);
38
+
39
+ const toolkit = Toolkit.merge(...[toolkitProp, blueprintToolkit, genericToolkit.toolkit].filter(isTruthy));
40
+ return yield* toolkit.pipe(
41
+ Effect.provide(blueprintToolHandler),
42
+ Effect.provide(genericToolkit.layer),
43
+ ) as any as Effect.Effect<Toolkit.WithHandler<any>, never, Tool.HandlersFor<any>>;
44
+ });