effect-assistant 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/dist/Assistant.d.ts +77 -37
  2. package/dist/Assistant.d.ts.map +1 -1
  3. package/dist/Assistant.js +11 -4
  4. package/dist/Assistant.js.map +1 -1
  5. package/dist/AssistantActivity.d.ts +10 -16
  6. package/dist/AssistantActivity.d.ts.map +1 -1
  7. package/dist/AssistantActivity.js +11 -4
  8. package/dist/AssistantActivity.js.map +1 -1
  9. package/dist/AssistantEngine.d.ts +39 -1
  10. package/dist/AssistantEngine.d.ts.map +1 -1
  11. package/dist/AssistantEngine.js +81 -28
  12. package/dist/AssistantEngine.js.map +1 -1
  13. package/dist/AssistantEvent.d.ts +104 -11
  14. package/dist/AssistantEvent.d.ts.map +1 -1
  15. package/dist/AssistantEvent.js +88 -13
  16. package/dist/AssistantEvent.js.map +1 -1
  17. package/dist/AssistantLoop.d.ts +4 -2
  18. package/dist/AssistantLoop.d.ts.map +1 -1
  19. package/dist/AssistantLoop.js +4 -2
  20. package/dist/AssistantLoop.js.map +1 -1
  21. package/dist/BlockKit.d.ts +4 -4
  22. package/dist/BlockKit.js +1 -1
  23. package/dist/BlockKit.js.map +1 -1
  24. package/dist/ChatLog.js +1 -1
  25. package/dist/Compaction.d.ts +7 -3
  26. package/dist/Compaction.d.ts.map +1 -1
  27. package/dist/Compaction.js +19 -8
  28. package/dist/Compaction.js.map +1 -1
  29. package/dist/CurrentChat.d.ts +2 -1
  30. package/dist/CurrentChat.d.ts.map +1 -1
  31. package/dist/CurrentChat.js.map +1 -1
  32. package/dist/Instructions.js +1 -1
  33. package/dist/Skill.d.ts +86 -11
  34. package/dist/Skill.d.ts.map +1 -1
  35. package/dist/Skill.js +93 -18
  36. package/dist/Skill.js.map +1 -1
  37. package/dist/Specialist.d.ts +7 -7
  38. package/dist/Specialist.d.ts.map +1 -1
  39. package/dist/Specialist.js.map +1 -1
  40. package/dist/Timeline.d.ts +516 -0
  41. package/dist/Timeline.d.ts.map +1 -0
  42. package/dist/Timeline.js +631 -0
  43. package/dist/Timeline.js.map +1 -0
  44. package/dist/ToolApproval.d.ts +146 -0
  45. package/dist/ToolApproval.d.ts.map +1 -0
  46. package/dist/ToolApproval.js +201 -0
  47. package/dist/ToolApproval.js.map +1 -0
  48. package/dist/ToolDiscovery.d.ts +6 -4
  49. package/dist/ToolDiscovery.d.ts.map +1 -1
  50. package/dist/ToolDiscovery.js +5 -4
  51. package/dist/ToolDiscovery.js.map +1 -1
  52. package/dist/index.d.ts +4 -4
  53. package/dist/index.js +4 -4
  54. package/dist/internal/chat.js +9 -7
  55. package/dist/internal/chat.js.map +1 -1
  56. package/dist/internal/events.d.ts +15 -16
  57. package/dist/internal/events.d.ts.map +1 -1
  58. package/dist/internal/events.js +29 -28
  59. package/dist/internal/events.js.map +1 -1
  60. package/dist/internal/history.js +53 -22
  61. package/dist/internal/history.js.map +1 -1
  62. package/dist/internal/input.js +7 -0
  63. package/dist/internal/input.js.map +1 -0
  64. package/dist/internal/loop.d.ts +10 -5
  65. package/dist/internal/loop.d.ts.map +1 -1
  66. package/dist/internal/loop.js +55 -22
  67. package/dist/internal/loop.js.map +1 -1
  68. package/dist/internal/specialists.js +10 -16
  69. package/dist/internal/specialists.js.map +1 -1
  70. package/dist/internal/toolkit.d.ts +5 -7
  71. package/dist/internal/toolkit.d.ts.map +1 -1
  72. package/dist/internal/toolkit.js +28 -18
  73. package/dist/internal/toolkit.js.map +1 -1
  74. package/dist/internal/tools.js +1 -1
  75. package/dist/internal/turns.d.ts.map +1 -1
  76. package/dist/internal/turns.js +35 -42
  77. package/dist/internal/turns.js.map +1 -1
  78. package/package.json +1 -1
  79. package/src/Assistant.ts +80 -40
  80. package/src/AssistantActivity.ts +11 -18
  81. package/src/AssistantEngine.ts +110 -43
  82. package/src/AssistantEvent.ts +106 -14
  83. package/src/AssistantLoop.ts +4 -2
  84. package/src/BlockKit.ts +6 -6
  85. package/src/Compaction.ts +25 -4
  86. package/src/CurrentChat.ts +2 -1
  87. package/src/Skill.ts +133 -23
  88. package/src/Specialist.ts +9 -9
  89. package/src/Timeline.ts +947 -0
  90. package/src/ToolApproval.ts +249 -0
  91. package/src/ToolDiscovery.ts +8 -3
  92. package/src/index.ts +10 -10
  93. package/src/internal/chat.ts +15 -5
  94. package/src/internal/events.ts +43 -43
  95. package/src/internal/history.ts +94 -32
  96. package/src/internal/input.ts +3 -0
  97. package/src/internal/loop.ts +120 -34
  98. package/src/internal/specialists.ts +7 -12
  99. package/src/internal/toolkit.ts +47 -30
  100. package/src/internal/turns.ts +52 -58
  101. package/dist/AppEvent.d.ts +0 -111
  102. package/dist/AppEvent.d.ts.map +0 -1
  103. package/dist/AppEvent.js +0 -71
  104. package/dist/AppEvent.js.map +0 -1
  105. package/dist/ChatView.d.ts +0 -408
  106. package/dist/ChatView.d.ts.map +0 -1
  107. package/dist/ChatView.js +0 -443
  108. package/dist/ChatView.js.map +0 -1
  109. package/dist/internal/skills.js +0 -28
  110. package/dist/internal/skills.js.map +0 -1
  111. package/src/AppEvent.ts +0 -134
  112. package/src/ChatView.ts +0 -587
  113. package/src/internal/skills.ts +0 -37
@@ -0,0 +1,631 @@
1
+ import { __exportAll } from "./_virtual/_rolldown/runtime.js";
2
+ import { InputId } from "./InputId.js";
3
+ import { Attachment, ToolOutcome, isLibraryEvent } from "./AssistantEvent.js";
4
+ import { apply as apply$1 } from "./ToolApproval.js";
5
+ import { decodeWith, tagOf } from "./internal/events.js";
6
+ import { toJson } from "./internal/schema.js";
7
+ import { apply as apply$2, empty as empty$1 } from "./AssistantActivity.js";
8
+ import { apply as apply$3 } from "./BlockKit.js";
9
+ import * as Predicate from "effect/Predicate";
10
+ import * as Schema from "effect/Schema";
11
+ import * as Stream from "effect/Stream";
12
+ import * as Option from "effect/Option";
13
+ import * as Match from "effect/Match";
14
+ import { dual } from "effect/Function";
15
+ import * as SchemaTransformation from "effect/SchemaTransformation";
16
+ //#region src/Timeline.ts
17
+ /**
18
+ * This module provides a chat's timeline: the chat as its user sees it. A chat client receives the
19
+ * timeline and nothing else.
20
+ *
21
+ * A {@link Timeline} holds the chat's items: the messages with their blocks, the app's own events
22
+ * it shows (each typed with its event, see {@link EventItem}), and the tool calls waiting for the
23
+ * user's approval ({@link ApprovalItem}). It also holds the chat's
24
+ * {@link Activity}, and whether the last turn failed or the chat was deleted. It never contains the
25
+ * model's output, tool calls or results, what the model read, or why a turn failed. The server
26
+ * streams {@link Change}s by piping the assistant's `subscribe` through {@link changes}, and the
27
+ * client folds them with {@link fold} (or {@link apply}, from {@link empty}). On the server,
28
+ * {@link fromEntries} builds a whole timeline from a chat's log, and {@link snapshot} the whole
29
+ * timeline as one change.
30
+ *
31
+ * **Only the app events in `show` reach the timeline**, and none by default: an event that is only
32
+ * for the assistant, like a summary of the chat, stays out. The RPC's schema, `Change([...])`,
33
+ * names the same events and types the client's timeline with them.
34
+ *
35
+ * @since 0.3.0
36
+ */
37
+ var Timeline_exports = /* @__PURE__ */ __exportAll({
38
+ Activity: () => Activity,
39
+ ApprovalItem: () => ApprovalItem,
40
+ AssistantMessage: () => AssistantMessage,
41
+ Block: () => Block,
42
+ BlockStatus: () => BlockStatus,
43
+ Change: () => Change,
44
+ SpecialistWorking: () => SpecialistWorking,
45
+ UserMessage: () => UserMessage,
46
+ apply: () => apply,
47
+ changes: () => changes,
48
+ empty: () => empty,
49
+ fold: () => fold,
50
+ fromEntries: () => fromEntries,
51
+ isIdle: () => isIdle,
52
+ snapshot: () => snapshot
53
+ });
54
+ /**
55
+ * Schema for the status of a block. A block is `"open"` while it accepts an answer, `"answered"`
56
+ * once the user has used it, and `"closed"` when it closed without an answer because the user
57
+ * answered in words or moved on.
58
+ *
59
+ * @category schemas
60
+ * @since 0.1.0
61
+ */
62
+ const BlockStatus = Schema.Literals([
63
+ "open",
64
+ "answered",
65
+ "closed"
66
+ ]);
67
+ /**
68
+ * Schema for a block on an assistant's message. It holds the block's kind, its props (JSON, as
69
+ * the block's props schema encodes them), its status, and the user's answer (JSON, as the block's
70
+ * answer schema encodes it).
71
+ *
72
+ * @category schemas
73
+ * @since 0.1.0
74
+ */
75
+ const Block = Schema.Struct({
76
+ kind: Schema.String,
77
+ props: Schema.Json,
78
+ status: BlockStatus,
79
+ answer: Schema.optional(Schema.Json)
80
+ });
81
+ /**
82
+ * Schema for a message the user sent. `at` is when it was stored, in epoch
83
+ * milliseconds.
84
+ *
85
+ * @category schemas
86
+ * @since 0.1.0
87
+ */
88
+ const UserMessage = Schema.TaggedStruct("UserMessage", {
89
+ id: InputId,
90
+ text: Schema.String,
91
+ attachments: Schema.optional(Schema.Array(Attachment)),
92
+ at: Schema.Finite
93
+ });
94
+ /**
95
+ * Schema for a message the assistant sent the user, including its block if it has one.
96
+ *
97
+ * @category schemas
98
+ * @since 0.1.0
99
+ */
100
+ const AssistantMessage = Schema.TaggedStruct("AssistantMessage", {
101
+ id: Schema.String,
102
+ text: Schema.String,
103
+ at: Schema.Finite,
104
+ block: Schema.optional(Block)
105
+ });
106
+ /**
107
+ * Schema for a tool call that waits, or waited, for the user's approval, as an item on the
108
+ * timeline.
109
+ *
110
+ * **Details**
111
+ *
112
+ * It is only the yes or no: the assistant's own message tells the user what it asks them to
113
+ * approve, so the item carries no params. `id` is the call's id, which the decision is for (see
114
+ * `ToolApproval.approve`), and `tool` the tool's name, for a label. `status` is where the call stands
115
+ * (`ToolApproval.Status`); `reason` is the user's, when they rejected it with one; `outcome` is how
116
+ * the approved call went, once it ran. `at` is when the call started waiting, in epoch
117
+ * milliseconds. A change updates the item in place as the call is decided and run.
118
+ *
119
+ * @category schemas
120
+ * @since 0.4.0
121
+ */
122
+ const ApprovalItem = Schema.TaggedStruct("ToolApproval", {
123
+ id: Schema.String,
124
+ tool: Schema.String,
125
+ status: Schema.Literals([
126
+ "pending",
127
+ "approved",
128
+ "rejected",
129
+ "expired"
130
+ ]),
131
+ reason: Schema.optional(Schema.String),
132
+ outcome: Schema.optional(ToolOutcome),
133
+ at: Schema.Finite
134
+ });
135
+ /**
136
+ * Schema for a specialist that is working for the assistant. It holds the specialist's name and
137
+ * when it started, in epoch milliseconds.
138
+ *
139
+ * @category schemas
140
+ * @since 0.1.0
141
+ */
142
+ const SpecialistWorking = Schema.Struct({
143
+ specialist: Schema.String,
144
+ since: Schema.Finite
145
+ });
146
+ /**
147
+ * Schema for what is working in the chat.
148
+ *
149
+ * `working` is true while the assistant runs a turn, or when a turn is about to start for
150
+ * something that arrived. `specialists` lists the specialists that are working. When the chat is
151
+ * idle, nothing is working, even if a workflow is waiting on a timer or on the user.
152
+ *
153
+ * @category schemas
154
+ * @since 0.1.0
155
+ */
156
+ const Activity = Schema.Struct({
157
+ working: Schema.Boolean,
158
+ specialists: Schema.Array(SpecialistWorking)
159
+ });
160
+ const eventItem = (schema) => Schema.Struct({
161
+ _tag: Schema.Literal(tagOf(schema)),
162
+ id: Schema.String,
163
+ at: Schema.Finite,
164
+ event: schema
165
+ });
166
+ function makeChange(events = []) {
167
+ const AnyItem = Schema.toCodecJson(Schema.Union([
168
+ UserMessage,
169
+ AssistantMessage,
170
+ ApprovalItem,
171
+ ...events.map(eventItem)
172
+ ]));
173
+ const decodeItem = Schema.decodeUnknownOption(AnyItem);
174
+ const encodeItem = Schema.encodeUnknownSync(AnyItem);
175
+ return Schema.Struct({
176
+ seq: Schema.Int,
177
+ items: Schema.Array(Schema.Json).pipe(Schema.decodeTo(Schema.Array(Schema.toType(AnyItem)), SchemaTransformation.transform({
178
+ decode: (json) => json.flatMap((item) => Option.toArray(decodeItem(item))),
179
+ encode: (items) => items.map((item) => encodeItem(item))
180
+ }))),
181
+ activity: Activity,
182
+ failed: Schema.Boolean,
183
+ deleted: Schema.Boolean
184
+ });
185
+ }
186
+ /**
187
+ * Creates the schema for the changes to a chat's timeline, with items for the given app events,
188
+ * in JSON.
189
+ *
190
+ * **Details**
191
+ *
192
+ * It is the success schema of the RPC that streams a chat's timeline: name the events the chat's
193
+ * users see, the same ones the handler passes to {@link changes} as `show`. Tool calls waiting for
194
+ * approval are always in it, as {@link ApprovalItem}s. Each item decodes its
195
+ * event with that event's schema, so the client gets `Change<Events>` typed with them and folds it
196
+ * with {@link fold}, no definition needed. `Change()` carries no app events.
197
+ *
198
+ * A handler whose changes show an event the schema doesn't name doesn't compile.
199
+ *
200
+ * **Example** (An RPC that streams a chat's timeline, with the budget notice)
201
+ *
202
+ * ```ts
203
+ * import { ChatId, Timeline } from "effect-assistant"
204
+ * import * as Rpc from "effect/rpc/Rpc"
205
+ * import * as Schema from "effect/Schema"
206
+ *
207
+ * class DailyBudgetReached extends Schema.TaggedClass<DailyBudgetReached>()("DailyBudgetReached", {}) {}
208
+ *
209
+ * const Subscribe = Rpc.make("Subscribe", {
210
+ * payload: { chatId: ChatId.ChatId },
211
+ * success: Timeline.Change([DailyBudgetReached]),
212
+ * stream: true
213
+ * })
214
+ * ```
215
+ *
216
+ * @category schemas
217
+ * @since 0.3.0
218
+ */
219
+ const Change = makeChange;
220
+ /**
221
+ * The timeline of a chat before anything has happened in it, with no items and nothing working.
222
+ * Use it as the starting point when folding changes with {@link apply}.
223
+ *
224
+ * @category constructors
225
+ * @since 0.1.0
226
+ */
227
+ const empty = {
228
+ seq: 0,
229
+ items: [],
230
+ activity: {
231
+ working: false,
232
+ specialists: []
233
+ },
234
+ failed: false,
235
+ deleted: false
236
+ };
237
+ /**
238
+ * Returns the timeline after a change.
239
+ *
240
+ * **Details**
241
+ *
242
+ * Each item in the change replaces the item with the same id, in place, and new items are added
243
+ * after the rest. The change's activity, `failed`, and `deleted` replace the timeline's. Applying a
244
+ * change twice changes nothing, so a client that resubscribes from the start can fold again and get
245
+ * the same timeline.
246
+ *
247
+ * The timeline is typed with the change's events, as its RPC's schema decoded them (see
248
+ * {@link Change}). To fold a stream of changes, use {@link fold}: in
249
+ * `Stream.scan(() => Timeline.empty, Timeline.apply)`, TypeScript fixes the state's type from
250
+ * `empty` before it sees the changes' events, so that form needs the timeline's type written out.
251
+ *
252
+ * **Example** (Folding changes into your own state)
253
+ *
254
+ * ```ts
255
+ * import { Timeline } from "effect-assistant"
256
+ *
257
+ * interface State<Events> {
258
+ * readonly timeline: Timeline.Timeline<Events>
259
+ * readonly unread: number
260
+ * }
261
+ *
262
+ * const next = <Events>(state: State<Events>, change: Timeline.Change<Events>): State<Events> => ({
263
+ * timeline: Timeline.apply(state.timeline, change),
264
+ * unread: state.unread + change.items.length
265
+ * })
266
+ * ```
267
+ *
268
+ * @category combinators
269
+ * @since 0.1.0
270
+ */
271
+ const apply = (timeline, change) => {
272
+ const changed = new Map(change.items.map((item) => [item.id, item]));
273
+ const known = new Set(timeline.items.map((item) => item.id));
274
+ return {
275
+ seq: Math.max(timeline.seq, change.seq),
276
+ items: [...timeline.items.map((item) => changed.get(item.id) ?? item), ...change.items.filter((item) => !known.has(item.id))],
277
+ activity: change.activity,
278
+ failed: change.failed,
279
+ deleted: change.deleted
280
+ };
281
+ };
282
+ /**
283
+ * Returns the timelines a stream of changes folds to, from {@link empty}, typed with the changes'
284
+ * events.
285
+ *
286
+ * **Example** (A chat's timeline on the client)
287
+ *
288
+ * ```ts
289
+ * import { Timeline } from "effect-assistant"
290
+ * import * as Schema from "effect/Schema"
291
+ * import * as Stream from "effect/Stream"
292
+ *
293
+ * class DailyBudgetReached extends Schema.TaggedClass<DailyBudgetReached>()("DailyBudgetReached", {}) {}
294
+ *
295
+ * // The stream of an RPC whose success schema is `Timeline.Change([DailyBudgetReached])`.
296
+ * declare const changes: Stream.Stream<Timeline.Change<DailyBudgetReached>>
297
+ *
298
+ * const timelines = changes.pipe(Timeline.fold) // Stream<Timeline.Timeline<DailyBudgetReached>>
299
+ * ```
300
+ *
301
+ * @category combinators
302
+ * @since 0.3.0
303
+ */
304
+ const fold = (changes) => changes.pipe(Stream.scan(() => empty, apply));
305
+ /**
306
+ * Returns whether nothing is working in the chat: no turn is running or about to start, and no
307
+ * specialist is working.
308
+ *
309
+ * @category guards
310
+ * @since 0.1.0
311
+ */
312
+ const isIdle = (timeline) => !timeline.activity.working && timeline.activity.specialists.length === 0;
313
+ function snapshotWith(entries, _definition, options) {
314
+ const timeline = folding(options?.show ?? []);
315
+ for (const entry of entries) timeline.step(entry);
316
+ return timeline.snapshot;
317
+ }
318
+ /**
319
+ * Returns a chat's whole timeline as one change, built from the entries of its log.
320
+ *
321
+ * **When to use**
322
+ *
323
+ * Use it on the server for an RPC that returns a whole chat at once: its success schema is
324
+ * {@link Change} with the same events as `show`, and the client applies it to {@link empty}.
325
+ *
326
+ * **Details**
327
+ *
328
+ * Only the app's events in `show` become items; without it, the timeline has none.
329
+ *
330
+ * @category constructors
331
+ * @since 0.3.0
332
+ */
333
+ const snapshot = snapshotWith;
334
+ function fromEntriesWith(entries, _definition, options) {
335
+ const timeline = folding(options?.show ?? []);
336
+ for (const entry of entries) timeline.step(entry);
337
+ return timeline.snapshot;
338
+ }
339
+ /**
340
+ * Returns a chat's timeline, built from the entries of its log.
341
+ *
342
+ * **When to use**
343
+ *
344
+ * Use it in server-side code that reads a whole chat. A client gets the timeline as a stream from
345
+ * {@link changes} instead, and an RPC that returns a whole chat sends a {@link snapshot}.
346
+ *
347
+ * **Details**
348
+ *
349
+ * Only the app's events in `show` become items, typed with their schemas; without it, the timeline
350
+ * has none.
351
+ *
352
+ * **Example** (Building a chat's timeline from its log)
353
+ *
354
+ * ```ts
355
+ * import { type ChatId, ChatLog, Timeline } from "effect-assistant"
356
+ * import * as Effect from "effect/Effect"
357
+ *
358
+ * const timelineOf = (chatId: ChatId.ChatId) =>
359
+ * ChatLog.ChatLog.use((log) => log.read(chatId)).pipe(
360
+ * Effect.map((entries) => Timeline.fromEntries(entries))
361
+ * )
362
+ * ```
363
+ *
364
+ * @see {@link changes} for a stream of changes instead of a whole timeline
365
+ *
366
+ * @category constructors
367
+ * @since 0.1.0
368
+ */
369
+ const fromEntries = fromEntriesWith;
370
+ const activityOf = (activity) => ({
371
+ working: activity.turnSince !== void 0 || activity.queued,
372
+ specialists: [...activity.specialists.values()].map((run) => ({
373
+ specialist: run.specialist,
374
+ since: run.since
375
+ }))
376
+ });
377
+ const sameActivity = (a, b) => a.working === b.working && a.specialists.length === b.specialists.length && a.specialists.every((run, i) => {
378
+ const other = b.specialists[i];
379
+ return other !== void 0 && run.specialist === other.specialist && run.since === other.since;
380
+ });
381
+ const folding = (show) => {
382
+ const shown = new Map(show.map((schema) => [tagOf(schema), schema]));
383
+ let approvals = /* @__PURE__ */ new Map();
384
+ let seq = 0;
385
+ let failed = false;
386
+ let deleted = false;
387
+ let activity = empty$1;
388
+ let blocks = /* @__PURE__ */ new Map();
389
+ const items = /* @__PURE__ */ new Map();
390
+ const sent = /* @__PURE__ */ new Map();
391
+ const blockOf = (message) => {
392
+ if (message.block === void 0) return void 0;
393
+ const state = blocks.get(message.id);
394
+ const status = state === void 0 || state.open ? "open" : state.answer === void 0 ? "closed" : "answered";
395
+ const block = {
396
+ kind: message.block.kind,
397
+ props: toJson(message.block.props),
398
+ status
399
+ };
400
+ return state?.answer === void 0 ? block : {
401
+ ...block,
402
+ answer: toJson(state.answer.value)
403
+ };
404
+ };
405
+ const itemOf = (message, at) => {
406
+ const block = blockOf(message);
407
+ return block === void 0 ? AssistantMessage.make({
408
+ id: message.id,
409
+ text: message.text,
410
+ at
411
+ }) : AssistantMessage.make({
412
+ id: message.id,
413
+ text: message.text,
414
+ at,
415
+ block
416
+ });
417
+ };
418
+ const touched = (event, at) => Match.value(event).pipe(Match.tagsExhaustive({
419
+ UserMessage: (message) => {
420
+ items.set(message.id, message.attachments === void 0 ? UserMessage.make({
421
+ id: message.id,
422
+ text: message.text,
423
+ at
424
+ }) : UserMessage.make({
425
+ id: message.id,
426
+ text: message.text,
427
+ attachments: message.attachments,
428
+ at
429
+ }));
430
+ failed = false;
431
+ return [message.id];
432
+ },
433
+ AssistantMessage: (message) => {
434
+ sent.set(message.id, message);
435
+ items.set(message.id, itemOf(message, at));
436
+ failed = false;
437
+ return [message.id, ...message.closes ?? []];
438
+ },
439
+ BlockAnswer: (answer) => {
440
+ failed = false;
441
+ return [answer.messageId];
442
+ },
443
+ BlocksClosed: (closed) => closed.messageIds,
444
+ ApprovalRequested: () => [],
445
+ ApprovalDecision: () => {
446
+ failed = false;
447
+ return [];
448
+ },
449
+ ApprovalsExpired: () => [],
450
+ TurnFailed: () => {
451
+ failed = true;
452
+ return [];
453
+ },
454
+ ModelOutput: () => [],
455
+ ToolStarted: () => [],
456
+ ToolResult: () => [],
457
+ TurnStarted: () => [],
458
+ TurnCompleted: () => [],
459
+ SpecialistStarted: () => [],
460
+ SpecialistFinished: () => [],
461
+ ChatDeleted: () => {
462
+ deleted = true;
463
+ return [];
464
+ }
465
+ }));
466
+ const eventItemOf = (entry) => {
467
+ const schema = shown.get(entry.event._tag);
468
+ if (schema === void 0) return Option.none();
469
+ const event = Schema.is(schema)(entry.event) ? Option.some(entry.event) : decodeWith(schema, entry.event);
470
+ return Option.map(event, (decoded) => ({
471
+ _tag: decoded._tag,
472
+ id: `app-event:${entry.seq}`,
473
+ at: entry.at,
474
+ event: decoded
475
+ }));
476
+ };
477
+ const callsOf = (event) => Predicate.isTagged(event, "ApprovalsExpired") ? event.callIds : Predicate.isTagged(event, "ApprovalRequested") || Predicate.isTagged(event, "ApprovalDecision") || Predicate.isTagged(event, "ToolResult") ? [event.callId] : [];
478
+ const approvalItems = (event, before) => callsOf(event).flatMap((callId) => {
479
+ const state = approvals.get(callId);
480
+ if (state === void 0 || state === before.get(callId)) return [];
481
+ const item = ApprovalItem.make({
482
+ id: callId,
483
+ tool: state.request.tool,
484
+ status: state.status,
485
+ reason: state.reason,
486
+ outcome: state.outcome,
487
+ at: state.at
488
+ });
489
+ items.set(callId, item);
490
+ return [item];
491
+ });
492
+ const step = (entry) => {
493
+ const previous = {
494
+ activity: activityOf(activity),
495
+ failed,
496
+ deleted
497
+ };
498
+ const { event } = entry;
499
+ seq = entry.seq;
500
+ activity = apply$2(activity, entry);
501
+ if (!isLibraryEvent(event)) {
502
+ const now = activityOf(activity);
503
+ return Option.match(eventItemOf(entry), {
504
+ onNone: () => sameActivity(previous.activity, now) ? void 0 : {
505
+ seq,
506
+ items: [],
507
+ activity: now,
508
+ failed,
509
+ deleted
510
+ },
511
+ onSome: (item) => {
512
+ items.set(item.id, item);
513
+ return {
514
+ seq,
515
+ items: [item],
516
+ activity: now,
517
+ failed,
518
+ deleted
519
+ };
520
+ }
521
+ });
522
+ }
523
+ blocks = apply$3(blocks, event);
524
+ const decidedBefore = approvals;
525
+ approvals = apply$1(approvals, entry);
526
+ const all = [...[...new Set(touched(event, entry.at))].flatMap((id) => {
527
+ const message = sent.get(id);
528
+ const current = items.get(id);
529
+ if (message === void 0 || current === void 0) return current === void 0 ? [] : [current];
530
+ const next = itemOf(message, current.at);
531
+ items.set(id, next);
532
+ return [next];
533
+ }), ...approvalItems(event, decidedBefore)];
534
+ const now = activityOf(activity);
535
+ return all.length === 0 && failed === previous.failed && deleted === previous.deleted && sameActivity(previous.activity, now) ? void 0 : {
536
+ seq,
537
+ items: all,
538
+ activity: now,
539
+ failed,
540
+ deleted
541
+ };
542
+ };
543
+ return {
544
+ step,
545
+ get snapshot() {
546
+ return {
547
+ seq,
548
+ items: [...items.values()],
549
+ activity: activityOf(activity),
550
+ failed,
551
+ deleted
552
+ };
553
+ }
554
+ };
555
+ };
556
+ const merge = (changes) => {
557
+ const last = changes.at(-1);
558
+ if (last === void 0) return void 0;
559
+ const items = /* @__PURE__ */ new Map();
560
+ for (const change of changes) for (const item of change.items) items.set(item.id, item);
561
+ return {
562
+ seq: last.seq,
563
+ items: [...items.values()],
564
+ activity: last.activity,
565
+ failed: last.failed,
566
+ deleted: last.deleted
567
+ };
568
+ };
569
+ function changesWith(entries, _definition, options) {
570
+ const after = options?.after ?? 0;
571
+ return entries.pipe(Stream.mapAccumArray(() => folding(options?.show ?? []), (timeline, chunk) => {
572
+ const changed = chunk.flatMap((entry) => {
573
+ const change = timeline.step(entry);
574
+ return change === void 0 || entry.seq <= after ? [] : [change];
575
+ });
576
+ const merged = merge(changed);
577
+ return [timeline, merged === void 0 ? [] : [merged]];
578
+ }));
579
+ }
580
+ /**
581
+ * Returns the stream of changes to a chat's timeline that its entries produce, for a chat client.
582
+ *
583
+ * **Details**
584
+ *
585
+ * The entries must stream from the start of the chat's log, as `assistant.subscribe(chatId)` does
586
+ * without `after`. The fold reads every entry, because a message depends on earlier entries: its
587
+ * block is answered or closed later. `after` only skips emitting the changes up to that `seq`, so a
588
+ * client that has the timeline up to a `seq` receives only what changed since. Pass `after` here,
589
+ * not to `subscribe`: a subscription that starts after a `seq` gives the fold an incomplete log,
590
+ * and the changes it produces are wrong.
591
+ *
592
+ * Each chunk of entries becomes one change; the stored entries, for example, arrive as one chunk.
593
+ *
594
+ * **Only the app's events in `show` reach the client.** Each one becomes an item between the
595
+ * messages (see {@link EventItem}); the app's other events, like a summary of the chat for the
596
+ * model, are left out. Without `show`, none are shown. The changes are typed with the events shown,
597
+ * so the RPC's success schema names the same ones (`Timeline.Change([...])`), and a handler that
598
+ * shows one the schema doesn't name doesn't compile. A timeline shows a turn as working once it
599
+ * starts, or from the user's input.
600
+ *
601
+ * **Example** (Streaming a chat's timeline to a client, with the one app event its user sees)
602
+ *
603
+ * ```ts
604
+ * import { Assistant, type ChatId, Timeline } from "effect-assistant"
605
+ * import * as Schema from "effect/Schema"
606
+ * import * as Stream from "effect/Stream"
607
+ *
608
+ * class Summary extends Schema.TaggedClass<Summary>()("Summary", { text: Schema.String, throughSeq: Schema.Int }) {}
609
+ * class DailyBudgetReached extends Schema.TaggedClass<DailyBudgetReached>()("DailyBudgetReached", {}) {}
610
+ *
611
+ * const MyAssistant = Assistant.make("my-assistant", { events: [Summary, DailyBudgetReached] })
612
+ *
613
+ * // The summary is for the model: only the budget notice reaches the user.
614
+ * const watch = (chatId: ChatId.ChatId, after: number) =>
615
+ * Stream.unwrap(
616
+ * MyAssistant.useSync((assistant) =>
617
+ * assistant.subscribe(chatId).pipe(Timeline.changes(MyAssistant, { show: [DailyBudgetReached], after }))
618
+ * )
619
+ * )
620
+ * ```
621
+ *
622
+ * @see {@link fold} for folding the changes into a timeline on the client
623
+ *
624
+ * @category utils
625
+ * @since 0.3.0
626
+ */
627
+ const changes = dual((args) => Stream.isStream(args[0]), changesWith);
628
+ //#endregion
629
+ export { Activity, ApprovalItem, AssistantMessage, Block, BlockStatus, Change, SpecialistWorking, Timeline_exports, UserMessage, apply, changes, empty, fold, fromEntries, isIdle, snapshot };
630
+
631
+ //# sourceMappingURL=Timeline.js.map