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,355 @@
1
+ /**
2
+ * Defines specialists, which are focused agents an assistant hands work to in the background.
3
+ *
4
+ * A specialist has a definition, created with {@link make}, that gives its name and when to use
5
+ * it, and a handler, provided with its `toLayer`, that works on a {@link Brief} as a loop. The
6
+ * {@link Runner} service starts, steers, and stops a chat's specialist runs.
7
+ *
8
+ * @since 0.1.0
9
+ */
10
+ import type * as AiError from "effect/ai/AiError"
11
+ import type * as LanguageModel from "effect/ai/LanguageModel"
12
+ import type * as Prompt from "effect/ai/Prompt"
13
+ import type * as Tool from "effect/ai/Tool"
14
+ import type * as Toolkit from "effect/ai/Toolkit"
15
+ import * as Context from "effect/Context"
16
+ import * as Effect from "effect/Effect"
17
+ import * as Layer from "effect/Layer"
18
+ import * as Schema from "effect/Schema"
19
+ import type * as Scope from "effect/Scope"
20
+ import * as Stream from "effect/Stream"
21
+ import type * as AssistantLoop from "./AssistantLoop.ts"
22
+ import type { ChatId } from "./ChatId.ts"
23
+ import type { CurrentChat } from "./CurrentChat.ts"
24
+ import type * as Instructions from "./Instructions.ts"
25
+ import type { RunId } from "./RunId.ts"
26
+ import type { Skill } from "./Skill.ts"
27
+
28
+ const TypeId = "~effect-assistant/Specialist"
29
+
30
+ /**
31
+ * Represents a specialist's definition.
32
+ *
33
+ * @category models
34
+ * @since 0.1.0
35
+ */
36
+ export interface Specialist<Name extends string = string> {
37
+ readonly [TypeId]: typeof TypeId
38
+ readonly name: Name
39
+ /** When to use the specialist. The assistant sees this. */
40
+ readonly description: string
41
+ /** The key of the specialist's handler, as `toLayer` provides it. */
42
+ readonly Handler: Context.Key<SpecialistHandler<Name>, Built>
43
+ /**
44
+ * Provides the specialist's handler. The handler may use the {@link RunServices} and the
45
+ * services its build Effect required (see {@link CheckServices}).
46
+ */
47
+ readonly toLayer: <R = never, RE = never, E = never, RX = never>(
48
+ build: (Handler<R, RE> | Effect.Effect<Handler<R, RE>, E, RX>) &
49
+ CheckServices<Exclude<R | RE, RunServices | RX | Tool.Handler<string>>>
50
+ ) => Layer.Layer<SpecialistHandler<Name>, never, Exclude<RX, Scope.Scope | CurrentChat> | ToolHandlers<R | RE>>
51
+ }
52
+
53
+ /**
54
+ * Represents the check `toLayer` applies to the services a handler uses.
55
+ *
56
+ * **Details**
57
+ *
58
+ * Every service a handler uses must be one the run (or turn) provides, or one its build Effect
59
+ * required, which the layer provides. A handler that uses any other service gets this type
60
+ * error, which names those services.
61
+ *
62
+ * @category utility types
63
+ * @since 0.1.0
64
+ */
65
+ export type CheckServices<Undeclared> = [Undeclared] extends [never]
66
+ ? unknown
67
+ : {
68
+ readonly "~undeclaredServices": {
69
+ readonly error: "The handler uses services that neither the turn provides nor its build Effect requires"
70
+ readonly services: Undeclared
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Extracts the tool handlers (`Toolkit.toLayer`) that a handler's loop uses.
76
+ *
77
+ * They are requirements of the layer, so `Layer.provide(MyToolsLive)` provides them, and a
78
+ * missing one is a type error.
79
+ *
80
+ * @category utility types
81
+ * @since 0.1.0
82
+ */
83
+ export type ToolHandlers<R> = Extract<R, Tool.Handler<string>>
84
+
85
+ /**
86
+ * Represents any specialist.
87
+ *
88
+ * @category utility types
89
+ * @since 0.1.0
90
+ */
91
+ export type Any = Specialist<any>
92
+
93
+ /**
94
+ * Identifies a specialist's handler in a context.
95
+ *
96
+ * @category models
97
+ * @since 0.1.0
98
+ */
99
+ export interface SpecialistHandler<Name extends string> {
100
+ readonly _SpecialistHandler: Name
101
+ }
102
+
103
+ /**
104
+ * Extracts the handlers that these specialists need.
105
+ *
106
+ * @category utility types
107
+ * @since 0.1.0
108
+ */
109
+ export type HandlerOf<S> = S extends Specialist<infer Name> ? SpecialistHandler<Name> : never
110
+
111
+ /**
112
+ * The services a run provides to its handler, which are its chat, the chat's runner, and the
113
+ * model.
114
+ *
115
+ * @category utility types
116
+ * @since 0.1.0
117
+ */
118
+ export type RunServices = CurrentChat | Runner | LanguageModel.LanguageModel
119
+
120
+ /**
121
+ * Represents the options for `run.loop`.
122
+ *
123
+ * @category models
124
+ * @since 0.1.0
125
+ */
126
+ export interface LoopOptions<T extends Record<string, Tool.Any> = Record<never, never>, RS = never> {
127
+ readonly instructions: string | Instructions.Resolved
128
+ /** The loop's tools, as a native toolkit whose handlers come from the layer. */
129
+ readonly toolkit?: Toolkit.Toolkit<T> | undefined
130
+ /** Playbooks, listed as a catalog in the prompt and loaded with `load_skill` when one matches. */
131
+ readonly skills?: ReadonlyArray<Skill<RS>> | undefined
132
+ /** The model calls allowed per brief. Defaults to 8. */
133
+ readonly maxSteps?: number | undefined
134
+ }
135
+
136
+ /**
137
+ * Represents one run of a specialist on a brief, as its handler sees it.
138
+ *
139
+ * @category models
140
+ * @since 0.1.0
141
+ */
142
+ export interface Run {
143
+ readonly runId: RunId
144
+ /** The chat the run belongs to, which is the chat of the turn that spawned it. */
145
+ readonly chatId: ChatId
146
+ readonly brief: Brief
147
+ /**
148
+ * The run's history, with the brief, what the run did so far, and the messages sent to it since
149
+ * (follow-ups and steering). Read it before every model call, as the loop does.
150
+ */
151
+ readonly history: Effect.Effect<ReadonlyArray<Prompt.Message>>
152
+ /** Runs the agent loop over the run's history, with the given instructions, tools, and skills. */
153
+ readonly loop: <T extends Record<string, Tool.Any> = Record<never, never>, RS = never>(
154
+ options: LoopOptions<T, RS>
155
+ ) => Stream.Stream<
156
+ AssistantLoop.Event,
157
+ AiError.AiError,
158
+ LanguageModel.LanguageModel | AssistantLoop.ToolkitServices<T> | RS
159
+ >
160
+ }
161
+
162
+ /**
163
+ * Represents a specialist's handler, which works on a brief and returns a stream of what the loop
164
+ * did.
165
+ *
166
+ * It may return the stream from an Effect. `R` is the services the stream needs, and `RE` the
167
+ * services the Effect needs.
168
+ *
169
+ * @category models
170
+ * @since 0.1.0
171
+ */
172
+ export type Handler<R = never, RE = R> = (
173
+ run: Run
174
+ ) =>
175
+ | Stream.Stream<AssistantLoop.Event, unknown, R>
176
+ | Effect.Effect<Stream.Stream<AssistantLoop.Event, unknown, R>, unknown, RE>
177
+
178
+ /**
179
+ * Represents a specialist's handler as its layer provides it.
180
+ *
181
+ * `open` runs the build for a chat (once per chat activation, see `CurrentChat`) and returns
182
+ * the handler with the layer's services already provided.
183
+ *
184
+ * @category models
185
+ * @since 0.1.0
186
+ */
187
+ export interface Built {
188
+ readonly open: Effect.Effect<
189
+ (run: Run) => Stream.Stream<AssistantLoop.Event, unknown, RunServices>,
190
+ never,
191
+ Scope.Scope | CurrentChat
192
+ >
193
+ }
194
+
195
+ const toStream = <R, RE>(
196
+ result:
197
+ | Stream.Stream<AssistantLoop.Event, unknown, R>
198
+ | Effect.Effect<Stream.Stream<AssistantLoop.Event, unknown, R>, unknown, RE>
199
+ ): Stream.Stream<AssistantLoop.Event, unknown, R | RE> => (Effect.isEffect(result) ? Stream.unwrap(result) : result)
200
+
201
+ /**
202
+ * Creates a specialist from its name and a description of when to use it.
203
+ *
204
+ * **Details**
205
+ *
206
+ * The specialist's `toLayer` provides its handler, which is usually `run.loop` with the
207
+ * specialist's own instructions and tools. List the specialist in `Assistant.make`'s
208
+ * `specialists`, and provide its layer to the assistant's.
209
+ *
210
+ * **Example** (Defining a research specialist)
211
+ *
212
+ * ```ts
213
+ * import { Assistant, Specialist } from "effect-assistant"
214
+ * import * as Layer from "effect/Layer"
215
+ *
216
+ * const Research = Specialist.make("research", {
217
+ * description: "Researches past campaigns and orders. Give it the question and any context."
218
+ * })
219
+ *
220
+ * const ResearchLive = Research.toLayer((run) =>
221
+ * run.loop({
222
+ * instructions: "Answer with the findings and numbers the colleague needs, in a few lines.",
223
+ * toolkit: ResearchTools
224
+ * })
225
+ * ).pipe(Layer.provide(ResearchToolsLive))
226
+ *
227
+ * export const MyAssistant = Assistant.make("my-assistant", { specialists: [Research] })
228
+ *
229
+ * const MyAssistantLive = MyAssistant.toLayer((turn) => turn.loop({ instructions })).pipe(
230
+ * Layer.provide(ResearchLive)
231
+ * )
232
+ * ```
233
+ *
234
+ * @category constructors
235
+ * @since 0.1.0
236
+ */
237
+ export const make = <const Name extends string>(
238
+ name: Name,
239
+ options: {
240
+ /** When to use it. The assistant sees this. */
241
+ readonly description: string
242
+ }
243
+ ): Specialist<Name> => {
244
+ const Handler = Context.Service<SpecialistHandler<Name>, Built>(`effect-assistant/Specialist/${name}`)
245
+
246
+ // The signature checks what the handler uses (`CheckServices`); the layer's services, which the
247
+ // build required, are provided to it and to every run's stream, so what's left is the run
248
+ // services.
249
+ function toLayer<R = never, RE = never, E = never, RX = never>(
250
+ build: (Handler<R, RE> | Effect.Effect<Handler<R, RE>, E, RX>) &
251
+ CheckServices<Exclude<R | RE, RunServices | RX | Tool.Handler<string>>>
252
+ ): Layer.Layer<SpecialistHandler<Name>, never, Exclude<RX, Scope.Scope | CurrentChat> | ToolHandlers<R | RE>>
253
+ function toLayer(
254
+ build: Handler<any, any> | Effect.Effect<Handler<any, any>, unknown, any>
255
+ ): Layer.Layer<SpecialistHandler<Name>, never, any> {
256
+ return Layer.effect(
257
+ Handler,
258
+ Effect.gen(function* () {
259
+ const context = yield* Effect.context()
260
+
261
+ return {
262
+ // The build runs per chat activation, in the chat; here it only keeps the layer's
263
+ // services for it.
264
+ open: Effect.gen(function* () {
265
+ const handler = Effect.isEffect(build) ? yield* build : build
266
+
267
+ return (run: Run) => Stream.provideContext(toStream(handler(run)), context)
268
+ }).pipe(Effect.provideContext(context), Effect.orDie)
269
+ }
270
+ })
271
+ )
272
+ }
273
+
274
+ return { [TypeId]: TypeId, name, description: options.description, Handler, toLayer }
275
+ }
276
+
277
+ /**
278
+ * Schema for the brief the assistant gives a specialist.
279
+ *
280
+ * Only `goal` is required.
281
+ *
282
+ * @category schemas
283
+ * @since 0.1.0
284
+ */
285
+ export const Brief = Schema.Struct({
286
+ goal: Schema.String.annotate({ description: "What to find out or do." }),
287
+ context: Schema.optional(Schema.String).annotate({
288
+ description: "What it needs to know. It can't see this chat."
289
+ }),
290
+ successCriteria: Schema.optional(Schema.String).annotate({ description: "When it is done." }),
291
+ report: Schema.optional(Schema.String).annotate({
292
+ description: "What to put in the answer, and how."
293
+ }),
294
+ limits: Schema.optional(Schema.String).annotate({
295
+ description: "Hard limits for this job, like read-only or a deadline."
296
+ })
297
+ })
298
+
299
+ /**
300
+ * Represents the brief the assistant gives a specialist.
301
+ *
302
+ * @category models
303
+ * @since 0.1.0
304
+ */
305
+ export type Brief = typeof Brief.Type
306
+
307
+ /**
308
+ * Represents a request to start a specialist run in the runner's chat.
309
+ *
310
+ * @category models
311
+ * @since 0.1.0
312
+ */
313
+ export interface SpawnRequest {
314
+ /** The specialist's name. */
315
+ readonly specialist: string
316
+ readonly brief: Brief
317
+ }
318
+
319
+ /**
320
+ * Represents why a run request can't be carried out.
321
+ *
322
+ * **Details**
323
+ *
324
+ * The `reason` says which request failed: `"UnknownSpecialist"` when `spawn` names no specialist
325
+ * of the assistant, `"RunNotFound"` when `message` names a run that is gone, and `"NotRunning"`
326
+ * when `stop` names a run that isn't running. The `message` is written for the model.
327
+ *
328
+ * @category errors
329
+ * @since 0.1.0
330
+ */
331
+ export class SpecialistError extends Schema.Error<SpecialistError>("effect-assistant/Specialist/SpecialistError")({
332
+ _tag: Schema.tag("SpecialistError"),
333
+ reason: Schema.Literals(["UnknownSpecialist", "RunNotFound", "NotRunning"]),
334
+ message: Schema.String
335
+ }) {}
336
+
337
+ /**
338
+ * Service that runs a chat's specialists and keeps finished runs warm for follow-ups.
339
+ *
340
+ * The engine provides it to every turn. The specialist tools use it, and a handler can too.
341
+ *
342
+ * @category services
343
+ * @since 0.1.0
344
+ */
345
+ export class Runner extends Context.Service<
346
+ Runner,
347
+ {
348
+ /** Starts a run and returns its id. */
349
+ readonly spawn: (request: SpawnRequest) => Effect.Effect<RunId, SpecialistError>
350
+ /** Steers a running run, or continues a finished one. */
351
+ readonly message: (runId: RunId, text: string) => Effect.Effect<"steered" | "continued", SpecialistError>
352
+ /** Interrupts a running run. */
353
+ readonly stop: (runId: RunId) => Effect.Effect<void, SpecialistError>
354
+ }
355
+ >()("effect-assistant/Specialist/Runner") {}
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Turns a catalog toolkit into two tools, so the prompt doesn't carry every tool definition.
3
+ *
4
+ * `search_tools` finds tools by what the model needs to do and returns their names, descriptions,
5
+ * and input schemas. `execute_tool` validates an input and runs one of them. {@link make} returns
6
+ * the two tools as a native toolkit, and {@link layer} their handlers.
7
+ *
8
+ * @since 0.1.0
9
+ */
10
+ import * as Response from "effect/ai/Response"
11
+ import * as Tool from "effect/ai/Tool"
12
+ import * as Toolkit from "effect/ai/Toolkit"
13
+ import * as Effect from "effect/Effect"
14
+ import * as Schema from "effect/Schema"
15
+ import { AssistantToolCall } from "./AssistantToolCall.ts"
16
+ import { toJson } from "./internal/schema.ts"
17
+ import * as Toolkits from "./internal/toolkit.ts"
18
+
19
+ const words = (text: string): ReadonlySet<string> =>
20
+ new Set(
21
+ text
22
+ .toLowerCase()
23
+ .split(/[^a-z0-9]+/)
24
+ .filter((word) => word.length >= 3)
25
+ )
26
+
27
+ const score = (query: ReadonlySet<string>, text: string): number => {
28
+ const target = [...words(text)]
29
+
30
+ const matches = (word: string) =>
31
+ target.some((t) => t === word || (word.length >= 4 && (t.startsWith(word) || word.startsWith(t))))
32
+
33
+ return [...query].filter(matches).length
34
+ }
35
+
36
+ // Everything when nothing matches, so the model can pick or search again.
37
+ const search = (tools: ReadonlyArray<Tool.Any>, query: string, limit: number): ReadonlyArray<Tool.Any> => {
38
+ const queryWords = words(query)
39
+
40
+ const ranked = tools
41
+ .flatMap((tool) => {
42
+ const hits = score(queryWords, `${tool.name} ${Tool.getDescription(tool) ?? ""}`)
43
+
44
+ return hits > 0 ? [{ tool, score: hits }] : []
45
+ })
46
+ .sort((a, b) => b.score - a.score)
47
+
48
+ return (ranked.length > 0 ? ranked.map((hit) => hit.tool) : tools).slice(0, limit)
49
+ }
50
+
51
+ const SearchTools = Tool.make("search_tools", {
52
+ description:
53
+ "Search for more tools by what you need to do. Returns their names, descriptions, and input schemas; call one with execute_tool.",
54
+ parameters: Schema.Struct({
55
+ query: Schema.String.annotate({ description: "What you need to do, in a few words." })
56
+ }),
57
+ success: Schema.Array(
58
+ Schema.Struct({
59
+ name: Schema.String,
60
+ description: Schema.optional(Schema.String),
61
+ inputSchema: Schema.Json
62
+ })
63
+ )
64
+ })
65
+
66
+ const ExecuteTool = Tool.make("execute_tool", {
67
+ description: "Run a tool found with search_tools. Pass its name and an input matching its input schema.",
68
+ parameters: Schema.Struct({
69
+ name: Schema.String,
70
+ input: Schema.Record(Schema.String, Schema.Json)
71
+ }),
72
+ success: Schema.Json,
73
+ // The tool's failure is this call's, so the loop sees it failed (a `wait: true` message doesn't
74
+ // end the turn).
75
+ failure: Schema.Json,
76
+ dependencies: [AssistantToolCall]
77
+ })
78
+
79
+ // A tool's requirements are a covariant phantom type, so one that needs nothing is one that needs
80
+ // any.
81
+ const requiring =
82
+ <R>() =>
83
+ <N extends string, C extends (Tool.Any extends Tool.Tool<string, infer K, unknown> ? K : never), R0>(
84
+ tool: Tool.Tool<N, C, R0>
85
+ ): Tool.Tool<N, C, R0 | R> =>
86
+ tool
87
+
88
+ /**
89
+ * Creates the `search_tools` and `execute_tool` tools over a catalog toolkit, as a native toolkit.
90
+ *
91
+ * **Details**
92
+ *
93
+ * Merge the toolkit into the loop's, and provide {@link layer} for the same catalog, which has
94
+ * the tools' handlers. A catalog tool's services are `execute_tool`'s, so the turn provides them
95
+ * as it does for any tool.
96
+ *
97
+ * `execute_tool` doesn't run a tool that needs the user's decision; keep such tools in the loop's
98
+ * toolkit directly.
99
+ *
100
+ * **Example** (Finding rarely used tools when needed)
101
+ *
102
+ * ```ts
103
+ * import { ToolDiscovery } from "effect-assistant"
104
+ * import * as Toolkit from "effect/ai/Toolkit"
105
+ * import * as Layer from "effect/Layer"
106
+ *
107
+ * // OfficeTools is a big toolkit, found when needed. CommonTools stays in the prompt.
108
+ * const OfficeDiscovery = ToolDiscovery.make(OfficeTools)
109
+ * const OfficeDiscoveryLive = ToolDiscovery.layer(OfficeTools).pipe(Layer.provide(OfficeToolsLive))
110
+ *
111
+ * const MyAssistantLive = MyAssistant.toLayer((turn) =>
112
+ * turn.loop({
113
+ * instructions,
114
+ * toolkit: Toolkit.merge(CommonTools, OfficeDiscovery)
115
+ * })
116
+ * ).pipe(Layer.provide([CommonToolsLive, OfficeDiscoveryLive]))
117
+ * ```
118
+ *
119
+ * @see {@link layer} for the tools' handlers
120
+ *
121
+ * @category constructors
122
+ * @since 0.1.0
123
+ */
124
+ export const make = <T extends Record<string, Tool.Any>>(
125
+ // Only its type is used here, for `execute_tool`'s services: `layer` runs its tools.
126
+ _catalog: Toolkit.Toolkit<T>
127
+ ) => Toolkit.make(SearchTools, requiring<Toolkits.ToolServices<T>>()(ExecuteTool))
128
+
129
+ /**
130
+ * Creates the layer with the handlers of the `search_tools` and `execute_tool` tools over a
131
+ * catalog toolkit.
132
+ *
133
+ * **Details**
134
+ *
135
+ * The layer needs the catalog's handlers. It provides the handlers of the toolkit that
136
+ * {@link make} returns for the same catalog: handlers are keyed by tool name, so the two calls
137
+ * don't need to share a value. Give both the same catalog.
138
+ *
139
+ * @see {@link make} for the toolkit and an example
140
+ *
141
+ * @category layers
142
+ * @since 0.1.0
143
+ */
144
+ export const layer = <T extends Record<string, Tool.Any>>(catalog: Toolkit.Toolkit<T>) =>
145
+ make(catalog).toLayer(
146
+ Effect.gen(function* () {
147
+ const resolved = yield* catalog
148
+ const definitions = Object.values(resolved.tools)
149
+
150
+ return {
151
+ search_tools: ({ query }) =>
152
+ Effect.sync(() =>
153
+ search(definitions, query, 5).map((tool) => ({
154
+ name: tool.name,
155
+ description: Tool.getDescription(tool),
156
+ inputSchema: toJson(Tool.getJsonSchema(tool))
157
+ }))
158
+ ),
159
+ execute_tool: Effect.fnUntraced(function* ({ name, input }) {
160
+ // Named after the call that runs it: the same id when a crash runs that call again.
161
+ const outer = yield* AssistantToolCall
162
+
163
+ const call = Response.makePart("tool-call", {
164
+ id: `${outer.callId}:${name}`,
165
+ name,
166
+ params: input,
167
+ providerExecuted: false
168
+ })
169
+
170
+ // A call that needs the user's decision parks only as a call the model made itself,
171
+ // where it can ask for it.
172
+ if (yield* Toolkits.needsDecision(resolved, call, [])) {
173
+ return yield* Effect.fail(
174
+ Toolkits.failed(
175
+ `${name} needs the user's decision, so it can't run through execute_tool.`,
176
+ "NeedsDecision"
177
+ ).result
178
+ )
179
+ }
180
+
181
+ const { outcome, result } = yield* Toolkits.runNative(resolved, call)
182
+
183
+ return outcome === "failure" ? yield* Effect.fail(result) : result
184
+ })
185
+ }
186
+ })
187
+ )
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Defines the failure a tool returns to the model.
3
+ *
4
+ * A {@link ToolFailure} carries a message the model reads and adapts to, such as "No office named
5
+ * Paris", instead of failing the turn. Tools declare it as their `failure` schema and fail with
6
+ * {@link fail}.
7
+ *
8
+ * @since 0.1.0
9
+ */
10
+ import * as Effect from "effect/Effect"
11
+ import * as Schema from "effect/Schema"
12
+
13
+ /**
14
+ * Represents a tool's failure, written for the model.
15
+ *
16
+ * It holds a message that says what went wrong and, optionally, a `reason` the model can tell
17
+ * apart, such as `"NotFound"` or `"OverBudget"`. The library's own reasons are PascalCase, like
18
+ * `"NeedsDecision"`.
19
+ *
20
+ * @category errors
21
+ * @since 0.1.0
22
+ */
23
+ export class ToolFailure extends Schema.Error<ToolFailure>("effect-assistant/ToolFailure/ToolFailure")({
24
+ _tag: Schema.tag("ToolFailure"),
25
+ message: Schema.String,
26
+ reason: Schema.optional(Schema.String)
27
+ }) {}
28
+
29
+ /**
30
+ * Fails a tool's handler with a message for the model.
31
+ *
32
+ * **Details**
33
+ *
34
+ * The tool must declare `failure: ToolFailure.ToolFailure`. The model reads the failure as the
35
+ * call's result, with its `reason` when one is given.
36
+ *
37
+ * **Example** (Failing a tool call the model can correct)
38
+ *
39
+ * ```ts
40
+ * import { ToolFailure } from "effect-assistant"
41
+ * import * as Tool from "effect/ai/Tool"
42
+ * import * as Toolkit from "effect/ai/Toolkit"
43
+ * import * as Effect from "effect/Effect"
44
+ * import * as Schema from "effect/Schema"
45
+ *
46
+ * const budgets = new Map([["Oslo", 1200]])
47
+ *
48
+ * const BudgetTools = Toolkit.make(
49
+ * Tool.make("office_budget", {
50
+ * description: "The budget left for an office.",
51
+ * parameters: Schema.Struct({ office: Schema.String }),
52
+ * success: Schema.Number,
53
+ * failure: ToolFailure.ToolFailure
54
+ * })
55
+ * )
56
+ *
57
+ * const BudgetToolsLive = BudgetTools.toLayer({
58
+ * office_budget: ({ office }) =>
59
+ * Effect.fromNullishOr(budgets.get(office)).pipe(
60
+ * Effect.catch(() => ToolFailure.fail(`No office named ${office}.`, { reason: "NotFound" }))
61
+ * )
62
+ * })
63
+ * ```
64
+ *
65
+ * @category constructors
66
+ * @since 0.1.0
67
+ */
68
+ export const fail = (
69
+ message: string,
70
+ options?: { readonly reason?: string | undefined }
71
+ ): Effect.Effect<never, ToolFailure> =>
72
+ Effect.fail(
73
+ options?.reason === undefined ? new ToolFailure({ message }) : new ToolFailure({ message, reason: options.reason })
74
+ )