effect-assistant 0.0.0-stage → 0.1.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 (185) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +22 -2
  3. package/dist/AppEvent.d.ts +111 -0
  4. package/dist/AppEvent.d.ts.map +1 -0
  5. package/dist/AppEvent.js +71 -0
  6. package/dist/AppEvent.js.map +1 -0
  7. package/dist/Assistant.d.ts +403 -0
  8. package/dist/Assistant.d.ts.map +1 -0
  9. package/dist/Assistant.js +126 -0
  10. package/dist/Assistant.js.map +1 -0
  11. package/dist/AssistantActivity.d.ts +85 -0
  12. package/dist/AssistantActivity.d.ts.map +1 -0
  13. package/dist/AssistantActivity.js +105 -0
  14. package/dist/AssistantActivity.js.map +1 -0
  15. package/dist/AssistantEngine.d.ts +116 -0
  16. package/dist/AssistantEngine.d.ts.map +1 -0
  17. package/dist/AssistantEngine.js +255 -0
  18. package/dist/AssistantEngine.js.map +1 -0
  19. package/dist/AssistantEvent.d.ts +614 -0
  20. package/dist/AssistantEvent.d.ts.map +1 -0
  21. package/dist/AssistantEvent.js +523 -0
  22. package/dist/AssistantEvent.js.map +1 -0
  23. package/dist/AssistantLoop.d.ts +171 -0
  24. package/dist/AssistantLoop.d.ts.map +1 -0
  25. package/dist/AssistantLoop.js +108 -0
  26. package/dist/AssistantLoop.js.map +1 -0
  27. package/dist/AssistantToolCall.d.ts +32 -0
  28. package/dist/AssistantToolCall.d.ts.map +1 -0
  29. package/dist/AssistantToolCall.js +37 -0
  30. package/dist/AssistantToolCall.js.map +1 -0
  31. package/dist/Block.d.ts +69 -0
  32. package/dist/Block.d.ts.map +1 -0
  33. package/dist/Block.js +39 -0
  34. package/dist/Block.js.map +1 -0
  35. package/dist/BlockKit.d.ts +184 -0
  36. package/dist/BlockKit.d.ts.map +1 -0
  37. package/dist/BlockKit.js +151 -0
  38. package/dist/BlockKit.js.map +1 -0
  39. package/dist/ChatId.d.ts +31 -0
  40. package/dist/ChatId.d.ts.map +1 -0
  41. package/dist/ChatId.js +36 -0
  42. package/dist/ChatId.js.map +1 -0
  43. package/dist/ChatLog.d.ts +109 -0
  44. package/dist/ChatLog.d.ts.map +1 -0
  45. package/dist/ChatLog.js +177 -0
  46. package/dist/ChatLog.js.map +1 -0
  47. package/dist/ChatView.d.ts +459 -0
  48. package/dist/ChatView.d.ts.map +1 -0
  49. package/dist/ChatView.js +496 -0
  50. package/dist/ChatView.js.map +1 -0
  51. package/dist/Chats.d.ts +110 -0
  52. package/dist/Chats.d.ts.map +1 -0
  53. package/dist/Chats.js +98 -0
  54. package/dist/Chats.js.map +1 -0
  55. package/dist/Compaction.d.ts +105 -0
  56. package/dist/Compaction.d.ts.map +1 -0
  57. package/dist/Compaction.js +133 -0
  58. package/dist/Compaction.js.map +1 -0
  59. package/dist/CurrentChat.d.ts +40 -0
  60. package/dist/CurrentChat.d.ts.map +1 -0
  61. package/dist/CurrentChat.js +33 -0
  62. package/dist/CurrentChat.js.map +1 -0
  63. package/dist/Decision.d.ts +145 -0
  64. package/dist/Decision.d.ts.map +1 -0
  65. package/dist/Decision.js +178 -0
  66. package/dist/Decision.js.map +1 -0
  67. package/dist/DecisionId.d.ts +37 -0
  68. package/dist/DecisionId.d.ts.map +1 -0
  69. package/dist/DecisionId.js +44 -0
  70. package/dist/DecisionId.js.map +1 -0
  71. package/dist/InputId.d.ts +38 -0
  72. package/dist/InputId.d.ts.map +1 -0
  73. package/dist/InputId.js +43 -0
  74. package/dist/InputId.js.map +1 -0
  75. package/dist/Instructions.d.ts +56 -0
  76. package/dist/Instructions.d.ts.map +1 -0
  77. package/dist/Instructions.js +50 -0
  78. package/dist/Instructions.js.map +1 -0
  79. package/dist/Memory.d.ts +190 -0
  80. package/dist/Memory.d.ts.map +1 -0
  81. package/dist/Memory.js +216 -0
  82. package/dist/Memory.js.map +1 -0
  83. package/dist/RunId.d.ts +41 -0
  84. package/dist/RunId.d.ts.map +1 -0
  85. package/dist/RunId.js +46 -0
  86. package/dist/RunId.js.map +1 -0
  87. package/dist/Skill.d.ts +72 -0
  88. package/dist/Skill.d.ts.map +1 -0
  89. package/dist/Skill.js +58 -0
  90. package/dist/Skill.js.map +1 -0
  91. package/dist/Specialist.d.ts +267 -0
  92. package/dist/Specialist.d.ts.map +1 -0
  93. package/dist/Specialist.js +119 -0
  94. package/dist/Specialist.js.map +1 -0
  95. package/dist/ToolDiscovery.d.ts +108 -0
  96. package/dist/ToolDiscovery.d.ts.map +1 -0
  97. package/dist/ToolDiscovery.js +139 -0
  98. package/dist/ToolDiscovery.js.map +1 -0
  99. package/dist/ToolFailure.d.ts +66 -0
  100. package/dist/ToolFailure.d.ts.map +1 -0
  101. package/dist/ToolFailure.js +79 -0
  102. package/dist/ToolFailure.js.map +1 -0
  103. package/dist/_virtual/_rolldown/runtime.js +13 -0
  104. package/dist/index.d.ts +26 -0
  105. package/dist/index.js +26 -0
  106. package/dist/internal/chat.js +202 -0
  107. package/dist/internal/chat.js.map +1 -0
  108. package/dist/internal/decisions.js +66 -0
  109. package/dist/internal/decisions.js.map +1 -0
  110. package/dist/internal/events.d.ts +46 -0
  111. package/dist/internal/events.d.ts.map +1 -0
  112. package/dist/internal/events.js +65 -0
  113. package/dist/internal/events.js.map +1 -0
  114. package/dist/internal/history.js +185 -0
  115. package/dist/internal/history.js.map +1 -0
  116. package/dist/internal/instructions.js +11 -0
  117. package/dist/internal/instructions.js.map +1 -0
  118. package/dist/internal/loop.d.ts +73 -0
  119. package/dist/internal/loop.d.ts.map +1 -0
  120. package/dist/internal/loop.js +166 -0
  121. package/dist/internal/loop.js.map +1 -0
  122. package/dist/internal/schema.js +14 -0
  123. package/dist/internal/schema.js.map +1 -0
  124. package/dist/internal/skills.js +28 -0
  125. package/dist/internal/skills.js.map +1 -0
  126. package/dist/internal/specialists.js +273 -0
  127. package/dist/internal/specialists.js.map +1 -0
  128. package/dist/internal/telemetry.js +85 -0
  129. package/dist/internal/telemetry.js.map +1 -0
  130. package/dist/internal/toolkit.d.ts +76 -0
  131. package/dist/internal/toolkit.d.ts.map +1 -0
  132. package/dist/internal/toolkit.js +129 -0
  133. package/dist/internal/toolkit.js.map +1 -0
  134. package/dist/internal/tools.js +132 -0
  135. package/dist/internal/tools.js.map +1 -0
  136. package/dist/internal/turns.d.ts +64 -0
  137. package/dist/internal/turns.d.ts.map +1 -0
  138. package/dist/internal/turns.js +221 -0
  139. package/dist/internal/turns.js.map +1 -0
  140. package/dist/internal/uuid.js +45 -0
  141. package/dist/internal/uuid.js.map +1 -0
  142. package/dist/internal/writer.d.ts +12 -0
  143. package/dist/internal/writer.d.ts.map +1 -0
  144. package/package.json +51 -3
  145. package/src/AppEvent.ts +134 -0
  146. package/src/Assistant.ts +589 -0
  147. package/src/AssistantActivity.ts +150 -0
  148. package/src/AssistantEngine.ts +343 -0
  149. package/src/AssistantEvent.ts +635 -0
  150. package/src/AssistantLoop.ts +207 -0
  151. package/src/AssistantToolCall.ts +40 -0
  152. package/src/Block.ts +84 -0
  153. package/src/BlockKit.ts +311 -0
  154. package/src/ChatId.ts +36 -0
  155. package/src/ChatLog.ts +247 -0
  156. package/src/ChatView.ts +658 -0
  157. package/src/Chats.ts +122 -0
  158. package/src/Compaction.ts +224 -0
  159. package/src/CurrentChat.ts +47 -0
  160. package/src/Decision.ts +241 -0
  161. package/src/DecisionId.ts +46 -0
  162. package/src/InputId.ts +43 -0
  163. package/src/Instructions.ts +72 -0
  164. package/src/Memory.ts +276 -0
  165. package/src/RunId.ts +46 -0
  166. package/src/Skill.ts +91 -0
  167. package/src/Specialist.ts +355 -0
  168. package/src/ToolDiscovery.ts +187 -0
  169. package/src/ToolFailure.ts +74 -0
  170. package/src/index.ts +130 -0
  171. package/src/internal/chat.ts +372 -0
  172. package/src/internal/decisions.ts +131 -0
  173. package/src/internal/events.ts +100 -0
  174. package/src/internal/history.ts +366 -0
  175. package/src/internal/instructions.ts +9 -0
  176. package/src/internal/loop.ts +344 -0
  177. package/src/internal/schema.ts +25 -0
  178. package/src/internal/skills.ts +37 -0
  179. package/src/internal/specialists.ts +414 -0
  180. package/src/internal/telemetry.ts +125 -0
  181. package/src/internal/toolkit.ts +231 -0
  182. package/src/internal/tools.ts +210 -0
  183. package/src/internal/turns.ts +429 -0
  184. package/src/internal/uuid.ts +50 -0
  185. package/src/internal/writer.ts +12 -0
@@ -0,0 +1,589 @@
1
+ /**
2
+ * This module provides `Assistant.make`, which defines an assistant that works in a chat the way a
3
+ * colleague does: the user messages it, it works in the background with the app's tools, and it
4
+ * messages the user back.
5
+ *
6
+ * A definition is what the rest of an app imports. Yielding it (`yield* MyAssistant`) returns the
7
+ * assistant's {@link Service}, which submits inputs to chats and streams them back. Its `toLayer`
8
+ * registers the handler that runs each {@link Turn} with the `AssistantEngine`, and its
9
+ * `layerClient` provides the service without a handler. Code that runs inside a chat reads and
10
+ * extends that chat through the definition's {@link Chat} key.
11
+ *
12
+ * @since 0.1.0
13
+ */
14
+ import type * as AiError from "effect/ai/AiError"
15
+ import type * as LanguageModel from "effect/ai/LanguageModel"
16
+ import type * as Prompt from "effect/ai/Prompt"
17
+ import type * as Tool from "effect/ai/Tool"
18
+ import type * as Toolkit from "effect/ai/Toolkit"
19
+ import * as Context from "effect/Context"
20
+ import * as Effect from "effect/Effect"
21
+ import * as Filter from "effect/Filter"
22
+ import * as Layer from "effect/Layer"
23
+ import * as Predicate from "effect/Predicate"
24
+ import * as Schema from "effect/Schema"
25
+ import type * as Scope from "effect/Scope"
26
+ import * as Stream from "effect/Stream"
27
+ import type * as AppEvent from "./AppEvent.ts"
28
+ import { AssistantEngine } from "./AssistantEngine.ts"
29
+ import * as AssistantEvent from "./AssistantEvent.ts"
30
+ import type * as AssistantLoop from "./AssistantLoop.ts"
31
+ import type * as BlockKit from "./BlockKit.ts"
32
+ import { ChatId } from "./ChatId.ts"
33
+ import * as Chats from "./Chats.ts"
34
+ import type { CurrentChat } from "./CurrentChat.ts"
35
+ import type * as Instructions from "./Instructions.ts"
36
+ import * as AppEvents from "./internal/events.ts"
37
+ import * as Turns from "./internal/turns.ts"
38
+ import type { Skill } from "./Skill.ts"
39
+ import type * as Specialist from "./Specialist.ts"
40
+
41
+ const TypeId = "~effect-assistant/Assistant" as const
42
+
43
+ /**
44
+ * Represents the services that a turn provides to its tools, its loop, and its specialists. These
45
+ * are the turn's chat (`CurrentChat`) and the chat's specialist runner (`Specialist.Runner`).
46
+ *
47
+ * @category utility types
48
+ * @since 0.1.0
49
+ */
50
+ export type TurnServices = CurrentChat | Specialist.Runner
51
+
52
+ /**
53
+ * Represents the options accepted by `turn.loop`.
54
+ *
55
+ * @category models
56
+ * @since 0.1.0
57
+ */
58
+ export interface LoopOptions<T extends Record<string, Tool.Any> = Record<never, never>, RS = never, Events = never> {
59
+ /** The assistant's role and rules. The loop places `Instructions.protocol` before them. */
60
+ readonly instructions: string | Instructions.Resolved
61
+ /**
62
+ * Tools the model can use in addition to the assistant's own. This is an `effect/ai` toolkit,
63
+ * and its handlers come from the layer. Combine several toolkits with `Toolkit.merge`, for
64
+ * example `Memory.Tools` or a `ToolDiscovery.make(...)` toolkit together with your own.
65
+ */
66
+ readonly toolkit?: Toolkit.Toolkit<T> | undefined
67
+ /**
68
+ * Playbooks the model can follow. The prompt lists them as a catalog, and the model loads one
69
+ * with `load_skill` when a task matches it.
70
+ */
71
+ readonly skills?: ReadonlyArray<Skill<RS>> | undefined
72
+ /** The maximum number of model calls per turn. Defaults to 16. */
73
+ readonly maxSteps?: number | undefined
74
+ /** The message the reply pass gives the model when the turn would end without a reply. */
75
+ readonly replyNudge?: string | undefined
76
+ /**
77
+ * Selects which of the chat's entries the model reads, for example only those since the last
78
+ * summary. Defaults to all entries.
79
+ */
80
+ readonly history?: HistoryView<Events> | undefined
81
+ }
82
+
83
+ /**
84
+ * Represents a function that selects which of a chat's entries the model reads, and what else it
85
+ * reads with them.
86
+ *
87
+ * **Details**
88
+ *
89
+ * The function receives the chat's entries and returns, in order, a mix of entries and
90
+ * `Prompt.Message`s. The model reads the returned entries the same way it reads the full history,
91
+ * and reads each prompt message unchanged.
92
+ *
93
+ * @category models
94
+ * @since 0.1.0
95
+ */
96
+ export type HistoryView<Events = never> = (
97
+ entries: ReadonlyArray<Entry<Events>>
98
+ ) => ReadonlyArray<Entry<Events> | Prompt.Message>
99
+
100
+ /**
101
+ * Represents an entry in a chat's log, with the assistant's own events decoded to their types.
102
+ *
103
+ * @category models
104
+ * @since 0.1.0
105
+ */
106
+ export interface Entry<Events = never> {
107
+ readonly chatId: ChatId
108
+ readonly seq: number
109
+ readonly at: number
110
+ readonly event: AssistantEvent.AssistantEvent | Events
111
+ }
112
+
113
+ /**
114
+ * Represents the options accepted by `turn.replyIfOwed`.
115
+ *
116
+ * @category models
117
+ * @since 0.1.0
118
+ */
119
+ export interface ReplyOptions {
120
+ /** The assistant's instructions, as passed to `turn.loop`. `Instructions.protocol` is added. */
121
+ readonly instructions: string | Instructions.Resolved
122
+ /**
123
+ * The message given to the model after the history. Defaults to a message saying that it
124
+ * hasn't replied yet and should reply now.
125
+ */
126
+ readonly nudge?: string | undefined
127
+ }
128
+
129
+ /**
130
+ * Represents one turn, as the assistant's handler receives it.
131
+ *
132
+ * `Events` is the type of the app's own events that the assistant registered with its `events`
133
+ * option.
134
+ *
135
+ * @category models
136
+ * @since 0.1.0
137
+ */
138
+ export interface Turn<Events = never> {
139
+ readonly chatId: ChatId
140
+ /**
141
+ * Returns the model's history, built from the chat's entries (all of them, or those a view
142
+ * keeps), together with how far into the chat it reaches.
143
+ *
144
+ * Read it before every model call, as the loop does. Each step records how far its model call
145
+ * read, and the turn ends once it has handled what the model saw. An input that arrived after
146
+ * that is handled by the next turn.
147
+ */
148
+ readonly history: (view?: HistoryView<Events>) => Effect.Effect<AssistantLoop.ChatHistory>
149
+ /**
150
+ * Returns the full system prompt for the given instructions. It contains
151
+ * `Instructions.protocol`, the instructions, and the specialist runs that are working now.
152
+ */
153
+ readonly instructions: (instructions: string | Instructions.Resolved) => Instructions.Resolved
154
+ /**
155
+ * Runs the agent loop over the chat. The loop uses `Instructions.protocol`, the assistant's own
156
+ * tools, and the instructions, tools, and skills in `options`. Every step is a tool call, and
157
+ * the loop ends with `replyIfOwed`.
158
+ */
159
+ readonly loop: <T extends Record<string, Tool.Any> = Record<never, never>, RS = never>(
160
+ options: LoopOptions<T, RS, Events>
161
+ ) => Stream.Stream<
162
+ AssistantLoop.Event,
163
+ AiError.AiError,
164
+ LanguageModel.LanguageModel | TurnServices | AssistantLoop.ToolkitServices<T> | RS
165
+ >
166
+ /**
167
+ * Runs one more pass when the turn would otherwise end without a reply to the user's latest
168
+ * message. In this pass the model can only reply.
169
+ *
170
+ * The stream is empty when no reply is owed, or when an input arrived that the next turn will
171
+ * read. `loop` runs this pass with its own prompt and tools, so the pass reuses the cached
172
+ * prompt.
173
+ */
174
+ readonly replyIfOwed: (
175
+ options: ReplyOptions
176
+ ) => Stream.Stream<AssistantLoop.Event, AiError.AiError, LanguageModel.LanguageModel | TurnServices>
177
+ }
178
+
179
+ /**
180
+ * Represents an assistant's handler, which runs a turn and returns it as a stream of the loop's
181
+ * events.
182
+ *
183
+ * The handler may return the stream directly or from an Effect. `R` is the services the stream
184
+ * requires, and `RE` is the services the Effect requires.
185
+ *
186
+ * @category models
187
+ * @since 0.1.0
188
+ */
189
+ export type Handler<R = never, RE = R, Events = never> = (
190
+ turn: Turn<Events>
191
+ ) =>
192
+ | Stream.Stream<AssistantLoop.Event, unknown, R>
193
+ | Effect.Effect<Stream.Stream<AssistantLoop.Event, unknown, R>, unknown, RE>
194
+
195
+ /**
196
+ * Computes the services an assistant's handler needs beyond those its turns provide (the turn
197
+ * services and `LanguageModel`) and its tools' handlers (which its layer requires). These are the
198
+ * services that its build Effect must require.
199
+ *
200
+ * @category utility types
201
+ * @since 0.1.0
202
+ */
203
+ export type HandlerServices<R> = Exclude<R, TurnServices | LanguageModel.LanguageModel | Tool.Handler<string>>
204
+
205
+ const toStream = <R, RE>(
206
+ result:
207
+ | Stream.Stream<AssistantLoop.Event, unknown, R>
208
+ | Effect.Effect<Stream.Stream<AssistantLoop.Event, unknown, R>, unknown, RE>
209
+ ): Stream.Stream<AssistantLoop.Event, unknown, R | RE> => (Effect.isEffect(result) ? Stream.unwrap(result) : result)
210
+
211
+ /**
212
+ * Identifies an assistant's service in a context. Each assistant has its own identifier, keyed by
213
+ * its name.
214
+ *
215
+ * @category models
216
+ * @since 0.1.0
217
+ */
218
+ export interface Identifier<Name extends string> {
219
+ readonly "~effect-assistant/Assistant": Name
220
+ }
221
+
222
+ /**
223
+ * Identifies, in a context, the chat that an assistant's turns run in.
224
+ *
225
+ * @category models
226
+ * @since 0.1.0
227
+ */
228
+ export interface ChatIdentifier<Name extends string> {
229
+ readonly "~effect-assistant/Assistant/Chat": Name
230
+ }
231
+
232
+ /**
233
+ * Represents the chat that a turn of this assistant runs in, typed with the assistant's events.
234
+ *
235
+ * Use `yield* MyAssistant.Chat` in the handler, in the handler's build, or in a tool that its
236
+ * turns run. Code that doesn't know the assistant uses `CurrentChat` instead.
237
+ *
238
+ * @see {@link CurrentChat} for the same chat without the assistant's event types
239
+ *
240
+ * @category models
241
+ * @since 0.1.0
242
+ */
243
+ export interface Chat<Events = never> {
244
+ readonly chatId: ChatId
245
+ /**
246
+ * The chat's entries so far, in order. They include the library's events and the assistant's
247
+ * own events, typed.
248
+ */
249
+ readonly entries: Effect.Effect<ReadonlyArray<Entry<Events>>>
250
+ /**
251
+ * Appends events after the chat's last entry and sends them to the chat's subscribers. The
252
+ * events can be the library's or the assistant's own, such as a summary. An event that wakes a
253
+ * turn is handled by the next turn.
254
+ */
255
+ append(...events: ReadonlyArray<AssistantEvent.AssistantEvent | Events>): Effect.Effect<void>
256
+ }
257
+
258
+ /**
259
+ * Represents the service that `yield* MyAssistant` returns. It gives code outside a chat, such as
260
+ * a server or a workflow, access to the assistant's chats. Code that runs inside a chat uses
261
+ * `CurrentChat` instead.
262
+ *
263
+ * @category models
264
+ * @since 0.1.0
265
+ */
266
+ export interface Service<Events = never> {
267
+ /**
268
+ * Stores an input or one of the assistant's own events, then returns. Examples are a user's
269
+ * message or block answer, a workflow's progress, or the draft the user opened. The assistant
270
+ * works on events that wake a turn in the background.
271
+ *
272
+ * Fails with `GetChatError` when the app's `Chats` service reports that the chat can't be used,
273
+ * because the app doesn't know the chat or has deleted it.
274
+ */
275
+ // Method syntax: a service for these events is one for any (`Service<any>`), for helpers over assistants.
276
+ submit(chatId: ChatId, input: AssistantEvent.Input | Events): Effect.Effect<void, Chats.GetChatError>
277
+ /**
278
+ * Streams the chat's stored entries, then live ones, with the assistant's own events decoded.
279
+ * A reader that already has the earlier entries passes `after` and receives only the entries
280
+ * after that `seq`.
281
+ *
282
+ * For a chat client, pipe the whole log into `ChatView.updates(MyAssistant, { after })`, which
283
+ * streams the chat as its user sees it. Its `after` goes there, not here: the view of a message
284
+ * depends on earlier entries.
285
+ */
286
+ readonly subscribe: (
287
+ chatId: ChatId,
288
+ options?: { readonly after?: number | undefined }
289
+ ) => Stream.Stream<Entry<Events>>
290
+ /**
291
+ * Deletes a chat by storing a `ChatDeleted` event and stopping everything that works in it.
292
+ *
293
+ * **Details**
294
+ *
295
+ * A turn or specialist run working in the chat stops, and subscribers' streams end with the
296
+ * `ChatDeleted` event. After that the chat stores nothing more: a later input, a run's report,
297
+ * or a workflow's event for it is dropped without an error. The log is kept, and removing it is
298
+ * up to the app. Deleting a chat a second time does nothing.
299
+ *
300
+ * When the app's `Chats` service reports a chat as deleted, the assistant deletes it in the same
301
+ * way the next time it checks. It checks before an input is stored and before each model call.
302
+ */
303
+ readonly delete: (chatId: ChatId) => Effect.Effect<void>
304
+ }
305
+
306
+ /**
307
+ * Represents an assistant's definition.
308
+ *
309
+ * The definition holds what callers and the UI need to know about the assistant, builds its
310
+ * handler's layer, and is the key of its service (`yield* MyAssistant`). `Events` is the type of
311
+ * the app's own events that the assistant registered.
312
+ *
313
+ * @category models
314
+ * @since 0.1.0
315
+ */
316
+ export interface Assistant<
317
+ Name extends string = string,
318
+ Specs extends Specialist.Any = never,
319
+ Events = never
320
+ > extends Context.Service<Identifier<Name>, Service<Events>> {
321
+ readonly [TypeId]: typeof TypeId
322
+ readonly name: Name
323
+ readonly blocks: BlockKit.BlockKit | undefined
324
+ /** The specialists the assistant can hand work to. */
325
+ readonly specialists: ReadonlyArray<Specs>
326
+ /** The app's own events the assistant registered. */
327
+ readonly events: ReadonlyArray<AppEvent.Any>
328
+ /**
329
+ * Creates a layer that registers the assistant's handler with the engine and provides the
330
+ * assistant's service.
331
+ *
332
+ * **Details**
333
+ *
334
+ * `build` is the handler, or an Effect that returns it. The handler may use the turn services,
335
+ * `LanguageModel`, its tools' handlers (which the layer then requires), and services that its
336
+ * build Effect required (see `Specialist.CheckServices`). The layer requires the
337
+ * `AssistantEngine`, the `LanguageModel`, and each specialist's handler
338
+ * (`Research.toLayer(...)`).
339
+ *
340
+ * **Example** (Providing an assistant's handler)
341
+ *
342
+ * ```ts
343
+ * import { Assistant, AssistantEngine, ChatLog } from "effect-assistant"
344
+ * import type * as LanguageModel from "effect/ai/LanguageModel"
345
+ * import * as Layer from "effect/Layer"
346
+ *
347
+ * declare const ModelLive: Layer.Layer<LanguageModel.LanguageModel>
348
+ *
349
+ * export const MyAssistant = Assistant.make("my-assistant")
350
+ *
351
+ * const MyAssistantLive = MyAssistant.toLayer((turn) =>
352
+ * turn.loop({ instructions: "You help a small team. Be brief." })
353
+ * )
354
+ *
355
+ * const AppLive = MyAssistantLive.pipe(
356
+ * Layer.provide(ModelLive),
357
+ * Layer.provideMerge(AssistantEngine.layerMemory),
358
+ * Layer.provideMerge(ChatLog.layerMemory)
359
+ * )
360
+ * ```
361
+ */
362
+ readonly toLayer: <R = never, RE = never, E = never, RX = never>(
363
+ build: (Handler<R, RE, Events> | Effect.Effect<Handler<R, RE, Events>, E, RX>) &
364
+ Specialist.CheckServices<Exclude<HandlerServices<R | RE>, RX | ChatIdentifier<Name>>>
365
+ ) => Layer.Layer<
366
+ Identifier<Name>,
367
+ never,
368
+ | AssistantEngine
369
+ | LanguageModel.LanguageModel
370
+ | Specialist.HandlerOf<Specs>
371
+ | Exclude<RX, Scope.Scope | CurrentChat | ChatIdentifier<Name>>
372
+ | Specialist.ToolHandlers<R | RE>
373
+ >
374
+ /**
375
+ * A layer that provides the assistant's service without its handler.
376
+ *
377
+ * **When to use**
378
+ *
379
+ * Use it in a process that submits to chats that another process runs, such as a web server in
380
+ * front of a cluster.
381
+ *
382
+ * **Example** (Serving clients in front of a cluster)
383
+ *
384
+ * ```ts
385
+ * import { Assistant, AssistantEngine } from "effect-assistant"
386
+ * import * as Layer from "effect/Layer"
387
+ *
388
+ * export const MyAssistant = Assistant.make("my-assistant")
389
+ *
390
+ * // Requires `ChatLog` and `Sharding`. The cluster's runners run the chats.
391
+ * const MyAssistantClient = MyAssistant.layerClient.pipe(
392
+ * Layer.provide(AssistantEngine.layerCluster)
393
+ * )
394
+ * ```
395
+ */
396
+ readonly layerClient: Layer.Layer<Identifier<Name>, never, AssistantEngine>
397
+ /**
398
+ * The key of the chat that the assistant's turns run in, typed with its events. See
399
+ * {@link Chat}.
400
+ */
401
+ readonly Chat: Context.Service<ChatIdentifier<Name>, Chat<Events>>
402
+ /** A schema that encodes the chat's entries to JSON, including the assistant's own events. */
403
+ readonly Entry: Schema.Codec<Entry<Events>, Schema.Json>
404
+ }
405
+
406
+ /**
407
+ * Represents any assistant, for use in generic constraints.
408
+ *
409
+ * @category utility types
410
+ * @since 0.1.0
411
+ */
412
+ export type Any = Assistant<any, any, any>
413
+
414
+ /**
415
+ * Returns `true` when the value is an assistant's definition, created with {@link make}.
416
+ *
417
+ * @category guards
418
+ * @since 0.1.0
419
+ */
420
+ export const isAssistant = (u: unknown): u is Any => Predicate.hasProperty(u, TypeId)
421
+
422
+ function entrySchema<E>(events: ReadonlyArray<AppEvent.Any>): Schema.Codec<Entry<E>, Schema.Json>
423
+ // The overload states the decoded type: the library's events, or one of these schemas' types.
424
+ function entrySchema(events: ReadonlyArray<AppEvent.Any>): Schema.Top {
425
+ return Schema.toCodecJson(
426
+ Schema.Struct({
427
+ chatId: ChatId,
428
+ seq: Schema.Int,
429
+ at: Schema.Finite,
430
+ event: Schema.Union([AssistantEvent.AssistantEvent, ...events.map((event) => event.schema)])
431
+ })
432
+ )
433
+ }
434
+
435
+ /**
436
+ * Creates an assistant definition from its name, its UI blocks, its specialists, and the app's own
437
+ * events it stores (see `AppEvent`).
438
+ *
439
+ * **Details**
440
+ *
441
+ * The definition holds no code that runs turns. Provide a handler with its `toLayer`, then yield
442
+ * the definition to get the assistant's {@link Service}.
443
+ *
444
+ * **Example** (Defining an assistant and submitting a message)
445
+ *
446
+ * ```ts
447
+ * import { Assistant, AssistantEvent, ChatId } from "effect-assistant"
448
+ * import * as Effect from "effect/Effect"
449
+ *
450
+ * export const MyAssistant = Assistant.make("my-assistant")
451
+ *
452
+ * const program = Effect.gen(function* () {
453
+ * const assistant = yield* MyAssistant
454
+ * const chatId = yield* ChatId.make()
455
+ *
456
+ * yield* assistant.submit(chatId, AssistantEvent.UserMessage.make({ text: "Weather in Oslo?" }))
457
+ * })
458
+ * ```
459
+ *
460
+ * @see {@link Assistant} for the definition's members
461
+ * @see {@link Service} for what `yield* MyAssistant` returns
462
+ *
463
+ * @category constructors
464
+ * @since 0.1.0
465
+ */
466
+ export const make = <
467
+ const Name extends string,
468
+ const Specs extends ReadonlyArray<Specialist.Any> = readonly [],
469
+ const Events extends ReadonlyArray<AppEvent.Any> = readonly []
470
+ >(
471
+ name: Name,
472
+ options?: {
473
+ /** UI blocks the assistant can add to its messages. */
474
+ readonly blocks?: BlockKit.BlockKit | undefined
475
+ /**
476
+ * The specialists the assistant can hand work to. Each specialist's `toLayer` must be
477
+ * provided to the assistant's layer.
478
+ */
479
+ readonly specialists?: Specs | undefined
480
+ /**
481
+ * The app's own events, each created with `AppEvent.make(schema, { wakes, render })`.
482
+ * `submit` and the chat's `append` store them, and the chat's `entries` read them back,
483
+ * decoded. Events that wake a turn are inputs, and the model reads each event that has a
484
+ * `render`.
485
+ */
486
+ readonly events?: Events | undefined
487
+ }
488
+ ): Assistant<Name, Specs[number], AppEvent.Type<Events[number]>> => {
489
+ const events = options?.events ?? []
490
+
491
+ const definition: Turns.Definition = {
492
+ name,
493
+ blocks: options?.blocks,
494
+ specialists: options?.specialists ?? [],
495
+ app: AppEvents.make(events)
496
+ }
497
+
498
+ type OwnEvents = AppEvent.Type<Events[number]>
499
+
500
+ const Key = Context.Service<Identifier<Name>, Service<OwnEvents>>(`effect-assistant/Assistant/${name}`)
501
+
502
+ // The app's own events go to the engine encoded, as they're stored; the library's go unchanged.
503
+ const stored = (
504
+ input: AssistantEvent.Input | OwnEvents
505
+ ): Effect.Effect<AssistantEvent.Input | AssistantEvent.StoredAppEvent> =>
506
+ AssistantEvent.isInput(input) ? Effect.succeed(input) : AppEvents.encode(definition.app, input)
507
+
508
+ const ChatKey = Context.Service<ChatIdentifier<Name>, Chat<OwnEvents>>(`effect-assistant/Assistant/${name}/Chat`)
509
+
510
+ // On the engine and the app's `Chats` the layer was given, so every caller sees the same ones.
511
+ const makeService = Effect.gen(function* () {
512
+ const engine = yield* AssistantEngine
513
+ const chats = yield* Chats.Chats
514
+
515
+ return Key.of({
516
+ submit: Effect.fn("Assistant.submit")(function* (chatId: ChatId, input: AssistantEvent.Input | OwnEvents) {
517
+ // Checked here, where the caller can handle it: the chat checks again when it stores the input.
518
+ yield* chats.get(chatId)
519
+ yield* engine.submit(name, chatId, yield* stored(input))
520
+ }),
521
+ subscribe: (chatId, options) =>
522
+ engine
523
+ .subscribe(name, chatId, options?.after ?? 0)
524
+ .pipe(
525
+ Stream.filterMap(
526
+ Filter.fromPredicateOption((entry: AssistantEvent.Entry) => AppEvents.decodeEntry(definition.app, entry))
527
+ )
528
+ ),
529
+ delete: (chatId) => engine.delete(name, chatId)
530
+ })
531
+ })
532
+
533
+ // The signature checks what the handler uses (`CheckServices`); the layer's services, which the build
534
+ // required, are provided to it and to every turn's stream, so what's left is the turn services.
535
+ function toLayer<R = never, RE = never, E = never, RX = never>(
536
+ build: (Handler<R, RE, OwnEvents> | Effect.Effect<Handler<R, RE, OwnEvents>, E, RX>) &
537
+ Specialist.CheckServices<Exclude<HandlerServices<R | RE>, RX | ChatIdentifier<Name>>>
538
+ ): Layer.Layer<
539
+ Identifier<Name>,
540
+ never,
541
+ | AssistantEngine
542
+ | LanguageModel.LanguageModel
543
+ | Specialist.HandlerOf<Specs[number]>
544
+ | Exclude<RX, Scope.Scope | CurrentChat | ChatIdentifier<Name>>
545
+ | Specialist.ToolHandlers<R | RE>
546
+ >
547
+ function toLayer(
548
+ build: Handler<any, any, any> | Effect.Effect<Handler<any, any, any>, unknown, any>
549
+ ): Layer.Layer<Identifier<Name>, never, any> {
550
+ return Layer.effect(
551
+ Key,
552
+ Effect.gen(function* () {
553
+ const context = yield* Effect.context()
554
+
555
+ const registration = yield* Turns.register(definition, {
556
+ // The build runs per chat activation, in the chat; here it only keeps the layer's services for it.
557
+ open: (typed) =>
558
+ Effect.gen(function* () {
559
+ // The chat's entries are already decoded with this assistant's events.
560
+ const chat = ChatKey.of({ chatId: typed.chatId, entries: typed.entries, append: typed.append })
561
+ const handler = Effect.isEffect(build) ? yield* Effect.provideService(build, ChatKey, chat) : build
562
+
563
+ return (turn: Turn<any>) =>
564
+ Stream.provideContext(toStream(handler(turn)), context).pipe(Stream.provideService(ChatKey, chat))
565
+ }).pipe(Effect.provideContext(context), Effect.orDie)
566
+ })
567
+
568
+ yield* (yield* AssistantEngine).register(registration)
569
+
570
+ return yield* makeService
571
+ })
572
+ )
573
+ }
574
+
575
+ // The key is a function, whose own `name` is read-only: the assistant's name replaces it.
576
+ const named = Object.defineProperty(Key, "name", { value: name, writable: true })
577
+
578
+ return Object.assign(named, {
579
+ [TypeId]: TypeId,
580
+ name,
581
+ blocks: options?.blocks,
582
+ specialists: options?.specialists ?? [],
583
+ events,
584
+ toLayer,
585
+ layerClient: Layer.effect(Key, makeService),
586
+ Chat: ChatKey,
587
+ Entry: entrySchema<OwnEvents>(events)
588
+ })
589
+ }