effect-assistant 0.3.0 → 0.4.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 (73) hide show
  1. package/dist/Assistant.d.ts +5 -11
  2. package/dist/Assistant.d.ts.map +1 -1
  3. package/dist/Assistant.js.map +1 -1
  4. package/dist/AssistantEvent.d.ts +55 -7
  5. package/dist/AssistantEvent.d.ts.map +1 -1
  6. package/dist/AssistantEvent.js +61 -7
  7. package/dist/AssistantEvent.js.map +1 -1
  8. package/dist/AssistantLoop.d.ts +4 -2
  9. package/dist/AssistantLoop.d.ts.map +1 -1
  10. package/dist/AssistantLoop.js +4 -2
  11. package/dist/AssistantLoop.js.map +1 -1
  12. package/dist/Compaction.d.ts +1 -1
  13. package/dist/Compaction.d.ts.map +1 -1
  14. package/dist/Compaction.js +13 -6
  15. package/dist/Compaction.js.map +1 -1
  16. package/dist/Skill.d.ts +85 -43
  17. package/dist/Skill.d.ts.map +1 -1
  18. package/dist/Skill.js +93 -44
  19. package/dist/Skill.js.map +1 -1
  20. package/dist/Specialist.d.ts +6 -9
  21. package/dist/Specialist.d.ts.map +1 -1
  22. package/dist/Specialist.js.map +1 -1
  23. package/dist/Timeline.d.ts +40 -6
  24. package/dist/Timeline.d.ts.map +1 -1
  25. package/dist/Timeline.js +74 -15
  26. package/dist/Timeline.js.map +1 -1
  27. package/dist/ToolApproval.d.ts +146 -0
  28. package/dist/ToolApproval.d.ts.map +1 -0
  29. package/dist/ToolApproval.js +201 -0
  30. package/dist/ToolApproval.js.map +1 -0
  31. package/dist/ToolDiscovery.d.ts +6 -4
  32. package/dist/ToolDiscovery.d.ts.map +1 -1
  33. package/dist/ToolDiscovery.js +5 -4
  34. package/dist/ToolDiscovery.js.map +1 -1
  35. package/dist/index.d.ts +3 -2
  36. package/dist/index.js +3 -2
  37. package/dist/internal/chat.js +4 -1
  38. package/dist/internal/chat.js.map +1 -1
  39. package/dist/internal/history.js +28 -3
  40. package/dist/internal/history.js.map +1 -1
  41. package/dist/internal/loop.d.ts +10 -5
  42. package/dist/internal/loop.d.ts.map +1 -1
  43. package/dist/internal/loop.js +54 -21
  44. package/dist/internal/loop.js.map +1 -1
  45. package/dist/internal/specialists.js +9 -16
  46. package/dist/internal/specialists.js.map +1 -1
  47. package/dist/internal/toolkit.d.ts +5 -7
  48. package/dist/internal/toolkit.d.ts.map +1 -1
  49. package/dist/internal/toolkit.js +28 -18
  50. package/dist/internal/toolkit.js.map +1 -1
  51. package/dist/internal/turns.d.ts.map +1 -1
  52. package/dist/internal/turns.js +18 -13
  53. package/dist/internal/turns.js.map +1 -1
  54. package/package.json +1 -1
  55. package/src/Assistant.ts +7 -13
  56. package/src/AssistantEvent.ts +60 -6
  57. package/src/AssistantLoop.ts +4 -2
  58. package/src/Compaction.ts +19 -2
  59. package/src/Skill.ts +134 -57
  60. package/src/Specialist.ts +8 -11
  61. package/src/Timeline.ts +104 -18
  62. package/src/ToolApproval.ts +249 -0
  63. package/src/ToolDiscovery.ts +8 -3
  64. package/src/index.ts +5 -0
  65. package/src/internal/chat.ts +11 -1
  66. package/src/internal/history.ts +58 -3
  67. package/src/internal/loop.ts +120 -34
  68. package/src/internal/specialists.ts +7 -13
  69. package/src/internal/toolkit.ts +47 -30
  70. package/src/internal/turns.ts +24 -7
  71. package/dist/internal/skills.js +0 -33
  72. package/dist/internal/skills.js.map +0 -1
  73. package/src/internal/skills.ts +0 -48
@@ -140,13 +140,30 @@ export class SpecialistFinished extends Schema.TaggedClass<SpecialistFinished>()
140
140
  error: Schema.optional(Schema.String)
141
141
  }) {}
142
142
 
143
+ /**
144
+ * Represents the user's decision on a tool call that waits for their approval: `"approved"`, so
145
+ * the call runs, or `"rejected"`, with an optional short reason the model reads.
146
+ *
147
+ * Its id is fixed per call (see `ToolApproval.approve`), so a decision is stored once. A decision on
148
+ * a call that isn't waiting any more (decided, or expired) isn't stored.
149
+ *
150
+ * @category schemas
151
+ * @since 0.4.0
152
+ */
153
+ export class ApprovalDecision extends Schema.TaggedClass<ApprovalDecision>()("ApprovalDecision", {
154
+ id: InputId,
155
+ callId: Schema.String,
156
+ decision: Schema.Literals(["approved", "rejected"]),
157
+ reason: Schema.optional(Schema.String.check(Schema.isMaxLength(500)))
158
+ }) {}
159
+
143
160
  /**
144
161
  * Schema for every input `Assistant.submit` accepts.
145
162
  *
146
163
  * @category schemas
147
164
  * @since 0.1.0
148
165
  */
149
- export const Input = Schema.Union([UserMessage, BlockAnswer, SpecialistFinished])
166
+ export const Input = Schema.Union([UserMessage, BlockAnswer, SpecialistFinished, ApprovalDecision])
150
167
 
151
168
  /**
152
169
  * Represents an input to an assistant.
@@ -157,7 +174,8 @@ export const Input = Schema.Union([UserMessage, BlockAnswer, SpecialistFinished]
157
174
  export type Input = typeof Input.Type
158
175
 
159
176
  /**
160
- * Schema for the inputs from the chat's user, which are messages and block answers.
177
+ * Schema for the inputs from the chat's user, which are messages, block answers, and decisions on
178
+ * tool calls that wait for their approval.
161
179
  *
162
180
  * A turn owes each one a reply. It is also what a chat client usually may submit, since the app's
163
181
  * events come from the server.
@@ -165,7 +183,7 @@ export type Input = typeof Input.Type
165
183
  * @category schemas
166
184
  * @since 0.1.0
167
185
  */
168
- export const UserInput = Schema.Union([UserMessage, BlockAnswer])
186
+ export const UserInput = Schema.Union([UserMessage, BlockAnswer, ApprovalDecision])
169
187
 
170
188
  /**
171
189
  * Represents an input from the chat's user.
@@ -400,6 +418,36 @@ export class BlocksClosed extends Schema.TaggedClass<BlocksClosed>()("BlocksClos
400
418
  messageIds: Schema.Array(Schema.String)
401
419
  }) {}
402
420
 
421
+ /**
422
+ * Represents a tool call that waits for the user's approval instead of running: its tool needs
423
+ * approval (Effect's `needsApproval`).
424
+ *
425
+ * **Details**
426
+ *
427
+ * It holds the call's id, the tool, and its params, in JSON, as the model wrote them. The model
428
+ * reads it as the call's result, saying it waits for the user; the user's
429
+ * {@link ApprovalDecision} decides it, and an approved call runs then, with the same id.
430
+ *
431
+ * @category schemas
432
+ * @since 0.4.0
433
+ */
434
+ export class ApprovalRequested extends Schema.TaggedClass<ApprovalRequested>()("ApprovalRequested", {
435
+ callId: Schema.String,
436
+ tool: Schema.String,
437
+ params: Schema.Json
438
+ }) {}
439
+
440
+ /**
441
+ * Represents tool calls that waited for the user's approval and won't run: the user moved on, like
442
+ * the ones `ToolApproval.closeStale` expires. The model reads that they weren't approved.
443
+ *
444
+ * @category schemas
445
+ * @since 0.4.0
446
+ */
447
+ export class ApprovalsExpired extends Schema.TaggedClass<ApprovalsExpired>()("ApprovalsExpired", {
448
+ callIds: Schema.Array(Schema.String)
449
+ }) {}
450
+
403
451
  /**
404
452
  * Schema for every event the library defines.
405
453
  *
@@ -419,6 +467,9 @@ export const AssistantEvent = Schema.Union([
419
467
  SpecialistStarted,
420
468
  SpecialistFinished,
421
469
  BlocksClosed,
470
+ ApprovalRequested,
471
+ ApprovalDecision,
472
+ ApprovalsExpired,
422
473
  ChatDeleted
423
474
  ])
424
475
 
@@ -554,7 +605,7 @@ export type Entry = typeof Entry.Type
554
605
 
555
606
  /**
556
607
  * Returns whether an event is one of the library's inputs, which are a user message, a block
557
- * answer, or a finished specialist run.
608
+ * answer, a finished specialist run, or a decision on a tool call waiting for approval.
558
609
  *
559
610
  * The application's events that came in through the assistant's `submit` are inputs too (see
560
611
  * {@link isAppInput}).
@@ -565,7 +616,8 @@ export type Entry = typeof Entry.Type
565
616
  export const isInput = (event: { readonly _tag: string }): event is Input =>
566
617
  Predicate.isTagged(event, "UserMessage") ||
567
618
  Predicate.isTagged(event, "BlockAnswer") ||
568
- Predicate.isTagged(event, "SpecialistFinished")
619
+ Predicate.isTagged(event, "SpecialistFinished") ||
620
+ Predicate.isTagged(event, "ApprovalDecision")
569
621
 
570
622
  /**
571
623
  * Returns whether an input starts a turn, which every input does except a run the assistant
@@ -584,7 +636,9 @@ export const isWaking = (input: Input): boolean =>
584
636
  * @since 0.1.0
585
637
  */
586
638
  export const isUserInput = (input: Input): input is UserInput =>
587
- Predicate.isTagged(input, "UserMessage") || Predicate.isTagged(input, "BlockAnswer")
639
+ Predicate.isTagged(input, "UserMessage") ||
640
+ Predicate.isTagged(input, "BlockAnswer") ||
641
+ Predicate.isTagged(input, "ApprovalDecision")
588
642
 
589
643
  /**
590
644
  * Returns whether an event ends a turn, and so says how far the assistant has read.
@@ -161,8 +161,10 @@ const toolsOf = <T extends Record<string, Tool.Any>>(toolkit: Toolkit.Toolkit<T>
161
161
  * the step's result and decides what comes next, with `AssistantLoop.next(state)` or
162
162
  * `AssistantLoop.stop()`.
163
163
  *
164
- * A tool with Effect's `needsApproval` set fails the loop before the model is called: the
165
- * library doesn't run calls that wait for approval (see the guide on involving the user).
164
+ * In a turn's loop, a call to a tool with Effect's `needsApproval` doesn't run: the step emits an
165
+ * `ApprovalRequested` for it, and ends the turn, which waits for the user (see `ToolApproval`). A
166
+ * call the user approved runs at the start of the next step, before the model is called. A
167
+ * specialist's run can't wait for the user: such a call fails, and the run reads why.
166
168
  *
167
169
  * @category constructors
168
170
  * @since 0.1.0
package/src/Compaction.ts CHANGED
@@ -18,6 +18,8 @@ import * as AssistantActivity from "./AssistantActivity.ts"
18
18
  import * as AssistantEvent from "./AssistantEvent.ts"
19
19
  import * as BlockKit from "./BlockKit.ts"
20
20
  import { userText } from "./internal/history.ts"
21
+ import { toJsonText } from "./internal/schema.ts"
22
+ import * as ToolApproval from "./ToolApproval.ts"
21
23
 
22
24
  /**
23
25
  * Represents the fields a summary event must have: its text and the last `seq` it covers.
@@ -54,7 +56,7 @@ export const inputTokens = (entries: ReadonlyArray<AssistantEvent.AnyEntry>): nu
54
56
 
55
57
  /**
56
58
  * Returns the items that were open at `throughSeq` and are still open: blocks the user hasn't
57
- * answered, and specialists that are working. The result is text for
59
+ * answered, specialists that are working, and tool calls waiting for the user's approval. The result is text for
58
60
  * the model, or `None` when nothing is open.
59
61
  *
60
62
  * @category utils
@@ -100,9 +102,24 @@ export const openItems = (
100
102
  : []
101
103
  )
102
104
 
105
+ const approvals = [...ToolApproval.fromEntries(entries).values()].flatMap((approval) =>
106
+ approval.status === "pending" &&
107
+ opened(
108
+ seqOf(
109
+ (event) =>
110
+ AssistantEvent.isLibraryEvent(event) &&
111
+ Predicate.isTagged(event, "ApprovalRequested") &&
112
+ event.callId === approval.request.callId
113
+ )
114
+ )
115
+ ? [`- your call to ${approval.request.tool} (${approval.request.callId}): ${toJsonText(approval.request.params)}`]
116
+ : []
117
+ )
118
+
103
119
  const sections = [
104
120
  blocks.length > 0 ? `Blocks the user hasn't answered:\n${blocks.join("\n")}` : "",
105
- specialists.length > 0 ? `Specialists still working:\n${specialists.join("\n")}` : ""
121
+ specialists.length > 0 ? `Specialists still working:\n${specialists.join("\n")}` : "",
122
+ approvals.length > 0 ? `Calls waiting for the user's approval:\n${approvals.join("\n")}` : ""
106
123
  ].filter((section) => section.length > 0)
107
124
 
108
125
  return sections.length === 0 ? Option.none() : Option.some(sections.join("\n\n"))
package/src/Skill.ts CHANGED
@@ -1,14 +1,19 @@
1
1
  /**
2
2
  * Defines skills, which are playbooks with step-by-step instructions for a kind of task.
3
3
  *
4
- * A loop lists its skills as a catalog in the prompt, with each skill's name and description
5
- * ({@link withCatalog}). The model loads a skill's instructions with the `load_skill` tool when a
6
- * task matches.
4
+ * A list of skills becomes a toolkit with one tool, `load_skill` ({@link toolkit}). Its
5
+ * description is the catalog, each skill's name and when to use it, and the model calls it to read
6
+ * a skill's instructions when a task matches. Merge it into a loop's toolkit like any other.
7
7
  *
8
8
  * @since 0.1.0
9
9
  */
10
- import type * as Tool from "effect/ai/Tool"
11
- import type * as Instructions from "./Instructions.ts"
10
+ import * as Tool from "effect/ai/Tool"
11
+ import * as AiToolkit from "effect/ai/Toolkit"
12
+ import * as Effect from "effect/Effect"
13
+ import type * as Layer from "effect/Layer"
14
+ import * as Schema from "effect/Schema"
15
+ import * as Instructions from "./Instructions.ts"
16
+ import * as ToolFailure from "./ToolFailure.ts"
12
17
 
13
18
  /**
14
19
  * Represents a skill.
@@ -24,11 +29,6 @@ export interface Skill<R = never> {
24
29
  readonly description: string
25
30
  /** Text, or an Effect resolved each time the skill is loaded. */
26
31
  readonly instructions: Instructions.Instructions<R>
27
- /**
28
- * The tools its instructions use. A loop offers the skill only when its toolkit has every one of
29
- * them, matched by name. Unset, a loop always offers it.
30
- */
31
- readonly tools?: ReadonlyArray<Tool.Any> | undefined
32
32
  }
33
33
 
34
34
  /**
@@ -38,12 +38,7 @@ export interface Skill<R = never> {
38
38
  *
39
39
  * Write the description as a trigger ("Use when…"), since it's all the model sees until it loads
40
40
  * the skill. The instructions are text, or an Effect that is resolved each time the skill is
41
- * loaded.
42
- *
43
- * `tools` lists the tools the instructions use. A loop whose toolkit lacks any of them, matched by
44
- * name, leaves the skill out of its catalog and of `load_skill`, so a loop that gets its tools per
45
- * user (by role, say) never offers a playbook it can't follow. A tool that only `ToolDiscovery`
46
- * finds isn't in the toolkit, so it doesn't count. Without `tools`, every loop offers the skill.
41
+ * loaded. Offer skills to a loop with {@link toolkit}.
47
42
  *
48
43
  * **Example** (Defining a playbook for ordering campaigns)
49
44
  *
@@ -56,30 +51,6 @@ export interface Skill<R = never> {
56
51
  * 2. You need who it should reach and a budget. Ask for what's missing, one question at a time.
57
52
  * 3. When you have both, call start_campaign.`
58
53
  * })
59
- *
60
- * const MyAssistantLive = MyAssistant.toLayer((turn) =>
61
- * turn.loop({ instructions, skills: [orderingCampaigns] })
62
- * )
63
- * ```
64
- *
65
- * **Example** (Offering a skill only with the tools it uses)
66
- *
67
- * ```ts
68
- * import { Skill } from "effect-assistant"
69
- * import * as Tool from "effect/ai/Tool"
70
- * import * as Schema from "effect/Schema"
71
- *
72
- * const StartCampaign = Tool.make("start_campaign", {
73
- * parameters: Schema.Struct({ campaignId: Schema.String, budget: Schema.Finite }),
74
- * success: Schema.String
75
- * })
76
- *
77
- * // Left out of a loop whose toolkit has no start_campaign, like a viewer's.
78
- * const orderingCampaigns = Skill.make("Ordering a campaign", {
79
- * description: "Use when the user wants to order or start a campaign.",
80
- * instructions: "When you know who it should reach and the budget, call start_campaign.",
81
- * tools: [StartCampaign]
82
- * })
83
54
  * ```
84
55
  *
85
56
  * @category constructors
@@ -92,10 +63,8 @@ export const make = <R = never>(
92
63
  readonly description: string
93
64
  /** Text, or an Effect resolved each time the skill is loaded. */
94
65
  readonly instructions: Instructions.Instructions<R>
95
- /** The tools its instructions use: a loop whose toolkit lacks one leaves the skill out. */
96
- readonly tools?: ReadonlyArray<Tool.Any> | undefined
97
66
  }
98
- ): Skill<R> => ({ name, description: options.description, instructions: options.instructions, tools: options.tools })
67
+ ): Skill<R> => ({ name, description: options.description, instructions: options.instructions })
99
68
 
100
69
  /**
101
70
  * Extracts the services that skills need when they're loaded.
@@ -105,20 +74,128 @@ export const make = <R = never>(
105
74
  */
106
75
  export type Services<S> = S extends Skill<infer R> ? R : never
107
76
 
77
+ const loadSkill = (description: string) =>
78
+ Tool.make("load_skill", {
79
+ description,
80
+ parameters: Schema.Struct({ name: Schema.String }),
81
+ success: Schema.String,
82
+ failure: ToolFailure.ToolFailure
83
+ })
84
+
108
85
  /**
109
- * Returns the prompt with the skills' catalog appended, or the prompt unchanged when there are no
110
- * skills.
86
+ * Represents the `load_skill` tool, which reads a skill's instructions by name.
111
87
  *
112
- * @category utils
113
- * @since 0.1.0
88
+ * @category models
89
+ * @since 0.4.0
90
+ */
91
+ export type LoadSkill = ReturnType<typeof loadSkill>
92
+
93
+ /**
94
+ * Represents a list of skills as a toolkit: the `load_skill` tool, and the layer with its handler.
95
+ *
96
+ * `R` is the services the skills' instructions need, which the layer requires.
97
+ *
98
+ * @category models
99
+ * @since 0.4.0
100
+ */
101
+ export interface Toolkit<R = never> extends AiToolkit.Toolkit<{ readonly load_skill: LoadSkill }> {
102
+ /** The handler of `load_skill`, which resolves the instructions of these skills. */
103
+ readonly layer: Layer.Layer<Tool.Handler<"load_skill">, never, R>
104
+ }
105
+
106
+ const catalog = (skills: ReadonlyArray<Skill<unknown>>): string =>
107
+ [
108
+ "Load a skill, a playbook for a kind of task, by name, and follow it for the rest of the task. When one matches what you're doing, load it in the same step as your first lookups, not in a step of its own.",
109
+ "",
110
+ "Skills:",
111
+ ...skills.map((skill) => `- ${skill.name}: ${skill.description}`)
112
+ ].join("\n")
113
+
114
+ function toolkitOf<const Skills extends ReadonlyArray<Skill<unknown>>>(
115
+ skills: Skills
116
+ ): Toolkit<Services<Skills[number]>>
117
+ // The overload states what the layer requires: the services of these skills' instructions.
118
+ function toolkitOf(skills: ReadonlyArray<Skill<unknown>>): Toolkit<unknown> {
119
+ const byName = new Map(skills.map((skill) => [skill.name, skill]))
120
+ const tools = AiToolkit.make(loadSkill(catalog(skills)))
121
+
122
+ const layer = tools.toLayer(
123
+ Effect.gen(function* () {
124
+ const context = yield* Effect.context<unknown>()
125
+
126
+ return {
127
+ load_skill: Effect.fnUntraced(function* ({ name }) {
128
+ const skill = byName.get(name)
129
+
130
+ if (skill === undefined) {
131
+ return yield* ToolFailure.fail(
132
+ `No skill named ${name}. The skills are: ${skills.map((known) => known.name).join(", ")}.`
133
+ )
134
+ }
135
+
136
+ const resolved = yield* Instructions.resolve(skill.instructions).pipe(Effect.provideContext(context))
137
+
138
+ return resolved.text
139
+ })
140
+ }
141
+ })
142
+ )
143
+
144
+ return Object.assign(tools, { layer })
145
+ }
146
+
147
+ /**
148
+ * Creates a toolkit from a list of skills: the `load_skill` tool, whose description lists each
149
+ * skill's name and when to use it, and its handler's `layer`.
150
+ *
151
+ * **Details**
152
+ *
153
+ * Merge the toolkit into a loop's own with `Toolkit.merge`, and provide its `layer`, which requires
154
+ * the services the skills' instructions need. The model loads a skill by name; a name that isn't
155
+ * in the list is a failure the model reads, with the names there are.
156
+ *
157
+ * The handler resolves only the skills of its own list. To offer each user the skills they may
158
+ * use, build the toolkit in the turn from that user's list, and provide its layer to the loop's
159
+ * stream (`Stream.provide(skills.layer)`): building one does no work.
160
+ *
161
+ * **Example** (Giving an assistant its playbooks)
162
+ *
163
+ * ```ts
164
+ * import { Skill } from "effect-assistant"
165
+ * import * as Layer from "effect/Layer"
166
+ * import * as Toolkit from "effect/ai/Toolkit"
167
+ *
168
+ * const AssistantSkills = Skill.toolkit([orderingCampaigns, sharingAssets])
169
+ *
170
+ * const MyAssistantLive = MyAssistant.toLayer((turn) =>
171
+ * turn.loop({ instructions, toolkit: Toolkit.merge(MyTools, AssistantSkills) })
172
+ * ).pipe(Layer.provide([MyToolsLive, AssistantSkills.layer]))
173
+ * ```
174
+ *
175
+ * **Example** (Offering each user the skills their role allows)
176
+ *
177
+ * ```ts
178
+ * import { Skill } from "effect-assistant"
179
+ * import * as Effect from "effect/Effect"
180
+ * import * as Stream from "effect/Stream"
181
+ * import * as Toolkit from "effect/ai/Toolkit"
182
+ *
183
+ * const MyAssistantLive = MyAssistant.toLayer((turn) =>
184
+ * Stream.unwrap(
185
+ * Effect.map(roleOf(turn.chatId), (role) => {
186
+ * const skills = Skill.toolkit(allSkills.filter((skill) => allowed(role, skill)))
187
+ *
188
+ * return turn
189
+ * .loop({ instructions, toolkit: Toolkit.merge(MyTools, skills) })
190
+ * .pipe(Stream.provide(skills.layer))
191
+ * })
192
+ * )
193
+ * )
194
+ * ```
195
+ *
196
+ * @category constructors
197
+ * @since 0.4.0
114
198
  */
115
- export const withCatalog = <R>(prompt: string, skills: ReadonlyArray<Skill<R>>): string =>
116
- skills.length === 0
117
- ? prompt
118
- : [
119
- prompt,
120
- "",
121
- "## Skills",
122
- "Playbooks for common tasks. When one matches what you're doing, load it with load_skill in the same step as your first lookups, and follow it.",
123
- ...skills.map((skill) => `- ${skill.name}: ${skill.description}`)
124
- ].join("\n")
199
+ export const toolkit: <const Skills extends ReadonlyArray<Skill<unknown>>>(
200
+ skills: Skills
201
+ ) => Toolkit<Services<Skills[number]>> = toolkitOf
package/src/Specialist.ts CHANGED
@@ -23,7 +23,6 @@ import type { ChatId } from "./ChatId.ts"
23
23
  import type { CurrentChat } from "./CurrentChat.ts"
24
24
  import type * as Instructions from "./Instructions.ts"
25
25
  import type { RunId } from "./RunId.ts"
26
- import type { Skill } from "./Skill.ts"
27
26
 
28
27
  const TypeId = "~effect-assistant/Specialist"
29
28
 
@@ -123,15 +122,13 @@ export type RunServices = CurrentChat | Runner | LanguageModel.LanguageModel
123
122
  * @category models
124
123
  * @since 0.1.0
125
124
  */
126
- export interface LoopOptions<T extends Record<string, Tool.Any> = Record<never, never>, RS = never> {
125
+ export interface LoopOptions<T extends Record<string, Tool.Any> = Record<never, never>> {
127
126
  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
127
  /**
131
- * Playbooks, listed as a catalog in the prompt and loaded with `load_skill` when one matches. A
132
- * skill made with `tools` is left out when the toolkit lacks one of them.
128
+ * The loop's tools, as a native toolkit whose handlers come from the layer. Merge in a
129
+ * `Skill.toolkit(...)` for playbooks.
133
130
  */
134
- readonly skills?: ReadonlyArray<Skill<RS>> | undefined
131
+ readonly toolkit?: Toolkit.Toolkit<T> | undefined
135
132
  /** The model calls allowed per brief. Defaults to 8. */
136
133
  readonly maxSteps?: number | undefined
137
134
  }
@@ -152,13 +149,13 @@ export interface Run {
152
149
  * (follow-ups and steering). Read it before every model call, as the loop does.
153
150
  */
154
151
  readonly history: Effect.Effect<ReadonlyArray<Prompt.Message>>
155
- /** Runs the agent loop over the run's history, with the given instructions, tools, and skills. */
156
- readonly loop: <T extends Record<string, Tool.Any> = Record<never, never>, RS = never>(
157
- options: LoopOptions<T, RS>
152
+ /** Runs the agent loop over the run's history, with the given instructions and tools. */
153
+ readonly loop: <T extends Record<string, Tool.Any> = Record<never, never>>(
154
+ options: LoopOptions<T>
158
155
  ) => Stream.Stream<
159
156
  AssistantLoop.Event,
160
157
  AiError.AiError,
161
- LanguageModel.LanguageModel | AssistantLoop.ToolkitServices<T> | RS
158
+ LanguageModel.LanguageModel | AssistantLoop.ToolkitServices<T>
162
159
  >
163
160
  }
164
161