effect-assistant 0.1.0 → 0.3.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.
- package/README.md +1 -1
- package/dist/Assistant.d.ts +74 -28
- package/dist/Assistant.d.ts.map +1 -1
- package/dist/Assistant.js +11 -4
- package/dist/Assistant.js.map +1 -1
- package/dist/AssistantActivity.d.ts +10 -16
- package/dist/AssistantActivity.d.ts.map +1 -1
- package/dist/AssistantActivity.js +11 -4
- package/dist/AssistantActivity.js.map +1 -1
- package/dist/AssistantEngine.d.ts +39 -1
- package/dist/AssistantEngine.d.ts.map +1 -1
- package/dist/AssistantEngine.js +81 -28
- package/dist/AssistantEngine.js.map +1 -1
- package/dist/AssistantEvent.d.ts +52 -70
- package/dist/AssistantEvent.d.ts.map +1 -1
- package/dist/AssistantEvent.js +28 -57
- package/dist/AssistantEvent.js.map +1 -1
- package/dist/AssistantLoop.d.ts +7 -5
- package/dist/AssistantLoop.d.ts.map +1 -1
- package/dist/AssistantLoop.js +6 -3
- package/dist/AssistantLoop.js.map +1 -1
- package/dist/Block.js.map +1 -1
- package/dist/BlockKit.d.ts +14 -18
- package/dist/BlockKit.d.ts.map +1 -1
- package/dist/BlockKit.js +4 -7
- package/dist/BlockKit.js.map +1 -1
- package/dist/ChatLog.js +1 -1
- package/dist/Compaction.d.ts +8 -4
- package/dist/Compaction.d.ts.map +1 -1
- package/dist/Compaction.js +12 -15
- package/dist/Compaction.js.map +1 -1
- package/dist/CurrentChat.d.ts +2 -1
- package/dist/CurrentChat.d.ts.map +1 -1
- package/dist/CurrentChat.js.map +1 -1
- package/dist/Instructions.js +1 -1
- package/dist/Skill.d.ts +33 -0
- package/dist/Skill.d.ts.map +1 -1
- package/dist/Skill.js +27 -1
- package/dist/Skill.js.map +1 -1
- package/dist/Specialist.d.ts +4 -1
- package/dist/Specialist.d.ts.map +1 -1
- package/dist/Specialist.js.map +1 -1
- package/dist/Timeline.d.ts +482 -0
- package/dist/Timeline.d.ts.map +1 -0
- package/dist/Timeline.js +572 -0
- package/dist/Timeline.js.map +1 -0
- package/dist/ToolDiscovery.d.ts +4 -5
- package/dist/ToolDiscovery.d.ts.map +1 -1
- package/dist/ToolDiscovery.js +5 -4
- package/dist/ToolDiscovery.js.map +1 -1
- package/dist/ToolFailure.d.ts +1 -1
- package/dist/ToolFailure.js +1 -1
- package/dist/ToolFailure.js.map +1 -1
- package/dist/index.d.ts +2 -5
- package/dist/index.js +4 -7
- package/dist/internal/chat.js +11 -37
- package/dist/internal/chat.js.map +1 -1
- package/dist/internal/events.d.ts +15 -16
- package/dist/internal/events.d.ts.map +1 -1
- package/dist/internal/events.js +29 -28
- package/dist/internal/events.js.map +1 -1
- package/dist/internal/history.js +26 -58
- package/dist/internal/history.js.map +1 -1
- package/dist/internal/input.js +7 -0
- package/dist/internal/input.js.map +1 -0
- package/dist/internal/loop.d.ts +2 -2
- package/dist/internal/loop.d.ts.map +1 -1
- package/dist/internal/loop.js +21 -56
- package/dist/internal/loop.js.map +1 -1
- package/dist/internal/skills.js +6 -1
- package/dist/internal/skills.js.map +1 -1
- package/dist/internal/specialists.js +7 -7
- package/dist/internal/specialists.js.map +1 -1
- package/dist/internal/toolkit.d.ts +6 -20
- package/dist/internal/toolkit.d.ts.map +1 -1
- package/dist/internal/toolkit.js +10 -31
- package/dist/internal/toolkit.js.map +1 -1
- package/dist/internal/tools.js +2 -21
- package/dist/internal/tools.js.map +1 -1
- package/dist/internal/turns.d.ts.map +1 -1
- package/dist/internal/turns.js +24 -43
- package/dist/internal/turns.js.map +1 -1
- package/package.json +3 -2
- package/src/Assistant.ts +75 -29
- package/src/AssistantActivity.ts +11 -18
- package/src/AssistantEngine.ts +110 -43
- package/src/AssistantEvent.ts +46 -74
- package/src/AssistantLoop.ts +7 -5
- package/src/Block.ts +1 -1
- package/src/BlockKit.ts +18 -31
- package/src/Compaction.ts +8 -21
- package/src/CurrentChat.ts +2 -1
- package/src/Skill.ts +34 -1
- package/src/Specialist.ts +4 -1
- package/src/Timeline.ts +861 -0
- package/src/ToolDiscovery.ts +5 -13
- package/src/ToolFailure.ts +1 -1
- package/src/index.ts +5 -20
- package/src/internal/chat.ts +8 -57
- package/src/internal/events.ts +43 -43
- package/src/internal/history.ts +36 -101
- package/src/internal/input.ts +3 -0
- package/src/internal/loop.ts +31 -101
- package/src/internal/skills.ts +11 -0
- package/src/internal/specialists.ts +6 -9
- package/src/internal/toolkit.ts +17 -60
- package/src/internal/tools.ts +2 -53
- package/src/internal/turns.ts +37 -79
- package/dist/AppEvent.d.ts +0 -111
- package/dist/AppEvent.d.ts.map +0 -1
- package/dist/AppEvent.js +0 -71
- package/dist/AppEvent.js.map +0 -1
- package/dist/ChatView.d.ts +0 -459
- package/dist/ChatView.d.ts.map +0 -1
- package/dist/ChatView.js +0 -496
- package/dist/ChatView.js.map +0 -1
- package/dist/Decision.d.ts +0 -145
- package/dist/Decision.d.ts.map +0 -1
- package/dist/Decision.js +0 -178
- package/dist/Decision.js.map +0 -1
- package/dist/DecisionId.d.ts +0 -37
- package/dist/DecisionId.d.ts.map +0 -1
- package/dist/DecisionId.js +0 -44
- package/dist/DecisionId.js.map +0 -1
- package/dist/internal/decisions.js +0 -66
- package/dist/internal/decisions.js.map +0 -1
- package/src/AppEvent.ts +0 -134
- package/src/ChatView.ts +0 -658
- package/src/Decision.ts +0 -241
- package/src/DecisionId.ts +0 -46
- package/src/internal/decisions.ts +0 -131
package/src/AssistantEngine.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* This module provides the `AssistantEngine` service, which runs assistants' chats, and its
|
|
2
|
+
* This module provides the `AssistantEngine` service, which runs assistants' chats, and its
|
|
3
3
|
* implementations.
|
|
4
4
|
*
|
|
5
5
|
* An assistant's `toLayer` registers the assistant with the engine, and the assistant's `submit`
|
|
6
6
|
* and `subscribe` go through it. Pick one engine for the app, as with a workflow engine.
|
|
7
7
|
* {@link layerMemory} runs chats in the process, for tests and single-process apps.
|
|
8
|
-
* {@link layerCluster} runs each chat as a cluster entity, so a turn cut off by a crash resumes
|
|
8
|
+
* {@link layerCluster} runs each chat as a cluster entity, so a turn cut off by a crash resumes, and
|
|
9
|
+
* {@link layerClusterClient} reaches those chats from a process that runs none.
|
|
9
10
|
*
|
|
10
11
|
* @since 0.1.0
|
|
11
12
|
*/
|
|
@@ -155,7 +156,7 @@ export const layerMemory: Layer.Layer<AssistantEngine, never, ChatLog> = Layer.e
|
|
|
155
156
|
|
|
156
157
|
// A chat owes a turn when its last turn was cut off, or an input came after.
|
|
157
158
|
const resumeAll = Effect.fnUntraced(function* (registration: Turns.Registration) {
|
|
158
|
-
const { name
|
|
159
|
+
const { name } = registration.definition
|
|
159
160
|
|
|
160
161
|
for (const chatId of yield* log.unfinished) {
|
|
161
162
|
const entries = yield* log.read(chatId)
|
|
@@ -163,7 +164,7 @@ export const layerMemory: Layer.Layer<AssistantEngine, never, ChatLog> = Layer.e
|
|
|
163
164
|
const lastTurn = entries.findLast(({ event }) => Predicate.isTagged(event, "TurnStarted"))?.event
|
|
164
165
|
const ours = Predicate.isTagged(lastTurn, "TurnStarted") && lastTurn.assistant === name
|
|
165
166
|
|
|
166
|
-
if (ours && History.needsTurn(entries
|
|
167
|
+
if (ours && History.needsTurn(entries)) yield* withChat(name, chatId, (chat) => chat.resume)
|
|
167
168
|
}
|
|
168
169
|
})
|
|
169
170
|
|
|
@@ -212,6 +213,59 @@ const Delete = Rpc.make("Delete")
|
|
|
212
213
|
|
|
213
214
|
const chatEntity = (assistant: string) => Entity.make(`Assistant/${assistant}`, [Submit, Subscribe, Delete])
|
|
214
215
|
|
|
216
|
+
type EngineClient = Omit<AssistantEngine["Service"], "register">
|
|
217
|
+
|
|
218
|
+
// Entity clients are made on first use, so a process that registers no assistant can still reach its chats.
|
|
219
|
+
const makeClusterClient = Effect.gen(function* () {
|
|
220
|
+
const context = yield* Effect.context<Sharding.Sharding>()
|
|
221
|
+
const clients = new Map<string, EntityClient>()
|
|
222
|
+
|
|
223
|
+
const clientOf = Effect.fnUntraced(function* (assistant: string) {
|
|
224
|
+
const known = clients.get(assistant)
|
|
225
|
+
|
|
226
|
+
if (known !== undefined) return known
|
|
227
|
+
|
|
228
|
+
const makeClient = yield* chatEntity(assistant).client.pipe(Effect.provide(context))
|
|
229
|
+
|
|
230
|
+
const client: EntityClient = {
|
|
231
|
+
submit: (chatId, input) => makeClient(chatId).Submit({ input }, { discard: true }).pipe(Effect.orDie),
|
|
232
|
+
subscribe: (chatId, after) => makeClient(chatId).Subscribe({ after }).pipe(Stream.orDie),
|
|
233
|
+
delete: (chatId) => makeClient(chatId).Delete().pipe(Effect.orDie)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
clients.set(assistant, client)
|
|
237
|
+
|
|
238
|
+
return client
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
const engine: EngineClient = {
|
|
242
|
+
submit: Effect.fn("AssistantEngine.submit")(function* (
|
|
243
|
+
assistant: string,
|
|
244
|
+
chatId: ChatId,
|
|
245
|
+
input: AssistantEvent.Input | AssistantEvent.StoredAppEvent
|
|
246
|
+
) {
|
|
247
|
+
const client = yield* clientOf(assistant)
|
|
248
|
+
|
|
249
|
+
yield* client.submit(chatId, input)
|
|
250
|
+
}),
|
|
251
|
+
subscribe: (assistant, chatId, after) =>
|
|
252
|
+
Stream.unwrap(
|
|
253
|
+
Effect.gen(function* () {
|
|
254
|
+
const client = yield* clientOf(assistant)
|
|
255
|
+
|
|
256
|
+
return client.subscribe(chatId, after)
|
|
257
|
+
})
|
|
258
|
+
),
|
|
259
|
+
delete: Effect.fn("AssistantEngine.delete")(function* (assistant: string, chatId: ChatId) {
|
|
260
|
+
const client = yield* clientOf(assistant)
|
|
261
|
+
|
|
262
|
+
yield* client.delete(chatId)
|
|
263
|
+
})
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return engine
|
|
267
|
+
})
|
|
268
|
+
|
|
215
269
|
/**
|
|
216
270
|
* Layer that runs chats as cluster entities, one entity per chat, keyed by chat id.
|
|
217
271
|
*
|
|
@@ -249,6 +303,7 @@ const chatEntity = (assistant: string) => Entity.make(`Assistant/${assistant}`,
|
|
|
249
303
|
* ```
|
|
250
304
|
*
|
|
251
305
|
* @see {@link layerMemory} for running chats in one process
|
|
306
|
+
* @see {@link layerClusterClient} for a process that only reaches chats the cluster runs
|
|
252
307
|
*
|
|
253
308
|
* @category layers
|
|
254
309
|
* @since 0.1.0
|
|
@@ -258,7 +313,6 @@ export const layerCluster: Layer.Layer<AssistantEngine, never, ChatLog | Shardin
|
|
|
258
313
|
Effect.gen(function* () {
|
|
259
314
|
const context = yield* Effect.context<ChatLog | Sharding.Sharding>()
|
|
260
315
|
const sharding = yield* Sharding.Sharding
|
|
261
|
-
const clients = new Map<string, EntityClient>()
|
|
262
316
|
|
|
263
317
|
const register = Effect.fnUntraced(function* (registration: Turns.Registration) {
|
|
264
318
|
const ChatEntity = chatEntity(registration.definition.name)
|
|
@@ -295,49 +349,62 @@ export const layerCluster: Layer.Layer<AssistantEngine, never, ChatLog | Shardin
|
|
|
295
349
|
})
|
|
296
350
|
})
|
|
297
351
|
|
|
298
|
-
|
|
299
|
-
const clientOf = Effect.fnUntraced(function* (assistant: string) {
|
|
300
|
-
const known = clients.get(assistant)
|
|
301
|
-
|
|
302
|
-
if (known !== undefined) return known
|
|
303
|
-
|
|
304
|
-
const makeClient = yield* chatEntity(assistant).client.pipe(Effect.provide(context))
|
|
305
|
-
|
|
306
|
-
const client: EntityClient = {
|
|
307
|
-
submit: (chatId, input) => makeClient(chatId).Submit({ input }, { discard: true }).pipe(Effect.orDie),
|
|
308
|
-
subscribe: (chatId, after) => makeClient(chatId).Subscribe({ after }).pipe(Stream.orDie),
|
|
309
|
-
delete: (chatId) => makeClient(chatId).Delete().pipe(Effect.orDie)
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
clients.set(assistant, client)
|
|
313
|
-
|
|
314
|
-
return client
|
|
315
|
-
})
|
|
352
|
+
const client = yield* makeClusterClient
|
|
316
353
|
|
|
317
354
|
return AssistantEngine.of({
|
|
318
355
|
register: (registration) => register(registration).pipe(Effect.provide(context)),
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
) {
|
|
324
|
-
const client = yield* clientOf(assistant)
|
|
325
|
-
|
|
326
|
-
yield* client.submit(chatId, input)
|
|
327
|
-
}),
|
|
328
|
-
subscribe: (assistant, chatId, after) =>
|
|
329
|
-
Stream.unwrap(
|
|
330
|
-
Effect.gen(function* () {
|
|
331
|
-
const client = yield* clientOf(assistant)
|
|
356
|
+
...client
|
|
357
|
+
})
|
|
358
|
+
})
|
|
359
|
+
)
|
|
332
360
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
361
|
+
/**
|
|
362
|
+
* Layer that reaches chats running as cluster entities elsewhere, and runs none itself.
|
|
363
|
+
*
|
|
364
|
+
* **When to use**
|
|
365
|
+
*
|
|
366
|
+
* Use it in a process that only submits to, subscribes to, and deletes chats that the cluster's
|
|
367
|
+
* runners run with {@link layerCluster}, such as a web server that provides an assistant's
|
|
368
|
+
* `layerClient`. It needs no `ChatLog`, since the runners store the chats.
|
|
369
|
+
*
|
|
370
|
+
* **Details**
|
|
371
|
+
*
|
|
372
|
+
* The layer requires `Sharding`. An assistant's `toLayer` can't register with it: it dies, since
|
|
373
|
+
* no chats run in this process.
|
|
374
|
+
*
|
|
375
|
+
* **Example** (A web server in front of the cluster)
|
|
376
|
+
*
|
|
377
|
+
* ```ts
|
|
378
|
+
* import { Assistant, AssistantEngine } from "effect-assistant"
|
|
379
|
+
* import type * as Sharding from "effect/cluster/Sharding"
|
|
380
|
+
* import * as Layer from "effect/Layer"
|
|
381
|
+
*
|
|
382
|
+
* declare const ShardingLive: Layer.Layer<Sharding.Sharding>
|
|
383
|
+
*
|
|
384
|
+
* export const MyAssistant = Assistant.make("my-assistant")
|
|
385
|
+
*
|
|
386
|
+
* const MyAssistantClient = MyAssistant.layerClient.pipe(
|
|
387
|
+
* Layer.provide(AssistantEngine.layerClusterClient),
|
|
388
|
+
* Layer.provide(ShardingLive)
|
|
389
|
+
* )
|
|
390
|
+
* ```
|
|
391
|
+
*
|
|
392
|
+
* @see {@link layerCluster} for the runners that run the chats
|
|
393
|
+
*
|
|
394
|
+
* @category layers
|
|
395
|
+
* @since 0.3.0
|
|
396
|
+
*/
|
|
397
|
+
export const layerClusterClient: Layer.Layer<AssistantEngine, never, Sharding.Sharding> = Layer.effect(
|
|
398
|
+
AssistantEngine,
|
|
399
|
+
Effect.gen(function* () {
|
|
400
|
+
const client = yield* makeClusterClient
|
|
338
401
|
|
|
339
|
-
|
|
340
|
-
|
|
402
|
+
return AssistantEngine.of({
|
|
403
|
+
register: (registration) =>
|
|
404
|
+
Effect.die(
|
|
405
|
+
`The ${registration.definition.name} assistant can't register with AssistantEngine.layerClusterClient, which runs no chats: provide AssistantEngine.layerCluster where its toLayer runs.`
|
|
406
|
+
),
|
|
407
|
+
...client
|
|
341
408
|
})
|
|
342
409
|
})
|
|
343
410
|
)
|
package/src/AssistantEvent.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Defines the events that can happen in a chat.
|
|
3
3
|
*
|
|
4
4
|
* A chat is an append-only log of these events, and the log is the source of truth. A chat client
|
|
5
|
-
* receives the part of the log its user sees (`
|
|
5
|
+
* receives the part of the log its user sees (its `Timeline`), and the model's history is rebuilt
|
|
6
6
|
* from the log before every model call. Besides the library's events ({@link AssistantEvent}), a
|
|
7
7
|
* log holds the application's own events ({@link StoredAppEvent}).
|
|
8
8
|
*
|
|
@@ -11,9 +11,10 @@
|
|
|
11
11
|
import * as Response from "effect/ai/Response"
|
|
12
12
|
import * as Predicate from "effect/Predicate"
|
|
13
13
|
import * as Schema from "effect/Schema"
|
|
14
|
+
import type * as SchemaAST from "effect/SchemaAST"
|
|
14
15
|
import { ChatId } from "./ChatId.ts"
|
|
15
|
-
import { DecisionId } from "./DecisionId.ts"
|
|
16
16
|
import { InputId, make as makeInputId } from "./InputId.ts"
|
|
17
|
+
import { inputKey } from "./internal/input.ts"
|
|
17
18
|
import { RunId } from "./RunId.ts"
|
|
18
19
|
|
|
19
20
|
/**
|
|
@@ -139,69 +140,6 @@ export class SpecialistFinished extends Schema.TaggedClass<SpecialistFinished>()
|
|
|
139
140
|
error: Schema.optional(Schema.String)
|
|
140
141
|
}) {}
|
|
141
142
|
|
|
142
|
-
/**
|
|
143
|
-
* Schema for how the user decided, either `"approved"` or `"declined"`.
|
|
144
|
-
*
|
|
145
|
-
* @category schemas
|
|
146
|
-
* @since 0.1.0
|
|
147
|
-
*/
|
|
148
|
-
export const DecisionOutcome = Schema.Literals(["approved", "declined"])
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Represents how the user decided.
|
|
152
|
-
*
|
|
153
|
-
* @category models
|
|
154
|
-
* @since 0.1.0
|
|
155
|
-
*/
|
|
156
|
-
export type DecisionOutcome = typeof DecisionOutcome.Type
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Schema for the origin of a decision that a tool call waits for.
|
|
160
|
-
*
|
|
161
|
-
* It holds the call as the model wrote it. Approving the decision runs exactly this call.
|
|
162
|
-
*
|
|
163
|
-
* @category schemas
|
|
164
|
-
* @since 0.1.0
|
|
165
|
-
*/
|
|
166
|
-
export const CallOrigin = Schema.TaggedStruct("Call", {
|
|
167
|
-
callId: Schema.String,
|
|
168
|
-
tool: Schema.String,
|
|
169
|
-
params: Schema.Json
|
|
170
|
-
})
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* Represents the origin of a decision that a tool call waits for.
|
|
174
|
-
*
|
|
175
|
-
* @category models
|
|
176
|
-
* @since 0.1.0
|
|
177
|
-
*/
|
|
178
|
-
export type CallOrigin = typeof CallOrigin.Type
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* Represents a request for the user to decide something, by approving or declining it.
|
|
182
|
-
*
|
|
183
|
-
* @category schemas
|
|
184
|
-
* @since 0.1.0
|
|
185
|
-
*/
|
|
186
|
-
export class DecisionRequested extends Schema.TaggedClass<DecisionRequested>()("DecisionRequested", {
|
|
187
|
-
id: DecisionId,
|
|
188
|
-
origin: CallOrigin
|
|
189
|
-
}) {}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* Represents a decision the user made by answering a decision block for it.
|
|
193
|
-
*
|
|
194
|
-
* `answer` is the id of that `BlockAnswer`. An approved call runs at the turn's next step.
|
|
195
|
-
*
|
|
196
|
-
* @category schemas
|
|
197
|
-
* @since 0.1.0
|
|
198
|
-
*/
|
|
199
|
-
export class DecisionResolved extends Schema.TaggedClass<DecisionResolved>()("DecisionResolved", {
|
|
200
|
-
id: DecisionId,
|
|
201
|
-
outcome: DecisionOutcome,
|
|
202
|
-
answer: InputId
|
|
203
|
-
}) {}
|
|
204
|
-
|
|
205
143
|
/**
|
|
206
144
|
* Schema for every input `Assistant.submit` accepts.
|
|
207
145
|
*
|
|
@@ -481,8 +419,6 @@ export const AssistantEvent = Schema.Union([
|
|
|
481
419
|
SpecialistStarted,
|
|
482
420
|
SpecialistFinished,
|
|
483
421
|
BlocksClosed,
|
|
484
|
-
DecisionRequested,
|
|
485
|
-
DecisionResolved,
|
|
486
422
|
ChatDeleted
|
|
487
423
|
])
|
|
488
424
|
|
|
@@ -510,19 +446,43 @@ const libraryTags: ReadonlySet<string> = new Set(
|
|
|
510
446
|
*/
|
|
511
447
|
export const AppTag = Schema.String.pipe(Schema.brand("~effect-assistant/AppTag"))
|
|
512
448
|
|
|
449
|
+
/**
|
|
450
|
+
* Represents the schema of one of the application's own events, as `Assistant.make`'s `events`
|
|
451
|
+
* takes it.
|
|
452
|
+
*
|
|
453
|
+
* **Details**
|
|
454
|
+
*
|
|
455
|
+
* It is a tagged struct or class (`Schema.TaggedStruct`, `Schema.TaggedClass`) whose `_tag` is a
|
|
456
|
+
* string literal other than the library's, and that needs no services. The log stores the event in
|
|
457
|
+
* the schema's JSON form.
|
|
458
|
+
*
|
|
459
|
+
* @category models
|
|
460
|
+
* @since 0.3.0
|
|
461
|
+
*/
|
|
462
|
+
export type AppEventSchema = Schema.Codec<{ readonly _tag: string }, { readonly _tag: string }> & {
|
|
463
|
+
readonly fields: { readonly _tag: { readonly ast: SchemaAST.AST } }
|
|
464
|
+
}
|
|
465
|
+
|
|
513
466
|
/**
|
|
514
467
|
* Schema for one of the application's own events as it is stored, with its `_tag` and its fields
|
|
515
468
|
* in JSON.
|
|
516
469
|
*
|
|
517
|
-
*
|
|
518
|
-
*
|
|
470
|
+
* **Details**
|
|
471
|
+
*
|
|
472
|
+
* An assistant made with `events: [Summary]` decodes it to a `Summary` in `CurrentChat`'s
|
|
473
|
+
* `entries` and in its `subscribe`.
|
|
474
|
+
*
|
|
475
|
+
* An event that came in through the assistant's `submit`, from outside a turn, is an input, and the
|
|
476
|
+
* log marks it with a `"~effect-assistant/input": true` key (see {@link isAppInput}); one a turn
|
|
477
|
+
* appended has none. Decoding drops the key, so the app's schemas never see it.
|
|
519
478
|
*
|
|
520
479
|
* @category schemas
|
|
521
480
|
* @since 0.1.0
|
|
522
481
|
*/
|
|
523
|
-
export const StoredAppEvent = Schema.StructWithRest(
|
|
524
|
-
Schema.
|
|
525
|
-
|
|
482
|
+
export const StoredAppEvent = Schema.StructWithRest(
|
|
483
|
+
Schema.Struct({ _tag: AppTag, [inputKey]: Schema.optionalKey(Schema.Literal(true)) }),
|
|
484
|
+
[Schema.Record(Schema.String, Schema.Json)]
|
|
485
|
+
).check(Schema.makeFilter((event) => !libraryTags.has(event._tag) || `${event._tag} is one of the library's events`))
|
|
526
486
|
|
|
527
487
|
/**
|
|
528
488
|
* Represents one of the application's own events, as it is stored.
|
|
@@ -540,6 +500,18 @@ export type StoredAppEvent = typeof StoredAppEvent.Type
|
|
|
540
500
|
*/
|
|
541
501
|
export const isLibraryEvent = (event: { readonly _tag: string }): event is AssistantEvent => libraryTags.has(event._tag)
|
|
542
502
|
|
|
503
|
+
/**
|
|
504
|
+
* Returns whether one of the application's events, as the log stores it, is an input: it came in
|
|
505
|
+
* through the assistant's `submit`, from outside a turn, so storing it woke a turn. An event a turn
|
|
506
|
+
* appended (`CurrentChat`'s `append`) isn't, and neither is a decoded one, which no longer has the
|
|
507
|
+
* mark.
|
|
508
|
+
*
|
|
509
|
+
* @category guards
|
|
510
|
+
* @since 0.3.0
|
|
511
|
+
*/
|
|
512
|
+
export const isAppInput = (event: { readonly _tag: string }): boolean =>
|
|
513
|
+
!isLibraryEvent(event) && Predicate.hasProperty(event, inputKey) && event[inputKey] === true
|
|
514
|
+
|
|
543
515
|
/**
|
|
544
516
|
* Represents a chat's entry with any event in it, either the library's or the application's own
|
|
545
517
|
* (decoded or not).
|
|
@@ -584,8 +556,8 @@ export type Entry = typeof Entry.Type
|
|
|
584
556
|
* Returns whether an event is one of the library's inputs, which are a user message, a block
|
|
585
557
|
* answer, or a finished specialist run.
|
|
586
558
|
*
|
|
587
|
-
* The application's events that
|
|
588
|
-
*
|
|
559
|
+
* The application's events that came in through the assistant's `submit` are inputs too (see
|
|
560
|
+
* {@link isAppInput}).
|
|
589
561
|
*
|
|
590
562
|
* @category guards
|
|
591
563
|
* @since 0.1.0
|
package/src/AssistantLoop.ts
CHANGED
|
@@ -27,8 +27,7 @@ import * as Toolkits from "./internal/toolkit.ts"
|
|
|
27
27
|
* Represents what the loop emits.
|
|
28
28
|
*
|
|
29
29
|
* For each model call, the loop emits the call's output. Then, for each of its tool calls, it
|
|
30
|
-
* emits a `ToolStarted` before the call runs and the call's result
|
|
31
|
-
* a call that waits for the user's decision.
|
|
30
|
+
* emits a `ToolStarted` before the call runs and the call's result.
|
|
32
31
|
*
|
|
33
32
|
* @category models
|
|
34
33
|
* @since 0.1.0
|
|
@@ -158,9 +157,12 @@ const toolsOf = <T extends Record<string, Tool.Any>>(toolkit: Toolkit.Toolkit<T>
|
|
|
158
157
|
* **Details**
|
|
159
158
|
*
|
|
160
159
|
* The step calls the model with the current history and emits its `ModelOutput`. It then runs
|
|
161
|
-
* the output's tool calls and emits a `ToolStarted` and a `ToolResult` for each
|
|
162
|
-
*
|
|
163
|
-
*
|
|
160
|
+
* the output's tool calls and emits a `ToolStarted` and a `ToolResult` for each. `then` receives
|
|
161
|
+
* the step's result and decides what comes next, with `AssistantLoop.next(state)` or
|
|
162
|
+
* `AssistantLoop.stop()`.
|
|
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
166
|
*
|
|
165
167
|
* @category constructors
|
|
166
168
|
* @since 0.1.0
|
package/src/Block.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Defines UI blocks that an assistant can put in a message.
|
|
3
3
|
*
|
|
4
4
|
* A block is a bit of UI for when a structured answer is easier than typing, such as options to
|
|
5
|
-
* pick from,
|
|
5
|
+
* pick from, a yes or no, or a small input. The assistant fills in its `props`, and
|
|
6
6
|
* using the block sends back a value typed by its `answer` schema.
|
|
7
7
|
*
|
|
8
8
|
* @since 0.1.0
|
package/src/BlockKit.ts
CHANGED
|
@@ -14,9 +14,8 @@ import * as Predicate from "effect/Predicate"
|
|
|
14
14
|
import * as Schema from "effect/Schema"
|
|
15
15
|
import * as AssistantEvent from "./AssistantEvent.ts"
|
|
16
16
|
import type * as Block from "./Block.ts"
|
|
17
|
-
import type * as
|
|
17
|
+
import type * as Timeline from "./Timeline.ts"
|
|
18
18
|
import { CurrentChat } from "./CurrentChat.ts"
|
|
19
|
-
import * as Decision from "./Decision.ts"
|
|
20
19
|
import * as InputId from "./InputId.ts"
|
|
21
20
|
|
|
22
21
|
/**
|
|
@@ -118,8 +117,7 @@ export const fromEntries = (entries: ReadonlyArray<AssistantEvent.AnyEntry>): Re
|
|
|
118
117
|
*
|
|
119
118
|
* **Details**
|
|
120
119
|
*
|
|
121
|
-
* These are the blocks {@link closeStale} closes.
|
|
122
|
-
* on them and closing one would leave it waiting.
|
|
120
|
+
* These are the blocks {@link closeStale} closes.
|
|
123
121
|
*
|
|
124
122
|
* @category utils
|
|
125
123
|
* @since 0.1.0
|
|
@@ -136,15 +134,7 @@ export const staleBlocks = (
|
|
|
136
134
|
AssistantEvent.isLibraryEvent(event) && Predicate.isTagged(event, "AssistantMessage") && event.id === id
|
|
137
135
|
)
|
|
138
136
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
if (
|
|
142
|
-
message === undefined ||
|
|
143
|
-
(AssistantEvent.isLibraryEvent(message) &&
|
|
144
|
-
Predicate.isTagged(message, "AssistantMessage") &&
|
|
145
|
-
message.block?.kind === Decision.kind)
|
|
146
|
-
)
|
|
147
|
-
return false
|
|
137
|
+
if (shown === -1) return false
|
|
148
138
|
|
|
149
139
|
const typed = entries.slice(shown + 1).filter(({ event }) => Predicate.isTagged(event, "UserMessage")).length
|
|
150
140
|
|
|
@@ -180,6 +170,11 @@ export const closeStale = Effect.fn(function* (options: { readonly messages: num
|
|
|
180
170
|
* @since 0.1.0
|
|
181
171
|
*/
|
|
182
172
|
export interface RenderArgs<B> {
|
|
173
|
+
/**
|
|
174
|
+
* The id of the message the block is on. A block whose button calls your API can key the work
|
|
175
|
+
* on it, with the chat's id, so the work runs once per block.
|
|
176
|
+
*/
|
|
177
|
+
readonly messageId: string
|
|
183
178
|
readonly text: string
|
|
184
179
|
readonly props: PropsOf<B>
|
|
185
180
|
/**
|
|
@@ -189,11 +184,6 @@ export interface RenderArgs<B> {
|
|
|
189
184
|
readonly open: boolean
|
|
190
185
|
/** The user's answer, if they used the block. */
|
|
191
186
|
readonly answer: AnswerOf<B> | undefined
|
|
192
|
-
/**
|
|
193
|
-
* For the decision block, the call each decision is for (its tool and params) and how it was
|
|
194
|
-
* decided. Empty for other blocks.
|
|
195
|
-
*/
|
|
196
|
-
readonly decisions: ReadonlyArray<ChatView.PendingDecision>
|
|
197
187
|
/** Sends an answer. Only call it while `open`. */
|
|
198
188
|
readonly respond: (answer: AnswerOf<B>) => void
|
|
199
189
|
}
|
|
@@ -220,8 +210,8 @@ export interface RenderOptions {
|
|
|
220
210
|
}
|
|
221
211
|
|
|
222
212
|
/**
|
|
223
|
-
* Creates a client-side renderer for the blocks on a chat
|
|
224
|
-
* (`
|
|
213
|
+
* Creates a client-side renderer for the blocks on the assistant's messages in a chat's timeline
|
|
214
|
+
* (`Timeline.AssistantMessage`).
|
|
225
215
|
*
|
|
226
216
|
* **Details**
|
|
227
217
|
*
|
|
@@ -235,10 +225,10 @@ export interface RenderOptions {
|
|
|
235
225
|
* **Example** (Rendering blocks with React)
|
|
236
226
|
*
|
|
237
227
|
* ```tsx
|
|
238
|
-
* import { BlockKit
|
|
228
|
+
* import { BlockKit } from "effect-assistant"
|
|
239
229
|
* import type { ReactNode } from "react"
|
|
240
230
|
*
|
|
241
|
-
* const Blocks = BlockKit.make([PickOne,
|
|
231
|
+
* const Blocks = BlockKit.make([PickOne, OrderDraft])
|
|
242
232
|
*
|
|
243
233
|
* const renderBlock = BlockKit.toClient<typeof Blocks, ReactNode>(Blocks, {
|
|
244
234
|
* pick_one: ({ props, open, respond }) =>
|
|
@@ -247,14 +237,11 @@ export interface RenderOptions {
|
|
|
247
237
|
* {option}
|
|
248
238
|
* </button>
|
|
249
239
|
* )),
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
* {props.approveLabel ?? "Approve"}
|
|
253
|
-
* </button>
|
|
254
|
-
* )
|
|
240
|
+
* // No answer schema: the button calls the app's own API with the props the user sees.
|
|
241
|
+
* order_draft: ({ props }) => <button onClick={() => placeOrder(props)}>Place order</button>
|
|
255
242
|
* })
|
|
256
243
|
*
|
|
257
|
-
* // For each AssistantMessage in the chat's
|
|
244
|
+
* // For each AssistantMessage in the chat's timeline:
|
|
258
245
|
* renderBlock(message, { respond: (answer) => submit(answer) })
|
|
259
246
|
* ```
|
|
260
247
|
*
|
|
@@ -264,13 +251,13 @@ export interface RenderOptions {
|
|
|
264
251
|
export function toClient<Kit extends BlockKit, Out>(
|
|
265
252
|
kit: Kit,
|
|
266
253
|
renderers: Renderers<Kit, Out>
|
|
267
|
-
): (message:
|
|
254
|
+
): (message: Timeline.AssistantMessage, options: RenderOptions) => Out | undefined
|
|
268
255
|
// The overload above checks that every kind has a renderer; here each one takes any block's
|
|
269
256
|
// arguments.
|
|
270
257
|
export function toClient<Out>(
|
|
271
258
|
kit: BlockKit,
|
|
272
259
|
renderers: Readonly<Record<string, (args: RenderArgs<Block.Any>) => Out>>
|
|
273
|
-
): (message:
|
|
260
|
+
): (message: Timeline.AssistantMessage, options: RenderOptions) => Out | undefined {
|
|
274
261
|
return (message, options) => {
|
|
275
262
|
if (message.block === undefined) return undefined
|
|
276
263
|
|
|
@@ -291,11 +278,11 @@ export function toClient<Out>(
|
|
|
291
278
|
given === undefined || answerCodec === undefined ? Option.none() : Schema.decodeUnknownOption(answerCodec)(given)
|
|
292
279
|
|
|
293
280
|
return render({
|
|
281
|
+
messageId: message.id,
|
|
294
282
|
text: message.text,
|
|
295
283
|
props: props.value,
|
|
296
284
|
open: message.block.status === "open",
|
|
297
285
|
answer: Option.getOrUndefined(answer),
|
|
298
|
-
decisions: message.block.decisions ?? [],
|
|
299
286
|
respond: (value) =>
|
|
300
287
|
options.respond(
|
|
301
288
|
AssistantEvent.BlockAnswer.make({
|
package/src/Compaction.ts
CHANGED
|
@@ -17,9 +17,7 @@ import type * as Assistant from "./Assistant.ts"
|
|
|
17
17
|
import * as AssistantActivity from "./AssistantActivity.ts"
|
|
18
18
|
import * as AssistantEvent from "./AssistantEvent.ts"
|
|
19
19
|
import * as BlockKit from "./BlockKit.ts"
|
|
20
|
-
import * as Decision from "./Decision.ts"
|
|
21
20
|
import { userText } from "./internal/history.ts"
|
|
22
|
-
import { toJsonText } from "./internal/schema.ts"
|
|
23
21
|
|
|
24
22
|
/**
|
|
25
23
|
* Represents the fields a summary event must have: its text and the last `seq` it covers.
|
|
@@ -55,8 +53,8 @@ export const inputTokens = (entries: ReadonlyArray<AssistantEvent.AnyEntry>): nu
|
|
|
55
53
|
}
|
|
56
54
|
|
|
57
55
|
/**
|
|
58
|
-
* Returns the items that were open at `throughSeq` and are still open:
|
|
59
|
-
*
|
|
56
|
+
* 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
|
|
60
58
|
* the model, or `None` when nothing is open.
|
|
61
59
|
*
|
|
62
60
|
* @category utils
|
|
@@ -71,20 +69,6 @@ export const openItems = (
|
|
|
71
69
|
|
|
72
70
|
const opened = (seq: number) => seq <= throughSeq
|
|
73
71
|
|
|
74
|
-
const decisions = [...Decision.fromEntries(entries).values()].flatMap((state) =>
|
|
75
|
-
state.outcome === undefined &&
|
|
76
|
-
opened(
|
|
77
|
-
seqOf(
|
|
78
|
-
(event) =>
|
|
79
|
-
AssistantEvent.isLibraryEvent(event) &&
|
|
80
|
-
Predicate.isTagged(event, "DecisionRequested") &&
|
|
81
|
-
event.id === state.request.id
|
|
82
|
-
)
|
|
83
|
-
)
|
|
84
|
-
? [`- decision ${state.request.id}, for ${state.request.origin.tool}: ${toJsonText(state.request.origin.params)}`]
|
|
85
|
-
: []
|
|
86
|
-
)
|
|
87
|
-
|
|
88
72
|
const blocks = [...BlockKit.fromEntries(entries)].flatMap(([messageId, state]) => {
|
|
89
73
|
const shown = entries.find(
|
|
90
74
|
({ event }) =>
|
|
@@ -117,7 +101,6 @@ export const openItems = (
|
|
|
117
101
|
)
|
|
118
102
|
|
|
119
103
|
const sections = [
|
|
120
|
-
decisions.length > 0 ? `Decisions waiting for the user:\n${decisions.join("\n")}` : "",
|
|
121
104
|
blocks.length > 0 ? `Blocks the user hasn't answered:\n${blocks.join("\n")}` : "",
|
|
122
105
|
specialists.length > 0 ? `Specialists still working:\n${specialists.join("\n")}` : ""
|
|
123
106
|
].filter((section) => section.length > 0)
|
|
@@ -155,10 +138,14 @@ export const attachedFiles = (
|
|
|
155
138
|
* ({@link openItems}), followed by the entries after the summary. When the chat has no summary,
|
|
156
139
|
* the view returns every entry.
|
|
157
140
|
*
|
|
141
|
+
* The app's other events after the summary stay entries, which the model doesn't read: map those
|
|
142
|
+
* it should read to messages after this view, as with any history view (see
|
|
143
|
+
* `Assistant.HistoryView`). A summary the handler appends wakes no turn.
|
|
144
|
+
*
|
|
158
145
|
* **Example** (Compacting a long chat after each turn)
|
|
159
146
|
*
|
|
160
147
|
* ```ts
|
|
161
|
-
* import {
|
|
148
|
+
* import { Assistant, type AssistantEvent, Compaction } from "effect-assistant"
|
|
162
149
|
* import * as LanguageModel from "effect/ai/LanguageModel"
|
|
163
150
|
* import * as Effect from "effect/Effect"
|
|
164
151
|
* import * as Schema from "effect/Schema"
|
|
@@ -171,7 +158,7 @@ export const attachedFiles = (
|
|
|
171
158
|
*
|
|
172
159
|
* declare const transcript: (entries: ReadonlyArray<AssistantEvent.AnyEntry>) => string
|
|
173
160
|
*
|
|
174
|
-
* export const MyAssistant = Assistant.make("my-assistant", { events: [
|
|
161
|
+
* export const MyAssistant = Assistant.make("my-assistant", { events: [Summary] })
|
|
175
162
|
*
|
|
176
163
|
* const MyAssistantLive = MyAssistant.toLayer((turn) => {
|
|
177
164
|
* const compact = Effect.gen(function* () {
|
package/src/CurrentChat.ts
CHANGED
|
@@ -40,7 +40,8 @@ export class CurrentChat extends Context.Service<
|
|
|
40
40
|
/**
|
|
41
41
|
* Appends events after the chat's last entry and sends them to the chat's subscribers. The
|
|
42
42
|
* events can be the library's or the assistant's own (registered with its `events` option),
|
|
43
|
-
* such as a summary. An event
|
|
43
|
+
* such as a summary. An app event appended here wakes no turn: only one that comes in through
|
|
44
|
+
* the assistant's `submit` does.
|
|
44
45
|
*/
|
|
45
46
|
readonly append: (...events: ReadonlyArray<{ readonly _tag: string }>) => Effect.Effect<void>
|
|
46
47
|
}
|