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
package/src/InputId.ts ADDED
@@ -0,0 +1,43 @@
1
+ /**
2
+ * This module provides the id of an input to an assistant. An input is a user message, a block
3
+ * answer, or one of the app's events, and the id is stored in its `id` field.
4
+ *
5
+ * @since 0.1.0
6
+ */
7
+ import * as Effect from "effect/Effect"
8
+ import * as Schema from "effect/Schema"
9
+ import * as Uuid from "./internal/uuid.ts"
10
+
11
+ /**
12
+ * Schema for input ids. Any string is a valid input id, and the schema brands it.
13
+ *
14
+ * @category schemas
15
+ * @since 0.1.0
16
+ */
17
+ export const InputId = Schema.String.pipe(Schema.brand("~effect-assistant/InputId"))
18
+
19
+ /**
20
+ * Represents an input id.
21
+ *
22
+ * @category models
23
+ * @since 0.1.0
24
+ */
25
+ export type InputId = typeof InputId.Type
26
+
27
+ /**
28
+ * Creates a new random input id using Effect's `Random`. Seed `Random` in tests to get the same ids
29
+ * on every run.
30
+ *
31
+ * @category constructors
32
+ * @since 0.1.0
33
+ */
34
+ export const make = (): Effect.Effect<InputId> => Effect.map(Uuid.make, (uuid) => InputId.make(uuid))
35
+
36
+ /**
37
+ * Creates the input id for a key, such as `${runId}:order_placed`. The same key always gives the
38
+ * same id, so an input that is sent again is stored only once.
39
+ *
40
+ * @category constructors
41
+ * @since 0.1.0
42
+ */
43
+ export const fromKey = (key: string): InputId => InputId.make(key)
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Defines the instructions that tell an assistant, specialist, or skill what to do.
3
+ *
4
+ * Instructions are text, or an Effect that produces text, optionally with span attributes
5
+ * (a {@link Resolved}). {@link resolve} turns them into a `Resolved`, and {@link protocol} is the
6
+ * text a turn's loop needs the model to know.
7
+ *
8
+ * @since 0.1.0
9
+ */
10
+ import * as Effect from "effect/Effect"
11
+ import * as Predicate from "effect/Predicate"
12
+
13
+ /**
14
+ * The text the model has to know for a turn's loop to work.
15
+ *
16
+ * **Details**
17
+ *
18
+ * It tells the model that every step is a tool call, that `wait` (or a last message with
19
+ * `wait: true`) ends the turn, that the user sees only what it sends with `send_user_message`, and
20
+ * that messages can arrive mid-turn. The rest, such as what a `<specialist>` block or an app's
21
+ * event is, is in the tools' descriptions and in the history itself.
22
+ *
23
+ * `turn.instructions(text)` puts it before the text, and a custom loop on `AssistantLoop.run` can
24
+ * use it too.
25
+ *
26
+ * @category utils
27
+ * @since 0.1.0
28
+ */
29
+ export const protocol = `Every step is a tool call. When you have nothing more to do right now, end your turn: set wait to true on your last send_user_message, or call wait if you have nothing to say.
30
+ The user sees only the text of what you send with send_user_message: anything else you write is never shown, so put the whole answer in the message, not next to it.
31
+ Messages and updates can arrive while you work: handle the newest one, and don't repeat what you already told the user.`
32
+
33
+ /**
34
+ * Represents the span attributes added to every model call made with a prompt.
35
+ *
36
+ * @category models
37
+ * @since 0.1.0
38
+ */
39
+ export type SpanAttributes = Readonly<Record<string, string | number | boolean>>
40
+
41
+ /**
42
+ * Represents instructions resolved to text, with the span attributes to record them under.
43
+ *
44
+ * @category models
45
+ * @since 0.1.0
46
+ */
47
+ export interface Resolved {
48
+ readonly text: string
49
+ readonly attributes?: SpanAttributes | undefined
50
+ }
51
+
52
+ /**
53
+ * Represents instructions as text, or as an Effect that produces the text (optionally with span
54
+ * attributes).
55
+ *
56
+ * @category models
57
+ * @since 0.1.0
58
+ */
59
+ export type Instructions<R = never> = string | Effect.Effect<string | Resolved, never, R>
60
+
61
+ /**
62
+ * Resolves instructions to their text and span attributes.
63
+ *
64
+ * @category utils
65
+ * @since 0.1.0
66
+ */
67
+ export const resolve = Effect.fn("Instructions.resolve")(function* <R = never>(instructions: Instructions<R>) {
68
+ return Predicate.isString(instructions) ? { text: instructions } : toResolved(yield* instructions)
69
+ })
70
+
71
+ const toResolved = (resolved: string | Resolved): Resolved =>
72
+ Predicate.isString(resolved) ? { text: resolved } : resolved
package/src/Memory.ts ADDED
@@ -0,0 +1,276 @@
1
+ /**
2
+ * Keeps facts an assistant remembers across chats.
3
+ *
4
+ * The model keeps facts with the `remember` tool, finds them with `recall`, and drops them with
5
+ * `forget` ({@link Tools}). Where facts are stored and who shares them is up to the application,
6
+ * which implements {@link MemoryStore}; every method receives the chat it's for. {@link render}
7
+ * shows the facts in a turn's instructions.
8
+ *
9
+ * @since 0.1.0
10
+ */
11
+ import * as Tool from "effect/ai/Tool"
12
+ import * as Toolkit from "effect/ai/Toolkit"
13
+ import * as Clock from "effect/Clock"
14
+ import * as Context from "effect/Context"
15
+ import * as DateTime from "effect/DateTime"
16
+ import * as Effect from "effect/Effect"
17
+ import * as Layer from "effect/Layer"
18
+ import * as Option from "effect/Option"
19
+ import * as Ref from "effect/Ref"
20
+ import * as Schema from "effect/Schema"
21
+ import type { ChatId } from "./ChatId.ts"
22
+ import { CurrentChat } from "./CurrentChat.ts"
23
+ import * as Uuid from "./internal/uuid.ts"
24
+ import * as ToolFailure from "./ToolFailure.ts"
25
+
26
+ /**
27
+ * Represents a fact the assistant remembers.
28
+ *
29
+ * It holds the fact's id, one short sentence, and when the fact stops mattering, if it does.
30
+ *
31
+ * @category schemas
32
+ * @since 0.1.0
33
+ */
34
+ export class Fact extends Schema.Class<Fact>("effect-assistant/Memory/Fact")({
35
+ id: Schema.String,
36
+ text: Schema.String,
37
+ expiresAt: Schema.optional(Schema.DateTimeUtc)
38
+ }) {}
39
+
40
+ /**
41
+ * Represents a fact to keep, as the `remember` tool hands it to the store.
42
+ *
43
+ * @category models
44
+ * @since 0.1.0
45
+ */
46
+ export interface NewFact {
47
+ readonly text: string
48
+ readonly expiresAt?: DateTime.Utc | undefined
49
+ /** The id of a fact this one replaces. */
50
+ readonly replaces?: string | undefined
51
+ }
52
+
53
+ /**
54
+ * Service that keeps facts, implemented by the application.
55
+ *
56
+ * Every method receives the chat it's for, and the application decides whose facts that means
57
+ * (the chat's user, their team, ...).
58
+ *
59
+ * @category services
60
+ * @since 0.1.0
61
+ */
62
+ export class MemoryStore extends Context.Service<
63
+ MemoryStore,
64
+ {
65
+ /** Returns the unexpired facts a chat should know. */
66
+ readonly facts: (chatId: ChatId) => Effect.Effect<ReadonlyArray<Fact>>
67
+ /** Keeps a fact for the chat's owner, and returns its id. */
68
+ readonly remember: (chatId: ChatId, fact: NewFact) => Effect.Effect<string>
69
+ /**
70
+ * Drops a fact. Fails with a message for the model when the chat's owner has no fact with
71
+ * that id.
72
+ */
73
+ readonly forget: (chatId: ChatId, id: string) => Effect.Effect<void, ToolFailure.ToolFailure>
74
+ /** Returns the chat owner's facts that match a query, best first. */
75
+ readonly recall: (chatId: ChatId, query: string) => Effect.Effect<ReadonlyArray<Fact>>
76
+ }
77
+ >()("effect-assistant/Memory/MemoryStore") {}
78
+
79
+ const day = (at: DateTime.Utc): string => DateTime.formatIsoDateUtc(at)
80
+
81
+ const line = (fact: Fact): string =>
82
+ `- [${fact.id}] ${fact.text}${fact.expiresAt === undefined ? "" : ` (until ${day(fact.expiresAt)})`}`
83
+
84
+ /**
85
+ * Renders facts as the model reads them, for a turn's instructions.
86
+ *
87
+ * Each fact is listed with its id, so the model can replace or forget it. The result is an empty
88
+ * string when there are no facts.
89
+ *
90
+ * @see {@link Tools} for an example
91
+ *
92
+ * @category utils
93
+ * @since 0.1.0
94
+ */
95
+ export const render = (facts: ReadonlyArray<Fact>): string =>
96
+ facts.length === 0
97
+ ? ""
98
+ : `<memory>\nWhat you remember from earlier chats. Use it without saying where it came from; when their data or what they say now disagrees, that wins.\n${facts.map(line).join("\n")}\n</memory>`
99
+
100
+ const words = (text: string): ReadonlyArray<string> =>
101
+ text
102
+ .toLowerCase()
103
+ .split(/[^\p{L}\p{N}]+/u)
104
+ .filter((word) => word.length > 2)
105
+
106
+ const matches = (fact: Fact, query: ReadonlyArray<string>): number => {
107
+ const own = words(fact.text)
108
+
109
+ return query.filter((word) => own.some((w) => w.startsWith(word) || word.startsWith(w))).length
110
+ }
111
+
112
+ const live = (now: number) => (fact: Fact) =>
113
+ fact.expiresAt === undefined || DateTime.toEpochMillis(fact.expiresAt) > now
114
+
115
+ /**
116
+ * Layer that provides a {@link MemoryStore} in memory, for tests and local runs.
117
+ *
118
+ * Every chat shares one set of facts.
119
+ *
120
+ * @category layers
121
+ * @since 0.1.0
122
+ */
123
+ export const layerMemory: Layer.Layer<MemoryStore> = Layer.effect(
124
+ MemoryStore,
125
+ Effect.gen(function* () {
126
+ // Newest first.
127
+ const all = yield* Ref.make<ReadonlyArray<Fact>>([])
128
+
129
+ const current = Effect.gen(function* () {
130
+ const now = yield* Clock.currentTimeMillis
131
+
132
+ return (yield* Ref.get(all)).filter(live(now))
133
+ })
134
+
135
+ return MemoryStore.of({
136
+ facts: () => current,
137
+ remember: Effect.fnUntraced(function* (_chatId: ChatId, fact: NewFact) {
138
+ const same = (yield* current).find((kept) => kept.text === fact.text.trim())
139
+ const id = same?.id ?? `f-${(yield* Uuid.make).slice(0, 8)}`
140
+ const kept = new Fact({ id, text: fact.text.trim(), expiresAt: fact.expiresAt })
141
+
142
+ yield* Ref.update(all, (facts) => [kept, ...facts.filter((old) => old.id !== id && old.id !== fact.replaces)])
143
+
144
+ return id
145
+ }),
146
+ forget: Effect.fnUntraced(function* (_chatId: ChatId, id: string) {
147
+ if (!(yield* current).some((fact) => fact.id === id))
148
+ return yield* ToolFailure.fail(`Nothing remembered as ${id}.`)
149
+
150
+ yield* Ref.update(all, (facts) => facts.filter((fact) => fact.id !== id))
151
+ }),
152
+ recall: Effect.fnUntraced(function* (_chatId: ChatId, query: string) {
153
+ const wanted = words(query)
154
+
155
+ return (yield* current)
156
+ .map((fact) => ({ fact, score: matches(fact, wanted) }))
157
+ .filter(({ score }) => score > 0)
158
+ .sort((a, b) => b.score - a.score)
159
+ .slice(0, 10)
160
+ .map(({ fact }) => fact)
161
+ })
162
+ })
163
+ })
164
+ )
165
+
166
+ const Remember = Tool.make("remember", {
167
+ description: `Remember something for later chats. Only what you can't look up again and that will still matter: a preference, a standing decision, how they like to work ("Rotterdam's copy is always Dutch"). For something short-lived ("out of office until Friday"), pass until. Not records your tools can read (orders, locations, budgets, people), not the state of a task in progress, and nothing you'd only need in this chat. One fact per call, in one short sentence that stands on its own. When it changes one you already have, pass that one's id as replaces.`,
168
+ parameters: Schema.Struct({
169
+ text: Schema.String,
170
+ until: Schema.optional(Schema.String).annotate({
171
+ description: "For something short-lived: the last day it matters, as YYYY-MM-DD."
172
+ }),
173
+ replaces: Schema.optional(Schema.String).annotate({ description: "The id of a fact this one replaces." })
174
+ }),
175
+ success: Schema.String,
176
+ failure: ToolFailure.ToolFailure,
177
+ dependencies: [CurrentChat, MemoryStore]
178
+ })
179
+
180
+ const Forget = Tool.make("forget", {
181
+ description: "Forget a fact you remember, by its id, when they ask or it's no longer true.",
182
+ parameters: Schema.Struct({ id: Schema.String }),
183
+ success: Schema.String,
184
+ failure: ToolFailure.ToolFailure,
185
+ dependencies: [CurrentChat, MemoryStore]
186
+ })
187
+
188
+ const Recall = Tool.make("recall", {
189
+ description: "Search what you remember, by words that would be in it.",
190
+ parameters: Schema.Struct({ query: Schema.String }),
191
+ success: Schema.Array(Schema.String),
192
+ failure: ToolFailure.ToolFailure,
193
+ dependencies: [CurrentChat, MemoryStore]
194
+ })
195
+
196
+ /**
197
+ * Toolkit with the `remember`, `forget`, and `recall` tools.
198
+ *
199
+ * **Details**
200
+ *
201
+ * Merge it into the loop's toolkit, and provide {@link layerTools} and a {@link MemoryStore}. Show
202
+ * the facts in the turn's instructions with {@link render}.
203
+ *
204
+ * **Example** (Giving an assistant memory)
205
+ *
206
+ * ```ts
207
+ * import { Assistant, Memory } from "effect-assistant"
208
+ * import * as Effect from "effect/Effect"
209
+ * import * as Layer from "effect/Layer"
210
+ * import * as Stream from "effect/Stream"
211
+ *
212
+ * export const MyAssistant = Assistant.make("my-assistant")
213
+ *
214
+ * const MyAssistantLive = MyAssistant.toLayer(
215
+ * Effect.gen(function* () {
216
+ * const memory = yield* Memory.MemoryStore
217
+ *
218
+ * return (turn) =>
219
+ * Stream.unwrap(
220
+ * Effect.map(memory.facts(turn.chatId), (facts) =>
221
+ * turn.loop({
222
+ * instructions: `You help a small team.\n\n${Memory.render(facts)}`,
223
+ * toolkit: Memory.Tools
224
+ * })
225
+ * )
226
+ * ).pipe(Stream.provideService(Memory.MemoryStore, memory))
227
+ * })
228
+ * ).pipe(Layer.provide(Memory.layerTools), Layer.provide(Memory.layerMemory))
229
+ * ```
230
+ *
231
+ * @category tools
232
+ * @since 0.1.0
233
+ */
234
+ export const Tools = Toolkit.make(Remember, Forget, Recall)
235
+
236
+ const endOf = (date: string): Option.Option<DateTime.Utc> =>
237
+ /^\d{4}-\d{2}-\d{2}$/.test(date) ? DateTime.make(`${date}T23:59:59Z`) : Option.none()
238
+
239
+ /**
240
+ * Layer that provides the handlers of {@link Tools}.
241
+ *
242
+ * The handlers call the {@link MemoryStore} with the chat they run in.
243
+ *
244
+ * @see {@link Tools} for an example
245
+ *
246
+ * @category layers
247
+ * @since 0.1.0
248
+ */
249
+ export const layerTools = Tools.toLayer({
250
+ remember: Effect.fnUntraced(function* ({ text, until, replaces }) {
251
+ const chat = yield* CurrentChat
252
+ const store = yield* MemoryStore
253
+ const expiresAt = until === undefined ? Option.none() : endOf(until)
254
+
255
+ if (until !== undefined && Option.isNone(expiresAt)) {
256
+ return yield* ToolFailure.fail(`until must be a date as YYYY-MM-DD, not ${until}.`)
257
+ }
258
+
259
+ const id = yield* store.remember(chat.chatId, { text, expiresAt: Option.getOrUndefined(expiresAt), replaces })
260
+
261
+ return `Remembered as ${id}.`
262
+ }),
263
+ forget: Effect.fnUntraced(function* ({ id }) {
264
+ const chat = yield* CurrentChat
265
+
266
+ yield* (yield* MemoryStore).forget(chat.chatId, id)
267
+
268
+ return `Forgot ${id}.`
269
+ }),
270
+ recall: Effect.fnUntraced(function* ({ query }) {
271
+ const chat = yield* CurrentChat
272
+ const found = yield* (yield* MemoryStore).recall(chat.chatId, query)
273
+
274
+ return found.map(line)
275
+ })
276
+ })
package/src/RunId.ts ADDED
@@ -0,0 +1,46 @@
1
+ /**
2
+ * This module provides the id of a background run, such as a specialist run or a workflow started
3
+ * from a tool. A run id is `run:` followed by a UUID.
4
+ *
5
+ * @since 0.1.0
6
+ */
7
+ import * as Effect from "effect/Effect"
8
+ import * as Schema from "effect/Schema"
9
+ import * as Uuid from "./internal/uuid.ts"
10
+
11
+ /**
12
+ * Schema for run ids of the form `` `run:${uuid}` ``, branded.
13
+ *
14
+ * @category schemas
15
+ * @since 0.1.0
16
+ */
17
+ export const RunId = Schema.TemplateLiteral(["run:", Uuid.Uuid]).pipe(Schema.brand("~effect-assistant/RunId"))
18
+
19
+ /**
20
+ * Represents a run id.
21
+ *
22
+ * @category models
23
+ * @since 0.1.0
24
+ */
25
+ export type RunId = typeof RunId.Type
26
+
27
+ /**
28
+ * Creates a new random run id using Effect's `Random`.
29
+ *
30
+ * @category constructors
31
+ * @since 0.1.0
32
+ */
33
+ export const make = (): Effect.Effect<RunId> => Effect.map(Uuid.make, (uuid) => RunId.make(`run:${uuid}`))
34
+
35
+ /**
36
+ * Creates the run id for a key. The same key always gives the same id.
37
+ *
38
+ * **When to use**
39
+ *
40
+ * Use it for a run that a tool call starts, with the call's `AssistantToolCall.idempotencyKey` as
41
+ * the key, so running the call again finds the same run.
42
+ *
43
+ * @category constructors
44
+ * @since 0.1.0
45
+ */
46
+ export const fromKey = (key: string): RunId => RunId.make(`run:${Uuid.fromKey(key)}`)
package/src/Skill.ts ADDED
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Defines skills, which are playbooks with step-by-step instructions for a kind of task.
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.
7
+ *
8
+ * @since 0.1.0
9
+ */
10
+ import type * as Instructions from "./Instructions.ts"
11
+
12
+ /**
13
+ * Represents a skill.
14
+ *
15
+ * `R` is the services its instructions need when they're loaded.
16
+ *
17
+ * @category models
18
+ * @since 0.1.0
19
+ */
20
+ export interface Skill<R = never> {
21
+ readonly name: string
22
+ /** When to use it, in one line. This is all the model sees until it loads the skill. */
23
+ readonly description: string
24
+ /** Text, or an Effect resolved each time the skill is loaded. */
25
+ readonly instructions: Instructions.Instructions<R>
26
+ }
27
+
28
+ /**
29
+ * Creates a skill from its name, a description of when to use it, and its instructions.
30
+ *
31
+ * **Details**
32
+ *
33
+ * Write the description as a trigger ("Use when…"), since it's all the model sees until it loads
34
+ * the skill. The instructions are text, or an Effect that is resolved each time the skill is
35
+ * loaded.
36
+ *
37
+ * **Example** (Defining a playbook for ordering campaigns)
38
+ *
39
+ * ```ts
40
+ * import { Skill } from "effect-assistant"
41
+ *
42
+ * const orderingCampaigns = Skill.make("Ordering a campaign", {
43
+ * description: "Use when the user wants to order or start a campaign.",
44
+ * instructions: `1. Pick the product with list_campaign_products.
45
+ * 2. You need who it should reach and a budget. Ask for what's missing, one question at a time.
46
+ * 3. When you have both, call start_campaign.`
47
+ * })
48
+ *
49
+ * const MyAssistantLive = MyAssistant.toLayer((turn) =>
50
+ * turn.loop({ instructions, skills: [orderingCampaigns] })
51
+ * )
52
+ * ```
53
+ *
54
+ * @category constructors
55
+ * @since 0.1.0
56
+ */
57
+ export const make = <R = never>(
58
+ name: string,
59
+ options: {
60
+ /** When to use it, in one line. This is all the model sees until it loads the skill. */
61
+ readonly description: string
62
+ /** Text, or an Effect resolved each time the skill is loaded. */
63
+ readonly instructions: Instructions.Instructions<R>
64
+ }
65
+ ): Skill<R> => ({ name, description: options.description, instructions: options.instructions })
66
+
67
+ /**
68
+ * Extracts the services that skills need when they're loaded.
69
+ *
70
+ * @category utility types
71
+ * @since 0.1.0
72
+ */
73
+ export type Services<S> = S extends Skill<infer R> ? R : never
74
+
75
+ /**
76
+ * Returns the prompt with the skills' catalog appended, or the prompt unchanged when there are no
77
+ * skills.
78
+ *
79
+ * @category utils
80
+ * @since 0.1.0
81
+ */
82
+ export const withCatalog = <R>(prompt: string, skills: ReadonlyArray<Skill<R>>): string =>
83
+ skills.length === 0
84
+ ? prompt
85
+ : [
86
+ prompt,
87
+ "",
88
+ "## Skills",
89
+ "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.",
90
+ ...skills.map((skill) => `- ${skill.name}: ${skill.description}`)
91
+ ].join("\n")