effect-assistant 0.0.0-stage → 0.2.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 (172) 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 +551 -0
  20. package/dist/AssistantEvent.d.ts.map +1 -0
  21. package/dist/AssistantEvent.js +473 -0
  22. package/dist/AssistantEvent.js.map +1 -0
  23. package/dist/AssistantLoop.d.ts +173 -0
  24. package/dist/AssistantLoop.d.ts.map +1 -0
  25. package/dist/AssistantLoop.js +111 -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 +180 -0
  36. package/dist/BlockKit.d.ts.map +1 -0
  37. package/dist/BlockKit.js +148 -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 +408 -0
  48. package/dist/ChatView.d.ts.map +1 -0
  49. package/dist/ChatView.js +443 -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 +126 -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/InputId.d.ts +38 -0
  64. package/dist/InputId.d.ts.map +1 -0
  65. package/dist/InputId.js +43 -0
  66. package/dist/InputId.js.map +1 -0
  67. package/dist/Instructions.d.ts +56 -0
  68. package/dist/Instructions.d.ts.map +1 -0
  69. package/dist/Instructions.js +50 -0
  70. package/dist/Instructions.js.map +1 -0
  71. package/dist/Memory.d.ts +190 -0
  72. package/dist/Memory.d.ts.map +1 -0
  73. package/dist/Memory.js +216 -0
  74. package/dist/Memory.js.map +1 -0
  75. package/dist/RunId.d.ts +41 -0
  76. package/dist/RunId.d.ts.map +1 -0
  77. package/dist/RunId.js +46 -0
  78. package/dist/RunId.js.map +1 -0
  79. package/dist/Skill.d.ts +72 -0
  80. package/dist/Skill.d.ts.map +1 -0
  81. package/dist/Skill.js +58 -0
  82. package/dist/Skill.js.map +1 -0
  83. package/dist/Specialist.d.ts +267 -0
  84. package/dist/Specialist.d.ts.map +1 -0
  85. package/dist/Specialist.js +119 -0
  86. package/dist/Specialist.js.map +1 -0
  87. package/dist/ToolDiscovery.d.ts +107 -0
  88. package/dist/ToolDiscovery.d.ts.map +1 -0
  89. package/dist/ToolDiscovery.js +140 -0
  90. package/dist/ToolDiscovery.js.map +1 -0
  91. package/dist/ToolFailure.d.ts +66 -0
  92. package/dist/ToolFailure.d.ts.map +1 -0
  93. package/dist/ToolFailure.js +79 -0
  94. package/dist/ToolFailure.js.map +1 -0
  95. package/dist/_virtual/_rolldown/runtime.js +13 -0
  96. package/dist/index.d.ts +24 -0
  97. package/dist/index.js +24 -0
  98. package/dist/internal/chat.js +177 -0
  99. package/dist/internal/chat.js.map +1 -0
  100. package/dist/internal/events.d.ts +46 -0
  101. package/dist/internal/events.d.ts.map +1 -0
  102. package/dist/internal/events.js +65 -0
  103. package/dist/internal/events.js.map +1 -0
  104. package/dist/internal/history.js +147 -0
  105. package/dist/internal/history.js.map +1 -0
  106. package/dist/internal/instructions.js +11 -0
  107. package/dist/internal/instructions.js.map +1 -0
  108. package/dist/internal/loop.d.ts +73 -0
  109. package/dist/internal/loop.d.ts.map +1 -0
  110. package/dist/internal/loop.js +131 -0
  111. package/dist/internal/loop.js.map +1 -0
  112. package/dist/internal/schema.js +14 -0
  113. package/dist/internal/schema.js.map +1 -0
  114. package/dist/internal/skills.js +28 -0
  115. package/dist/internal/skills.js.map +1 -0
  116. package/dist/internal/specialists.js +272 -0
  117. package/dist/internal/specialists.js.map +1 -0
  118. package/dist/internal/telemetry.js +85 -0
  119. package/dist/internal/telemetry.js.map +1 -0
  120. package/dist/internal/toolkit.d.ts +62 -0
  121. package/dist/internal/toolkit.d.ts.map +1 -0
  122. package/dist/internal/toolkit.js +108 -0
  123. package/dist/internal/toolkit.js.map +1 -0
  124. package/dist/internal/tools.js +113 -0
  125. package/dist/internal/tools.js.map +1 -0
  126. package/dist/internal/turns.d.ts +64 -0
  127. package/dist/internal/turns.d.ts.map +1 -0
  128. package/dist/internal/turns.js +214 -0
  129. package/dist/internal/turns.js.map +1 -0
  130. package/dist/internal/uuid.js +45 -0
  131. package/dist/internal/uuid.js.map +1 -0
  132. package/dist/internal/writer.d.ts +12 -0
  133. package/dist/internal/writer.d.ts.map +1 -0
  134. package/package.json +52 -3
  135. package/src/AppEvent.ts +134 -0
  136. package/src/Assistant.ts +589 -0
  137. package/src/AssistantActivity.ts +150 -0
  138. package/src/AssistantEngine.ts +343 -0
  139. package/src/AssistantEvent.ts +569 -0
  140. package/src/AssistantLoop.ts +209 -0
  141. package/src/AssistantToolCall.ts +40 -0
  142. package/src/Block.ts +84 -0
  143. package/src/BlockKit.ts +298 -0
  144. package/src/ChatId.ts +36 -0
  145. package/src/ChatLog.ts +247 -0
  146. package/src/ChatView.ts +587 -0
  147. package/src/Chats.ts +122 -0
  148. package/src/Compaction.ts +207 -0
  149. package/src/CurrentChat.ts +47 -0
  150. package/src/InputId.ts +43 -0
  151. package/src/Instructions.ts +72 -0
  152. package/src/Memory.ts +276 -0
  153. package/src/RunId.ts +46 -0
  154. package/src/Skill.ts +91 -0
  155. package/src/Specialist.ts +355 -0
  156. package/src/ToolDiscovery.ts +179 -0
  157. package/src/ToolFailure.ts +74 -0
  158. package/src/index.ts +120 -0
  159. package/src/internal/chat.ts +323 -0
  160. package/src/internal/events.ts +100 -0
  161. package/src/internal/history.ts +294 -0
  162. package/src/internal/instructions.ts +9 -0
  163. package/src/internal/loop.ts +274 -0
  164. package/src/internal/schema.ts +25 -0
  165. package/src/internal/skills.ts +37 -0
  166. package/src/internal/specialists.ts +410 -0
  167. package/src/internal/telemetry.ts +125 -0
  168. package/src/internal/toolkit.ts +188 -0
  169. package/src/internal/tools.ts +159 -0
  170. package/src/internal/turns.ts +410 -0
  171. package/src/internal/uuid.ts +50 -0
  172. package/src/internal/writer.ts +12 -0
package/src/ChatLog.ts ADDED
@@ -0,0 +1,247 @@
1
+ /**
2
+ * This module provides the `ChatLog` service, which stores each chat as an append-only list of
3
+ * `AssistantEvent`s.
4
+ *
5
+ * A log is built on a {@link Storage}. {@link layerMemory} keeps every chat in memory, for tests
6
+ * and local runs, and {@link layerSql} stores chats in a SQL table so they survive restarts. To
7
+ * use another store, implement a `Storage` and pass it to {@link make}.
8
+ *
9
+ * @since 0.1.0
10
+ */
11
+ import * as Clock from "effect/Clock"
12
+ import * as Context from "effect/Context"
13
+ import * as Effect from "effect/Effect"
14
+ import * as Layer from "effect/Layer"
15
+ import * as Predicate from "effect/Predicate"
16
+ import * as Ref from "effect/Ref"
17
+ import * as Schema from "effect/Schema"
18
+ import * as SqlClient from "effect/sql/SqlClient"
19
+ import * as AssistantEvent from "./AssistantEvent.ts"
20
+ import { ChatId } from "./ChatId.ts"
21
+
22
+ /**
23
+ * Service that stores each chat's events as an ordered list of entries.
24
+ *
25
+ * @category services
26
+ * @since 0.1.0
27
+ */
28
+ export class ChatLog extends Context.Service<
29
+ ChatLog,
30
+ {
31
+ /** Appends events to a chat and returns them as stored entries. */
32
+ readonly append: (
33
+ chatId: ChatId,
34
+ events: ReadonlyArray<AssistantEvent.AssistantEvent | AssistantEvent.StoredAppEvent>
35
+ ) => Effect.Effect<ReadonlyArray<AssistantEvent.Entry>>
36
+ /** Reads a chat's stored entries in order, either all of them or only those after a `seq`. */
37
+ readonly read: (chatId: ChatId, options?: ReadOptions) => Effect.Effect<ReadonlyArray<AssistantEvent.Entry>>
38
+ /**
39
+ * Returns the chats whose last entry ends neither a turn nor the chat. After a restart, these
40
+ * are the chats that may still owe a turn.
41
+ */
42
+ readonly unfinished: Effect.Effect<ReadonlyArray<ChatId>>
43
+ }
44
+ >()("effect-assistant/ChatLog") {}
45
+
46
+ /**
47
+ * Represents the options that select which of a chat's entries to read.
48
+ *
49
+ * @category models
50
+ * @since 0.1.0
51
+ */
52
+ export interface ReadOptions {
53
+ /** Reads only the entries after this `seq`. */
54
+ readonly after?: number | undefined
55
+ }
56
+
57
+ /**
58
+ * Represents the storage that a `ChatLog` is built on.
59
+ *
60
+ * A storage reads a chat's entries, finds a chat's last `seq`, writes new entries, and finds every
61
+ * chat's last entry. Entries are never removed, so a deleted chat keeps its log, which ends in
62
+ * `ChatDeleted`.
63
+ *
64
+ * @category models
65
+ * @since 0.1.0
66
+ */
67
+ export interface Storage {
68
+ readonly read: (chatId: ChatId, options?: ReadOptions) => Effect.Effect<ReadonlyArray<AssistantEvent.Entry>>
69
+ readonly lastSeq: (chatId: ChatId) => Effect.Effect<number>
70
+ readonly write: (chatId: ChatId, entries: ReadonlyArray<AssistantEvent.Entry>) => Effect.Effect<void>
71
+ /** Returns the last entry of every chat. */
72
+ readonly lastEntries: Effect.Effect<ReadonlyArray<AssistantEvent.Entry>>
73
+ }
74
+
75
+ /**
76
+ * Creates a `ChatLog` from a {@link Storage}. `append` numbers new entries after the chat's last
77
+ * `seq` and stamps them with the current time.
78
+ *
79
+ * @category constructors
80
+ * @since 0.1.0
81
+ */
82
+ export const make = (storage: Storage): ChatLog["Service"] =>
83
+ ChatLog.of({
84
+ read: storage.read,
85
+ append: Effect.fn("ChatLog.append")(function* (
86
+ chatId: ChatId,
87
+ events: ReadonlyArray<AssistantEvent.AssistantEvent | AssistantEvent.StoredAppEvent>
88
+ ) {
89
+ if (events.length === 0) return []
90
+
91
+ const seq = yield* storage.lastSeq(chatId)
92
+ const at = yield* Clock.currentTimeMillis
93
+ const entries = events.map((event, n) => AssistantEvent.Entry.make({ chatId, seq: seq + n + 1, at, event }))
94
+
95
+ yield* storage.write(chatId, entries)
96
+
97
+ return entries
98
+ }),
99
+ unfinished: Effect.gen(function* () {
100
+ const last = yield* storage.lastEntries
101
+
102
+ // A deleted chat owes nothing.
103
+ return last.flatMap((entry) =>
104
+ AssistantEvent.isTurnEnd(entry.event) || Predicate.isTagged(entry.event, "ChatDeleted") ? [] : [entry.chatId]
105
+ )
106
+ })
107
+ })
108
+
109
+ /**
110
+ * Layer that keeps every chat in memory. The log is lost when the process stops.
111
+ *
112
+ * @category layers
113
+ * @since 0.1.0
114
+ */
115
+ export const layerMemory: Layer.Layer<ChatLog> = Layer.effect(
116
+ ChatLog,
117
+ Effect.gen(function* () {
118
+ const chats = yield* Ref.make(new Map<ChatId, ReadonlyArray<AssistantEvent.Entry>>())
119
+
120
+ return make({
121
+ read: Effect.fnUntraced(function* (chatId: ChatId, options?: ReadOptions) {
122
+ const all = yield* Ref.get(chats)
123
+
124
+ return (all.get(chatId) ?? []).filter((entry) => entry.seq > (options?.after ?? 0))
125
+ }),
126
+ lastSeq: Effect.fnUntraced(function* (chatId: ChatId) {
127
+ const all = yield* Ref.get(chats)
128
+
129
+ return all.get(chatId)?.at(-1)?.seq ?? 0
130
+ }),
131
+ write: (chatId, entries) =>
132
+ Ref.update(chats, (all) => new Map(all).set(chatId, [...(all.get(chatId) ?? []), ...entries])),
133
+ lastEntries: Effect.gen(function* () {
134
+ const all = yield* Ref.get(chats)
135
+
136
+ return [...all.values()].flatMap((entries) => entries.slice(-1))
137
+ })
138
+ })
139
+ })
140
+ )
141
+
142
+ const EventJson = Schema.fromJsonString(Schema.Union([AssistantEvent.AssistantEvent, AssistantEvent.StoredAppEvent]))
143
+
144
+ // Postgres returns BIGINT and DOUBLE PRECISION columns as strings; other databases as numbers.
145
+ const SqlNumber = Schema.Union([Schema.Finite, Schema.FiniteFromString])
146
+
147
+ const Row = Schema.Struct({ chat_id: ChatId, seq: SqlNumber, at: SqlNumber, event: EventJson })
148
+
149
+ const decodeLastSeq = Schema.decodeUnknownEffect(Schema.Array(Schema.Struct({ seq: Schema.NullOr(SqlNumber) })))
150
+
151
+ /**
152
+ * Layer that stores chats in a SQL table named `chat_log`.
153
+ *
154
+ * **Details**
155
+ *
156
+ * The table must already exist. It has the columns `chat_id`, `seq`, `at`, and `event`, with the
157
+ * primary key `(chat_id, seq)`. The layer requires a `SqlClient`.
158
+ *
159
+ * **Example** (Storing chats in Postgres)
160
+ *
161
+ * ```ts
162
+ * import * as PgClient from "@effect/sql-pg/PgClient"
163
+ * import { ChatLog } from "effect-assistant"
164
+ * import * as Config from "effect/Config"
165
+ * import * as Layer from "effect/Layer"
166
+ *
167
+ * const ChatLogLive = ChatLog.layerSql.pipe(
168
+ * Layer.provide(PgClient.layerConfig({ url: Config.Redacted("DATABASE_URL") }))
169
+ * )
170
+ * ```
171
+ *
172
+ * The table, in Postgres:
173
+ *
174
+ * ```sql
175
+ * CREATE TABLE chat_log (
176
+ * chat_id TEXT NOT NULL,
177
+ * seq INTEGER NOT NULL,
178
+ * at DOUBLE PRECISION NOT NULL,
179
+ * event TEXT NOT NULL,
180
+ * PRIMARY KEY (chat_id, seq)
181
+ * );
182
+ * ```
183
+ *
184
+ * @category layers
185
+ * @since 0.1.0
186
+ */
187
+ export const layerSql: Layer.Layer<ChatLog, never, SqlClient.SqlClient> = Layer.effect(
188
+ ChatLog,
189
+ Effect.gen(function* () {
190
+ const sql = (yield* SqlClient.SqlClient).withoutTransforms()
191
+
192
+ const toEntries = Effect.fnUntraced(function* (rows: ReadonlyArray<unknown>) {
193
+ const decoded = yield* Schema.decodeUnknownEffect(Schema.Array(Row))(rows)
194
+
195
+ return decoded.map((row) =>
196
+ AssistantEvent.Entry.make({
197
+ chatId: row.chat_id,
198
+ seq: row.seq,
199
+ at: row.at,
200
+ event: row.event
201
+ })
202
+ )
203
+ })
204
+
205
+ const encodeEvent = Schema.encodeEffect(EventJson)
206
+
207
+ return make({
208
+ read: Effect.fnUntraced(function* (chatId: ChatId, options?: ReadOptions) {
209
+ const rows = yield* sql`
210
+ SELECT chat_id, seq, at, event FROM chat_log WHERE chat_id = ${chatId} AND seq > ${options?.after ?? 0} ORDER BY seq
211
+ `
212
+
213
+ return yield* toEntries(rows)
214
+ }, Effect.orDie),
215
+ lastSeq: Effect.fnUntraced(function* (chatId: ChatId) {
216
+ const result = yield* sql`
217
+ SELECT MAX(seq) AS seq FROM chat_log WHERE chat_id = ${chatId}
218
+ `
219
+
220
+ const rows = yield* decodeLastSeq(result)
221
+
222
+ return rows[0]?.seq ?? 0
223
+ }, Effect.orDie),
224
+ write: Effect.fnUntraced(function* (chatId: ChatId, entries: ReadonlyArray<AssistantEvent.Entry>) {
225
+ const rows = yield* Effect.forEach(
226
+ entries,
227
+ Effect.fnUntraced(function* (entry: AssistantEvent.Entry) {
228
+ const event = yield* encodeEvent(entry.event)
229
+
230
+ return { chat_id: chatId, seq: entry.seq, at: entry.at, event }
231
+ })
232
+ )
233
+
234
+ yield* sql`INSERT INTO chat_log ${sql.insert(rows)}`
235
+ }, Effect.orDie),
236
+ lastEntries: Effect.gen(function* () {
237
+ const rows = yield* sql`
238
+ SELECT e.chat_id, e.seq, e.at, e.event FROM chat_log e
239
+ JOIN (SELECT chat_id, MAX(seq) AS seq FROM chat_log GROUP BY chat_id) last
240
+ ON last.chat_id = e.chat_id AND last.seq = e.seq
241
+ `
242
+
243
+ return yield* toEntries(rows)
244
+ }).pipe(Effect.orDie)
245
+ })
246
+ })
247
+ )