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.
Files changed (131) hide show
  1. package/README.md +1 -1
  2. package/dist/Assistant.d.ts +74 -28
  3. package/dist/Assistant.d.ts.map +1 -1
  4. package/dist/Assistant.js +11 -4
  5. package/dist/Assistant.js.map +1 -1
  6. package/dist/AssistantActivity.d.ts +10 -16
  7. package/dist/AssistantActivity.d.ts.map +1 -1
  8. package/dist/AssistantActivity.js +11 -4
  9. package/dist/AssistantActivity.js.map +1 -1
  10. package/dist/AssistantEngine.d.ts +39 -1
  11. package/dist/AssistantEngine.d.ts.map +1 -1
  12. package/dist/AssistantEngine.js +81 -28
  13. package/dist/AssistantEngine.js.map +1 -1
  14. package/dist/AssistantEvent.d.ts +52 -70
  15. package/dist/AssistantEvent.d.ts.map +1 -1
  16. package/dist/AssistantEvent.js +28 -57
  17. package/dist/AssistantEvent.js.map +1 -1
  18. package/dist/AssistantLoop.d.ts +7 -5
  19. package/dist/AssistantLoop.d.ts.map +1 -1
  20. package/dist/AssistantLoop.js +6 -3
  21. package/dist/AssistantLoop.js.map +1 -1
  22. package/dist/Block.js.map +1 -1
  23. package/dist/BlockKit.d.ts +14 -18
  24. package/dist/BlockKit.d.ts.map +1 -1
  25. package/dist/BlockKit.js +4 -7
  26. package/dist/BlockKit.js.map +1 -1
  27. package/dist/ChatLog.js +1 -1
  28. package/dist/Compaction.d.ts +8 -4
  29. package/dist/Compaction.d.ts.map +1 -1
  30. package/dist/Compaction.js +12 -15
  31. package/dist/Compaction.js.map +1 -1
  32. package/dist/CurrentChat.d.ts +2 -1
  33. package/dist/CurrentChat.d.ts.map +1 -1
  34. package/dist/CurrentChat.js.map +1 -1
  35. package/dist/Instructions.js +1 -1
  36. package/dist/Skill.d.ts +33 -0
  37. package/dist/Skill.d.ts.map +1 -1
  38. package/dist/Skill.js +27 -1
  39. package/dist/Skill.js.map +1 -1
  40. package/dist/Specialist.d.ts +4 -1
  41. package/dist/Specialist.d.ts.map +1 -1
  42. package/dist/Specialist.js.map +1 -1
  43. package/dist/Timeline.d.ts +482 -0
  44. package/dist/Timeline.d.ts.map +1 -0
  45. package/dist/Timeline.js +572 -0
  46. package/dist/Timeline.js.map +1 -0
  47. package/dist/ToolDiscovery.d.ts +4 -5
  48. package/dist/ToolDiscovery.d.ts.map +1 -1
  49. package/dist/ToolDiscovery.js +5 -4
  50. package/dist/ToolDiscovery.js.map +1 -1
  51. package/dist/ToolFailure.d.ts +1 -1
  52. package/dist/ToolFailure.js +1 -1
  53. package/dist/ToolFailure.js.map +1 -1
  54. package/dist/index.d.ts +2 -5
  55. package/dist/index.js +4 -7
  56. package/dist/internal/chat.js +11 -37
  57. package/dist/internal/chat.js.map +1 -1
  58. package/dist/internal/events.d.ts +15 -16
  59. package/dist/internal/events.d.ts.map +1 -1
  60. package/dist/internal/events.js +29 -28
  61. package/dist/internal/events.js.map +1 -1
  62. package/dist/internal/history.js +26 -58
  63. package/dist/internal/history.js.map +1 -1
  64. package/dist/internal/input.js +7 -0
  65. package/dist/internal/input.js.map +1 -0
  66. package/dist/internal/loop.d.ts +2 -2
  67. package/dist/internal/loop.d.ts.map +1 -1
  68. package/dist/internal/loop.js +21 -56
  69. package/dist/internal/loop.js.map +1 -1
  70. package/dist/internal/skills.js +6 -1
  71. package/dist/internal/skills.js.map +1 -1
  72. package/dist/internal/specialists.js +7 -7
  73. package/dist/internal/specialists.js.map +1 -1
  74. package/dist/internal/toolkit.d.ts +6 -20
  75. package/dist/internal/toolkit.d.ts.map +1 -1
  76. package/dist/internal/toolkit.js +10 -31
  77. package/dist/internal/toolkit.js.map +1 -1
  78. package/dist/internal/tools.js +2 -21
  79. package/dist/internal/tools.js.map +1 -1
  80. package/dist/internal/turns.d.ts.map +1 -1
  81. package/dist/internal/turns.js +24 -43
  82. package/dist/internal/turns.js.map +1 -1
  83. package/package.json +3 -2
  84. package/src/Assistant.ts +75 -29
  85. package/src/AssistantActivity.ts +11 -18
  86. package/src/AssistantEngine.ts +110 -43
  87. package/src/AssistantEvent.ts +46 -74
  88. package/src/AssistantLoop.ts +7 -5
  89. package/src/Block.ts +1 -1
  90. package/src/BlockKit.ts +18 -31
  91. package/src/Compaction.ts +8 -21
  92. package/src/CurrentChat.ts +2 -1
  93. package/src/Skill.ts +34 -1
  94. package/src/Specialist.ts +4 -1
  95. package/src/Timeline.ts +861 -0
  96. package/src/ToolDiscovery.ts +5 -13
  97. package/src/ToolFailure.ts +1 -1
  98. package/src/index.ts +5 -20
  99. package/src/internal/chat.ts +8 -57
  100. package/src/internal/events.ts +43 -43
  101. package/src/internal/history.ts +36 -101
  102. package/src/internal/input.ts +3 -0
  103. package/src/internal/loop.ts +31 -101
  104. package/src/internal/skills.ts +11 -0
  105. package/src/internal/specialists.ts +6 -9
  106. package/src/internal/toolkit.ts +17 -60
  107. package/src/internal/tools.ts +2 -53
  108. package/src/internal/turns.ts +37 -79
  109. package/dist/AppEvent.d.ts +0 -111
  110. package/dist/AppEvent.d.ts.map +0 -1
  111. package/dist/AppEvent.js +0 -71
  112. package/dist/AppEvent.js.map +0 -1
  113. package/dist/ChatView.d.ts +0 -459
  114. package/dist/ChatView.d.ts.map +0 -1
  115. package/dist/ChatView.js +0 -496
  116. package/dist/ChatView.js.map +0 -1
  117. package/dist/Decision.d.ts +0 -145
  118. package/dist/Decision.d.ts.map +0 -1
  119. package/dist/Decision.js +0 -178
  120. package/dist/Decision.js.map +0 -1
  121. package/dist/DecisionId.d.ts +0 -37
  122. package/dist/DecisionId.d.ts.map +0 -1
  123. package/dist/DecisionId.js +0 -44
  124. package/dist/DecisionId.js.map +0 -1
  125. package/dist/internal/decisions.js +0 -66
  126. package/dist/internal/decisions.js.map +0 -1
  127. package/src/AppEvent.ts +0 -134
  128. package/src/ChatView.ts +0 -658
  129. package/src/Decision.ts +0 -241
  130. package/src/DecisionId.ts +0 -46
  131. package/src/internal/decisions.ts +0 -131
package/src/ChatView.ts DELETED
@@ -1,658 +0,0 @@
1
- /**
2
- * This module provides the view of a chat as its user sees it. A chat client receives this view
3
- * and nothing else.
4
- *
5
- * A {@link View} holds the chat's messages with their blocks and decisions, the chat's
6
- * {@link Activity}, and whether the last turn failed or the chat was deleted. It never contains
7
- * the model's output, tool calls or results, what the model read, the app's own events, or why a
8
- * turn failed. The server streams {@link Update}s by piping the assistant's `subscribe` through
9
- * {@link updates}, and the client folds them with {@link apply}, starting from {@link empty}.
10
- * On the server, {@link fromEntries} builds a whole view from a chat's log.
11
- *
12
- * @since 0.1.0
13
- */
14
- import { dual } from "effect/Function"
15
- import * as Match from "effect/Match"
16
- import * as Option from "effect/Option"
17
- import * as Schema from "effect/Schema"
18
- import * as Stream from "effect/Stream"
19
- import * as AssistantActivity from "./AssistantActivity.ts"
20
- import * as AssistantEvent from "./AssistantEvent.ts"
21
- import * as BlockKit from "./BlockKit.ts"
22
- import * as Decisions from "./Decision.ts"
23
- import { DecisionId } from "./DecisionId.ts"
24
- import { InputId } from "./InputId.ts"
25
- import { toJson } from "./internal/schema.ts"
26
-
27
- /**
28
- * Schema for a decision as the user sees it on a decision block. It holds the call the decision is
29
- * for (its tool and params, which the app can display as it likes) and, once the user has
30
- * decided, the outcome.
31
- *
32
- * @category schemas
33
- * @since 0.1.0
34
- */
35
- export const PendingDecision = Schema.Struct({
36
- id: DecisionId,
37
- tool: Schema.String,
38
- params: Schema.Json,
39
- outcome: Schema.optional(AssistantEvent.DecisionOutcome)
40
- })
41
-
42
- /**
43
- * Represents a decision as the user sees it.
44
- *
45
- * @category models
46
- * @since 0.1.0
47
- */
48
- export type PendingDecision = typeof PendingDecision.Type
49
-
50
- /**
51
- * Schema for the status of a block. A block is `"open"` while it accepts an answer, `"answered"`
52
- * once the user has used it, and `"closed"` when it closed without an answer because the user
53
- * answered in words or moved on.
54
- *
55
- * @category schemas
56
- * @since 0.1.0
57
- */
58
- export const BlockStatus = Schema.Literals(["open", "answered", "closed"])
59
-
60
- /**
61
- * The status of a block.
62
- *
63
- * @category models
64
- * @since 0.1.0
65
- */
66
- export type BlockStatus = typeof BlockStatus.Type
67
-
68
- /**
69
- * Schema for a block on an assistant's message. It holds the block's kind, its props (JSON, as
70
- * the block's props schema encodes them), its status, the user's answer (JSON, as the block's
71
- * answer schema encodes it), and, for a decision block, its decisions.
72
- *
73
- * @category schemas
74
- * @since 0.1.0
75
- */
76
- export const Block = Schema.Struct({
77
- kind: Schema.String,
78
- props: Schema.Json,
79
- status: BlockStatus,
80
- answer: Schema.optional(Schema.Json),
81
- decisions: Schema.optional(Schema.Array(PendingDecision))
82
- })
83
-
84
- /**
85
- * Represents a block on an assistant's message.
86
- *
87
- * @category models
88
- * @since 0.1.0
89
- */
90
- export type Block = typeof Block.Type
91
-
92
- /**
93
- * Schema for a message the user sent. `at` is when it was stored, in epoch
94
- * milliseconds.
95
- *
96
- * @category schemas
97
- * @since 0.1.0
98
- */
99
- export const UserMessage = Schema.TaggedStruct("UserMessage", {
100
- id: InputId,
101
- text: Schema.String,
102
- attachments: Schema.optional(Schema.Array(AssistantEvent.Attachment)),
103
- at: Schema.Finite
104
- })
105
-
106
- /**
107
- * Represents a message the user sent.
108
- *
109
- * @category models
110
- * @since 0.1.0
111
- */
112
- export type UserMessage = typeof UserMessage.Type
113
-
114
- /**
115
- * Schema for a message the assistant sent the user, including its block if it has one.
116
- *
117
- * @category schemas
118
- * @since 0.1.0
119
- */
120
- export const AssistantMessage = Schema.TaggedStruct("AssistantMessage", {
121
- id: Schema.String,
122
- text: Schema.String,
123
- at: Schema.Finite,
124
- block: Schema.optional(Block)
125
- })
126
-
127
- /**
128
- * Represents a message the assistant sent the user.
129
- *
130
- * @category models
131
- * @since 0.1.0
132
- */
133
- export type AssistantMessage = typeof AssistantMessage.Type
134
-
135
- /**
136
- * Schema for a message in the chat, sent by either the user or the assistant.
137
- *
138
- * @category schemas
139
- * @since 0.1.0
140
- */
141
- export const Message = Schema.Union([UserMessage, AssistantMessage])
142
-
143
- /**
144
- * Represents a message in the chat.
145
- *
146
- * @category models
147
- * @since 0.1.0
148
- */
149
- export type Message = typeof Message.Type
150
-
151
- /**
152
- * Schema for a specialist that is working for the assistant. It holds the specialist's name and
153
- * when it started, in epoch milliseconds.
154
- *
155
- * @category schemas
156
- * @since 0.1.0
157
- */
158
- export const SpecialistWorking = Schema.Struct({
159
- specialist: Schema.String,
160
- since: Schema.Finite
161
- })
162
-
163
- /**
164
- * Represents a specialist that is working for the assistant.
165
- *
166
- * @category models
167
- * @since 0.1.0
168
- */
169
- export type SpecialistWorking = typeof SpecialistWorking.Type
170
-
171
- /**
172
- * Schema for what is working in the chat.
173
- *
174
- * `working` is true while the assistant runs a turn, or when a turn is about to start for
175
- * something that arrived. `specialists` lists the specialists that are working. When the chat is
176
- * idle, nothing is working, even if a workflow is waiting on a timer or a decision.
177
- *
178
- * @category schemas
179
- * @since 0.1.0
180
- */
181
- export const Activity = Schema.Struct({
182
- working: Schema.Boolean,
183
- specialists: Schema.Array(SpecialistWorking)
184
- })
185
-
186
- /**
187
- * Represents what is working in the chat.
188
- *
189
- * @category models
190
- * @since 0.1.0
191
- */
192
- export type Activity = typeof Activity.Type
193
-
194
- /**
195
- * Schema for a chat as its user sees it. It holds the chat's messages, what is working in it,
196
- * whether the last turn failed, and whether the chat was deleted. `seq` is the position in the
197
- * chat's log that the view is up to.
198
- *
199
- * @category schemas
200
- * @since 0.1.0
201
- */
202
- export const View = Schema.Struct({
203
- seq: Schema.Int,
204
- messages: Schema.Array(Message),
205
- activity: Activity,
206
- /** Whether the last turn failed with no message after it. */
207
- failed: Schema.Boolean,
208
- /** Whether the chat was deleted. A deleted chat accepts nothing more, and its stream ends. */
209
- deleted: Schema.Boolean
210
- })
211
-
212
- /**
213
- * Represents a chat as its user sees it.
214
- *
215
- * @category models
216
- * @since 0.1.0
217
- */
218
- export type View = typeof View.Type
219
-
220
- /**
221
- * Schema for a change to a chat's view.
222
- *
223
- * An update holds each message that is new or changed (for example, a block answered or closed,
224
- * or a decision made), in full. It also holds the current activity, whether the last turn failed,
225
- * and whether the chat was deleted. `seq` is the position in the chat's log that the update is up
226
- * to.
227
- *
228
- * @category schemas
229
- * @since 0.1.0
230
- */
231
- export const Update = Schema.Struct({
232
- seq: Schema.Int,
233
- messages: Schema.Array(Message),
234
- activity: Activity,
235
- failed: Schema.Boolean,
236
- deleted: Schema.Boolean
237
- })
238
-
239
- /**
240
- * Represents a change to a chat's view.
241
- *
242
- * @category models
243
- * @since 0.1.0
244
- */
245
- export type Update = typeof Update.Type
246
-
247
- /**
248
- * The view of a chat before anything has happened in it, with no messages and nothing working.
249
- * Use it as the starting point when folding updates with {@link apply}.
250
- *
251
- * @category constructors
252
- * @since 0.1.0
253
- */
254
- export const empty: View = {
255
- seq: 0,
256
- messages: [],
257
- activity: { working: false, specialists: [] },
258
- failed: false,
259
- deleted: false
260
- }
261
-
262
- /**
263
- * Returns the view after applying an update.
264
- *
265
- * **Details**
266
- *
267
- * Each message in the update replaces the message with the same id, in place, and new messages
268
- * are added after the rest. The update's activity, `failed`, and `deleted` replace the view's.
269
- * Applying an update twice changes nothing, so a client that resubscribes from the start can fold
270
- * again and get the same view.
271
- *
272
- * @category combinators
273
- * @since 0.1.0
274
- */
275
- export const apply = (view: View, update: Update): View => {
276
- const changed = new Map(update.messages.map((message) => [message.id, message]))
277
- const known = new Set(view.messages.map((message) => message.id))
278
-
279
- return {
280
- seq: Math.max(view.seq, update.seq),
281
- messages: [
282
- ...view.messages.map((message) => changed.get(message.id) ?? message),
283
- ...update.messages.filter((message) => !known.has(message.id))
284
- ],
285
- activity: update.activity,
286
- failed: update.failed,
287
- deleted: update.deleted
288
- }
289
- }
290
-
291
- /**
292
- * Returns whether nothing is working in the chat: no turn is running or about to start, and no
293
- * specialist is working.
294
- *
295
- * @category guards
296
- * @since 0.1.0
297
- */
298
- export const isIdle = (view: View): boolean => !view.activity.working && view.activity.specialists.length === 0
299
-
300
- /**
301
- * Represents the options for building a view from a chat's log.
302
- *
303
- * @category models
304
- * @since 0.1.0
305
- */
306
- export interface Options {
307
- /**
308
- * The tags of the app's events that wake a turn. After such an event, the view shows a turn as
309
- * about to start. Defaults to none.
310
- */
311
- readonly wakes?: ReadonlySet<string> | undefined
312
- }
313
-
314
- /**
315
- * Returns a chat's view, built from the entries of its log.
316
- *
317
- * **When to use**
318
- *
319
- * Use it in server-side code, for example to return a whole chat at once. A client gets the view
320
- * as a stream from {@link updates} instead.
321
- *
322
- * **Example** (Building a chat's view from its log)
323
- *
324
- * ```ts
325
- * import { type ChatId, ChatLog, ChatView } from "effect-assistant"
326
- * import * as Effect from "effect/Effect"
327
- *
328
- * const viewOf = (chatId: ChatId.ChatId) =>
329
- * ChatLog.ChatLog.use((log) => log.read(chatId)).pipe(
330
- * Effect.map((entries) => ChatView.fromEntries(entries))
331
- * )
332
- * ```
333
- *
334
- * @see {@link updates} for a stream of changes instead of a whole view
335
- *
336
- * @category constructors
337
- * @since 0.1.0
338
- */
339
- export const fromEntries = (entries: ReadonlyArray<AssistantEvent.AnyEntry>, options?: Options): View => {
340
- const fold = folding(options?.wakes ?? new Set())
341
-
342
- for (const entry of entries) fold.step(entry)
343
-
344
- return fold.view
345
- }
346
-
347
- interface Folding {
348
- readonly step: (entry: AssistantEvent.AnyEntry) => Update | undefined
349
- readonly view: View
350
- }
351
-
352
- const decodeProps = Schema.decodeUnknownOption(Decisions.Props)
353
-
354
- const decodeAnswer = Schema.decodeUnknownOption(Decisions.Answer)
355
-
356
- const activityOf = (activity: AssistantActivity.State): Activity => ({
357
- working: activity.turnSince !== undefined || activity.queued,
358
- specialists: [...activity.specialists.values()].map((run) => ({
359
- specialist: run.specialist,
360
- since: run.since
361
- }))
362
- })
363
-
364
- const sameActivity = (a: Activity, b: Activity) =>
365
- a.working === b.working &&
366
- a.specialists.length === b.specialists.length &&
367
- a.specialists.every((run, i) => {
368
- const other = b.specialists[i]
369
-
370
- return other !== undefined && run.specialist === other.specialist && run.since === other.since
371
- })
372
-
373
- const decisionIds = (block: AssistantEvent.MessageBlock): ReadonlyArray<string> =>
374
- block.kind === Decisions.kind
375
- ? Option.match(decodeProps(block.props), {
376
- onNone: () => [],
377
- onSome: (props) => props.decisionIds
378
- })
379
- : []
380
-
381
- const folding = (wakes: ReadonlySet<string>): Folding => {
382
- let seq = 0
383
- let failed = false
384
- let deleted = false
385
- let activity = AssistantActivity.empty
386
- let blocks: ReadonlyMap<string, BlockKit.State> = new Map()
387
- let decisions: ReadonlyMap<string, Decisions.State> = new Map()
388
- // The messages in order, and the assistant's as stored: their blocks change as the chat goes on.
389
- const messages = new Map<string, Message>()
390
- const sent = new Map<string, AssistantEvent.AssistantMessage>()
391
- // Which messages ask for a decision, so a decision's outcome updates the block that asked for it.
392
- const asking = new Map<string, Array<string>>()
393
-
394
- const blockOf = (message: AssistantEvent.AssistantMessage): Block | undefined => {
395
- if (message.block === undefined) return undefined
396
-
397
- const state = blocks.get(message.id)
398
- const status = state === undefined || state.open ? "open" : state.answer === undefined ? "closed" : "answered"
399
- const ids = decisionIds(message.block)
400
-
401
- const shown = ids.flatMap((id) => {
402
- const decision = decisions.get(id)
403
-
404
- if (decision === undefined) return []
405
-
406
- const { tool, params } = decision.request.origin
407
- const shown = { id: decision.request.id, tool, params }
408
-
409
- return [decision.outcome === undefined ? shown : { ...shown, outcome: decision.outcome }]
410
- })
411
-
412
- const block: Block = { kind: message.block.kind, props: toJson(message.block.props), status }
413
- const answered = state?.answer === undefined ? block : { ...block, answer: toJson(state.answer.value) }
414
-
415
- return message.block.kind === Decisions.kind ? { ...answered, decisions: shown } : answered
416
- }
417
-
418
- const viewOf = (message: AssistantEvent.AssistantMessage, at: number): AssistantMessage => {
419
- const block = blockOf(message)
420
-
421
- return block === undefined
422
- ? AssistantMessage.make({ id: message.id, text: message.text, at })
423
- : AssistantMessage.make({ id: message.id, text: message.text, at, block })
424
- }
425
-
426
- const touched = (event: AssistantEvent.AssistantEvent, at: number): ReadonlyArray<string> =>
427
- Match.value(event).pipe(
428
- Match.tagsExhaustive({
429
- UserMessage: (message) => {
430
- messages.set(
431
- message.id,
432
- message.attachments === undefined
433
- ? UserMessage.make({ id: message.id, text: message.text, at })
434
- : UserMessage.make({
435
- id: message.id,
436
- text: message.text,
437
- attachments: message.attachments,
438
- at
439
- })
440
- )
441
- failed = false
442
-
443
- return [message.id]
444
- },
445
- AssistantMessage: (message) => {
446
- sent.set(message.id, message)
447
- messages.set(message.id, viewOf(message, at))
448
- failed = false
449
-
450
- if (message.block !== undefined) {
451
- for (const id of decisionIds(message.block)) asking.set(id, [...(asking.get(id) ?? []), message.id])
452
- }
453
-
454
- return [message.id, ...(message.closes ?? [])]
455
- },
456
- BlockAnswer: (answer) => {
457
- failed = false
458
-
459
- const decided =
460
- answer.kind === Decisions.kind
461
- ? Option.match(decodeAnswer(answer.value), {
462
- onNone: () => [],
463
- onSome: ({ decisions }) => Object.keys(decisions).flatMap((id) => asking.get(id) ?? [])
464
- })
465
- : []
466
-
467
- return [answer.messageId, ...decided]
468
- },
469
- BlocksClosed: (closed) => closed.messageIds,
470
- DecisionResolved: (resolved) => asking.get(resolved.id) ?? [],
471
- TurnFailed: () => {
472
- failed = true
473
-
474
- return []
475
- },
476
- ModelOutput: () => [],
477
- ToolStarted: () => [],
478
- ToolResult: () => [],
479
- TurnStarted: () => [],
480
- TurnCompleted: () => [],
481
- SpecialistStarted: () => [],
482
- SpecialistFinished: () => [],
483
- DecisionRequested: () => [],
484
- ChatDeleted: () => {
485
- deleted = true
486
-
487
- return []
488
- }
489
- })
490
- )
491
-
492
- const step = (entry: AssistantEvent.AnyEntry): Update | undefined => {
493
- const before = { activity: activityOf(activity), failed, deleted }
494
- const { event } = entry
495
-
496
- seq = entry.seq
497
- activity = AssistantActivity.apply(activity, entry, { wakes })
498
-
499
- if (!AssistantEvent.isLibraryEvent(event)) {
500
- const now = activityOf(activity)
501
-
502
- return sameActivity(before.activity, now) ? undefined : { seq, messages: [], activity: now, failed, deleted }
503
- }
504
-
505
- blocks = BlockKit.apply(blocks, event)
506
- decisions = Decisions.apply(decisions, event)
507
-
508
- const changed = [...new Set(touched(event, entry.at))].flatMap((id) => {
509
- const message = sent.get(id)
510
- const current = messages.get(id)
511
-
512
- if (message === undefined || current === undefined) return current === undefined ? [] : [current]
513
-
514
- const next = viewOf(message, current.at)
515
- messages.set(id, next)
516
-
517
- return [next]
518
- })
519
-
520
- const now = activityOf(activity)
521
-
522
- return changed.length === 0 &&
523
- failed === before.failed &&
524
- deleted === before.deleted &&
525
- sameActivity(before.activity, now)
526
- ? undefined
527
- : { seq, messages: changed, activity: now, failed, deleted }
528
- }
529
-
530
- return {
531
- step,
532
- get view() {
533
- return { seq, messages: [...messages.values()], activity: activityOf(activity), failed, deleted }
534
- }
535
- }
536
- }
537
-
538
- const merge = (updates: ReadonlyArray<Update>): Update | undefined => {
539
- const last = updates.at(-1)
540
-
541
- if (last === undefined) return undefined
542
-
543
- const messages = new Map<string, Message>()
544
-
545
- for (const update of updates) for (const message of update.messages) messages.set(message.id, message)
546
-
547
- return {
548
- seq: last.seq,
549
- messages: [...messages.values()],
550
- activity: last.activity,
551
- failed: last.failed,
552
- deleted: last.deleted
553
- }
554
- }
555
-
556
- /**
557
- * Represents an assistant's definition as {@link updates} reads it: the app's events it
558
- * registered, and whether each one wakes a turn. An assistant made with `Assistant.make` fits it.
559
- *
560
- * @category models
561
- * @since 0.1.0
562
- */
563
- export interface Definition {
564
- readonly events: ReadonlyArray<{ readonly tag: string; readonly wakes: boolean }>
565
- }
566
-
567
- /**
568
- * Represents the options accepted by {@link updates}.
569
- *
570
- * @category models
571
- * @since 0.1.0
572
- */
573
- export interface UpdatesOptions {
574
- /**
575
- * The `seq` up to which the client already has the view. The result contains only the updates
576
- * after it. Defaults to `0`, every update.
577
- */
578
- readonly after?: number | undefined
579
- }
580
-
581
- const wakesOf = (definition: Definition): ReadonlySet<string> =>
582
- new Set(definition.events.flatMap((event) => (event.wakes ? [event.tag] : [])))
583
-
584
- /**
585
- * Returns the stream of view updates that a chat's entries produce, for a chat client.
586
- *
587
- * **Details**
588
- *
589
- * The entries must stream from the start of the chat's log, as `assistant.subscribe(chatId)` does
590
- * without `after`. The fold reads every entry, because a message's view depends on earlier
591
- * entries: a block is answered or closed later, and a decision is made later. `after` only skips
592
- * emitting the updates up to that `seq`, so a client that has the view up to a `seq` receives only
593
- * what changed since. Pass `after` here, not to `subscribe`: a subscription that starts after a
594
- * `seq` gives the fold an incomplete log, and the updates it produces are wrong.
595
- *
596
- * The definition tells the fold which of the app's events wake a turn, so the view shows a turn as
597
- * about to start after one. Each chunk of entries becomes one update; the stored entries, for
598
- * example, arrive as one chunk.
599
- *
600
- * **Example** (Streaming a chat's view to a client)
601
- *
602
- * ```ts
603
- * import { Assistant, type ChatId, ChatView } from "effect-assistant"
604
- * import * as Stream from "effect/Stream"
605
- *
606
- * const MyAssistant = Assistant.make("my-assistant")
607
- *
608
- * const watch = (chatId: ChatId.ChatId, after: number) =>
609
- * Stream.unwrap(
610
- * MyAssistant.useSync((assistant) =>
611
- * assistant.subscribe(chatId).pipe(ChatView.updates(MyAssistant, { after }))
612
- * )
613
- * )
614
- * ```
615
- *
616
- * @see {@link apply} for folding the updates into a view on the client
617
- *
618
- * @category utils
619
- * @since 0.1.0
620
- */
621
- export const updates: {
622
- (
623
- definition: Definition,
624
- options?: UpdatesOptions
625
- ): <E, R>(entries: Stream.Stream<AssistantEvent.AnyEntry, E, R>) => Stream.Stream<Update, E, R>
626
- <E, R>(
627
- entries: Stream.Stream<AssistantEvent.AnyEntry, E, R>,
628
- definition: Definition,
629
- options?: UpdatesOptions
630
- ): Stream.Stream<Update, E, R>
631
- } = dual(
632
- (args) => Stream.isStream(args[0]),
633
- <E, R>(
634
- entries: Stream.Stream<AssistantEvent.AnyEntry, E, R>,
635
- definition: Definition,
636
- options?: UpdatesOptions
637
- ): Stream.Stream<Update, E, R> => {
638
- const after = options?.after ?? 0
639
- const wakes = wakesOf(definition)
640
-
641
- return entries.pipe(
642
- Stream.mapAccumArray(
643
- () => folding(wakes),
644
- (fold, chunk) => {
645
- const changed = chunk.flatMap((entry) => {
646
- const update = fold.step(entry)
647
-
648
- return update === undefined || entry.seq <= after ? [] : [update]
649
- })
650
-
651
- const merged = merge(changed)
652
-
653
- return [fold, merged === undefined ? [] : [merged]]
654
- }
655
- )
656
- )
657
- }
658
- )