effect-assistant 0.4.0 → 0.6.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 (102) hide show
  1. package/dist/Assistant.d.ts +10 -10
  2. package/dist/Assistant.d.ts.map +1 -1
  3. package/dist/Assistant.js +1 -1
  4. package/dist/Assistant.js.map +1 -1
  5. package/dist/AssistantActivity.d.ts +2 -2
  6. package/dist/AssistantActivity.d.ts.map +1 -1
  7. package/dist/AssistantActivity.js +5 -5
  8. package/dist/AssistantActivity.js.map +1 -1
  9. package/dist/AssistantEngine.d.ts +3 -3
  10. package/dist/AssistantEngine.js +2 -2
  11. package/dist/AssistantEngine.js.map +1 -1
  12. package/dist/AssistantEvent.d.ts +19 -8
  13. package/dist/AssistantEvent.d.ts.map +1 -1
  14. package/dist/AssistantEvent.js +20 -9
  15. package/dist/AssistantEvent.js.map +1 -1
  16. package/dist/AssistantLoop.d.ts +2 -2
  17. package/dist/AssistantLoop.js.map +1 -1
  18. package/dist/Block.d.ts +63 -1
  19. package/dist/Block.d.ts.map +1 -1
  20. package/dist/Block.js +63 -2
  21. package/dist/Block.js.map +1 -1
  22. package/dist/BlockKit.d.ts +25 -3
  23. package/dist/BlockKit.d.ts.map +1 -1
  24. package/dist/BlockKit.js +14 -12
  25. package/dist/BlockKit.js.map +1 -1
  26. package/dist/ChatLog.d.ts +9 -9
  27. package/dist/ChatLog.d.ts.map +1 -1
  28. package/dist/ChatLog.js +21 -21
  29. package/dist/ChatLog.js.map +1 -1
  30. package/dist/Compaction.d.ts +10 -10
  31. package/dist/Compaction.d.ts.map +1 -1
  32. package/dist/Compaction.js +19 -19
  33. package/dist/Compaction.js.map +1 -1
  34. package/dist/Memory.d.ts +1 -1
  35. package/dist/Memory.js +1 -1
  36. package/dist/Memory.js.map +1 -1
  37. package/dist/Skill.d.ts +1 -1
  38. package/dist/Skill.js +1 -1
  39. package/dist/Skill.js.map +1 -1
  40. package/dist/Specialist.d.ts +1 -1
  41. package/dist/Specialist.js +1 -1
  42. package/dist/Specialist.js.map +1 -1
  43. package/dist/Timeline.d.ts +13 -9
  44. package/dist/Timeline.d.ts.map +1 -1
  45. package/dist/Timeline.js +35 -23
  46. package/dist/Timeline.js.map +1 -1
  47. package/dist/ToolApproval.d.ts +1 -1
  48. package/dist/ToolApproval.js +1 -1
  49. package/dist/ToolApproval.js.map +1 -1
  50. package/dist/ToolDiscovery.d.ts +1 -1
  51. package/dist/ToolDiscovery.js +1 -1
  52. package/dist/ToolDiscovery.js.map +1 -1
  53. package/dist/ToolResult.d.ts +56 -0
  54. package/dist/ToolResult.d.ts.map +1 -0
  55. package/dist/ToolResult.js +49 -0
  56. package/dist/ToolResult.js.map +1 -0
  57. package/dist/index.d.ts +2 -1
  58. package/dist/index.js +4 -3
  59. package/dist/internal/chat.js +4 -4
  60. package/dist/internal/chat.js.map +1 -1
  61. package/dist/internal/events.js +1 -1
  62. package/dist/internal/events.js.map +1 -1
  63. package/dist/internal/history.js +23 -23
  64. package/dist/internal/history.js.map +1 -1
  65. package/dist/internal/loop.d.ts +1 -1
  66. package/dist/internal/loop.d.ts.map +1 -1
  67. package/dist/internal/loop.js +12 -6
  68. package/dist/internal/loop.js.map +1 -1
  69. package/dist/internal/toolkit.d.ts +7 -1
  70. package/dist/internal/toolkit.d.ts.map +1 -1
  71. package/dist/internal/toolkit.js +47 -4
  72. package/dist/internal/toolkit.js.map +1 -1
  73. package/dist/internal/tools.js +39 -6
  74. package/dist/internal/tools.js.map +1 -1
  75. package/dist/internal/turns.d.ts.map +1 -1
  76. package/dist/internal/turns.js +24 -21
  77. package/dist/internal/turns.js.map +1 -1
  78. package/package.json +1 -1
  79. package/src/Assistant.ts +11 -11
  80. package/src/AssistantActivity.ts +9 -7
  81. package/src/AssistantEngine.ts +3 -3
  82. package/src/AssistantEvent.ts +21 -10
  83. package/src/AssistantLoop.ts +3 -3
  84. package/src/Block.ts +82 -1
  85. package/src/BlockKit.ts +37 -4
  86. package/src/ChatLog.ts +31 -26
  87. package/src/Compaction.ts +21 -21
  88. package/src/Memory.ts +1 -1
  89. package/src/Skill.ts +1 -1
  90. package/src/Specialist.ts +1 -1
  91. package/src/Timeline.ts +38 -25
  92. package/src/ToolApproval.ts +1 -1
  93. package/src/ToolDiscovery.ts +1 -1
  94. package/src/ToolResult.ts +80 -0
  95. package/src/index.ts +5 -0
  96. package/src/internal/chat.ts +4 -4
  97. package/src/internal/events.ts +1 -1
  98. package/src/internal/history.ts +32 -28
  99. package/src/internal/loop.ts +24 -7
  100. package/src/internal/toolkit.ts +55 -5
  101. package/src/internal/tools.ts +81 -5
  102. package/src/internal/turns.ts +35 -29
package/src/Compaction.ts CHANGED
@@ -22,9 +22,9 @@ import { toJsonText } from "./internal/schema.ts"
22
22
  import * as ToolApproval from "./ToolApproval.ts"
23
23
 
24
24
  /**
25
- * Represents the fields a summary event must have: its text and the last `seq` it covers.
25
+ * Represents the fields a summary event must have: its text and the last `sequence` it covers.
26
26
  *
27
- * Entries after `throughSeq` are kept as they are, so an input that arrived while the summary was
27
+ * Entries after `throughSequence` are kept as they are, so an input that arrived while the summary was
28
28
  * being written isn't lost.
29
29
  *
30
30
  * @category models
@@ -33,7 +33,7 @@ import * as ToolApproval from "./ToolApproval.ts"
33
33
  export interface Summary {
34
34
  readonly _tag: string
35
35
  readonly text: string
36
- readonly throughSeq: number
36
+ readonly throughSequence: number
37
37
  }
38
38
 
39
39
  /**
@@ -55,7 +55,7 @@ export const inputTokens = (entries: ReadonlyArray<AssistantEvent.AnyEntry>): nu
55
55
  }
56
56
 
57
57
  /**
58
- * Returns the items that were open at `throughSeq` and are still open: blocks the user hasn't
58
+ * Returns the items that were open at `throughSequence` and are still open: blocks the user hasn't
59
59
  * answered, specialists that are working, and tool calls waiting for the user's approval. The result is text for
60
60
  * the model, or `None` when nothing is open.
61
61
  *
@@ -64,12 +64,12 @@ export const inputTokens = (entries: ReadonlyArray<AssistantEvent.AnyEntry>): nu
64
64
  */
65
65
  export const openItems = (
66
66
  entries: ReadonlyArray<AssistantEvent.AnyEntry>,
67
- throughSeq: number
67
+ throughSequence: number
68
68
  ): Option.Option<string> => {
69
- const seqOf = (found: (event: AssistantEvent.AnyEntry["event"]) => boolean) =>
70
- entries.find(({ event }) => found(event))?.seq ?? Number.POSITIVE_INFINITY
69
+ const sequenceOf = (found: (event: AssistantEvent.AnyEntry["event"]) => boolean) =>
70
+ entries.find(({ event }) => found(event))?.sequence ?? Number.POSITIVE_INFINITY
71
71
 
72
- const opened = (seq: number) => seq <= throughSeq
72
+ const opened = (sequence: number) => sequence <= throughSequence
73
73
 
74
74
  const blocks = [...BlockKit.fromEntries(entries)].flatMap(([messageId, state]) => {
75
75
  const shown = entries.find(
@@ -81,7 +81,7 @@ export const openItems = (
81
81
 
82
82
  return state.open &&
83
83
  shown !== undefined &&
84
- opened(shown.seq) &&
84
+ opened(shown.sequence) &&
85
85
  message !== undefined &&
86
86
  AssistantEvent.isLibraryEvent(message) &&
87
87
  Predicate.isTagged(message, "AssistantMessage")
@@ -91,7 +91,7 @@ export const openItems = (
91
91
 
92
92
  const specialists = [...AssistantActivity.fromEntries(entries).specialists.values()].flatMap((run) =>
93
93
  opened(
94
- seqOf(
94
+ sequenceOf(
95
95
  (event) =>
96
96
  AssistantEvent.isLibraryEvent(event) &&
97
97
  Predicate.isTagged(event, "SpecialistStarted") &&
@@ -105,7 +105,7 @@ export const openItems = (
105
105
  const approvals = [...ToolApproval.fromEntries(entries).values()].flatMap((approval) =>
106
106
  approval.status === "pending" &&
107
107
  opened(
108
- seqOf(
108
+ sequenceOf(
109
109
  (event) =>
110
110
  AssistantEvent.isLibraryEvent(event) &&
111
111
  Predicate.isTagged(event, "ApprovalRequested") &&
@@ -126,7 +126,7 @@ export const openItems = (
126
126
  }
127
127
 
128
128
  /**
129
- * Returns the files the user attached up to `throughSeq`, as lines of text for the model, or
129
+ * Returns the files the user attached up to `throughSequence`, as lines of text for the model, or
130
130
  * `None` when there are none. A summary may leave out a file's id, which tools need.
131
131
  *
132
132
  * @category utils
@@ -134,10 +134,10 @@ export const openItems = (
134
134
  */
135
135
  export const attachedFiles = (
136
136
  entries: ReadonlyArray<AssistantEvent.AnyEntry>,
137
- throughSeq: number
137
+ throughSequence: number
138
138
  ): Option.Option<string> => {
139
- const lines = entries.flatMap(({ seq, event }) =>
140
- seq <= throughSeq && AssistantEvent.isLibraryEvent(event) && Predicate.isTagged(event, "UserMessage")
139
+ const lines = entries.flatMap(({ sequence, event }) =>
140
+ sequence <= throughSequence && AssistantEvent.isLibraryEvent(event) && Predicate.isTagged(event, "UserMessage")
141
141
  ? (event.attachments ?? []).map(AssistantEvent.attachmentLine)
142
142
  : []
143
143
  )
@@ -170,7 +170,7 @@ export const attachedFiles = (
170
170
  *
171
171
  * class Summary extends Schema.TaggedClass<Summary>()("Summary", {
172
172
  * text: Schema.String,
173
- * throughSeq: Schema.Int
173
+ * throughSequence: Schema.Int
174
174
  * }) {}
175
175
  *
176
176
  * declare const transcript: (entries: ReadonlyArray<AssistantEvent.AnyEntry>) => string
@@ -188,12 +188,12 @@ export const attachedFiles = (
188
188
  * prompt: `Summarize this chat:\n\n${transcript(entries)}`
189
189
  * })
190
190
  *
191
- * yield* chat.append(Summary.make({ text, throughSeq: entries.at(-1)?.seq ?? 0 }))
191
+ * yield* chat.append(Summary.make({ text, throughSequence: entries.at(-1)?.sequence ?? 0 }))
192
192
  * })
193
193
  *
194
194
  * // After the reply, so the user doesn't wait for the summary.
195
195
  * return Stream.concat(
196
- * turn.loop({ instructions: "Be brief.", history: Compaction.since(Schema.is(Summary)) }),
196
+ * turn.run({ instructions: "Be brief.", history: Compaction.since(Schema.is(Summary)) }),
197
197
  * Stream.drain(Stream.fromEffect(compact))
198
198
  * )
199
199
  * })
@@ -211,18 +211,18 @@ export const since =
211
211
 
212
212
  if (last === undefined || !isSummary(last)) return entries
213
213
 
214
- const open = Option.match(openItems(entries, last.throughSeq), {
214
+ const open = Option.match(openItems(entries, last.throughSequence), {
215
215
  onNone: () => "",
216
216
  onSome: (items) => `\n\n<still_open>\n${items}\n</still_open>`
217
217
  })
218
218
 
219
- const files = Option.match(attachedFiles(entries, last.throughSeq), {
219
+ const files = Option.match(attachedFiles(entries, last.throughSequence), {
220
220
  onNone: () => "",
221
221
  onSome: (lines) => `\n\n<files_attached_earlier>\n${lines}\n</files_attached_earlier>`
222
222
  })
223
223
 
224
224
  return [
225
225
  userText(`<earlier_in_this_chat>\n${last.text}\n</earlier_in_this_chat>${files}${open}`),
226
- ...entries.filter(({ seq, event }) => seq > last.throughSeq && !isSummary(event))
226
+ ...entries.filter(({ sequence, event }) => sequence > last.throughSequence && !isSummary(event))
227
227
  ]
228
228
  }
package/src/Memory.ts CHANGED
@@ -218,7 +218,7 @@ const Recall = Tool.make("recall", {
218
218
  * return (turn) =>
219
219
  * Stream.unwrap(
220
220
  * Effect.map(memory.facts(turn.chatId), (facts) =>
221
- * turn.loop({
221
+ * turn.run({
222
222
  * instructions: `You help a small team.\n\n${Memory.render(facts)}`,
223
223
  * toolkit: Memory.Tools
224
224
  * })
package/src/Skill.ts CHANGED
@@ -168,7 +168,7 @@ function toolkitOf(skills: ReadonlyArray<Skill<unknown>>): Toolkit<unknown> {
168
168
  * const AssistantSkills = Skill.toolkit([orderingCampaigns, sharingAssets])
169
169
  *
170
170
  * const MyAssistantLive = MyAssistant.toLayer((turn) =>
171
- * turn.loop({ instructions, toolkit: Toolkit.merge(MyTools, AssistantSkills) })
171
+ * turn.run({ instructions, toolkit: Toolkit.merge(MyTools, AssistantSkills) })
172
172
  * ).pipe(Layer.provide([MyToolsLive, AssistantSkills.layer]))
173
173
  * ```
174
174
  *
package/src/Specialist.ts CHANGED
@@ -226,7 +226,7 @@ const toStream = <R, RE>(
226
226
  *
227
227
  * export const MyAssistant = Assistant.make("my-assistant", { specialists: [Research] })
228
228
  *
229
- * const MyAssistantLive = MyAssistant.toLayer((turn) => turn.loop({ instructions })).pipe(
229
+ * const MyAssistantLive = MyAssistant.toLayer((turn) => turn.run({ instructions })).pipe(
230
230
  * Layer.provide(ResearchLive)
231
231
  * )
232
232
  * ```
package/src/Timeline.ts CHANGED
@@ -64,7 +64,9 @@ export const Block = Schema.Struct({
64
64
  kind: Schema.String,
65
65
  props: Schema.Json,
66
66
  status: BlockStatus,
67
- answer: Schema.optional(Schema.Json)
67
+ answer: Schema.optional(Schema.Json),
68
+ /** For a block that shows a tool's result (`Block.fromToolResult`): the result, as the tool's success schema encodes it. */
69
+ result: Schema.optional(Schema.Json)
68
70
  })
69
71
 
70
72
  /**
@@ -244,13 +246,13 @@ export type Activity = typeof Activity.Type
244
246
  /**
245
247
  * Represents a chat's timeline: the chat as its user sees it, typed with the app's events it shows.
246
248
  * It holds the chat's items in order, what is working in it, whether the last turn failed, and
247
- * whether the chat was deleted. `seq` is the position in the chat's log that the timeline is up to.
249
+ * whether the chat was deleted. `sequence` is the position in the chat's log that the timeline is up to.
248
250
  *
249
251
  * @category models
250
252
  * @since 0.3.0
251
253
  */
252
254
  export interface Timeline<Events = never> {
253
- readonly seq: number
255
+ readonly sequence: number
254
256
  readonly items: ReadonlyArray<Item<Events>>
255
257
  readonly activity: Activity
256
258
  /** Whether the last turn failed with no message after it. */
@@ -266,14 +268,14 @@ export interface Timeline<Events = never> {
266
268
  *
267
269
  * A change holds each item that is new or changed (for example, a message whose block was
268
270
  * answered or closed, or a call the user approved), in full. It also holds the current activity, whether the last turn failed,
269
- * and whether the chat was deleted. `seq` is the position in the chat's log that the change is up
271
+ * and whether the chat was deleted. `sequence` is the position in the chat's log that the change is up
270
272
  * to.
271
273
  *
272
274
  * @category models
273
275
  * @since 0.3.0
274
276
  */
275
277
  export interface Change<Events = never> {
276
- readonly seq: number
278
+ readonly sequence: number
277
279
  readonly items: ReadonlyArray<Item<Events>>
278
280
  readonly activity: Activity
279
281
  readonly failed: boolean
@@ -296,7 +298,7 @@ function makeChange(events: ReadonlyArray<AssistantEvent.AppEventSchema> = []):
296
298
  const encodeItem = Schema.encodeUnknownSync(AnyItem)
297
299
 
298
300
  return Schema.Struct({
299
- seq: Schema.Int,
301
+ sequence: Schema.Int,
300
302
  // An item for an event this schema doesn't name, or that doesn't decode (from a newer server, say), is left out
301
303
  // rather than failing the whole change.
302
304
  items: Schema.Array(Schema.Json).pipe(
@@ -359,7 +361,7 @@ export const Change: <const Events extends ReadonlyArray<AssistantEvent.AppEvent
359
361
  * @since 0.1.0
360
362
  */
361
363
  export const empty: Timeline = {
362
- seq: 0,
364
+ sequence: 0,
363
365
  items: [],
364
366
  activity: { working: false, specialists: [] },
365
367
  failed: false,
@@ -405,7 +407,7 @@ export const apply = <Events>(timeline: Timeline<Events>, change: Change<Events>
405
407
  const known = new Set(timeline.items.map((item) => item.id))
406
408
 
407
409
  return {
408
- seq: Math.max(timeline.seq, change.seq),
410
+ sequence: Math.max(timeline.sequence, change.sequence),
409
411
  items: [
410
412
  ...timeline.items.map((item) => changed.get(item.id) ?? item),
411
413
  ...change.items.filter((item) => !known.has(item.id))
@@ -463,7 +465,7 @@ type AnyItem = UserMessage | AssistantMessage | AnyEventItem | ApprovalItem
463
465
 
464
466
  // What the fold below builds: the overloads that use it type its items with the events shown.
465
467
  interface AnyChange {
466
- readonly seq: number
468
+ readonly sequence: number
467
469
  readonly items: ReadonlyArray<AnyItem>
468
470
  readonly activity: Activity
469
471
  readonly failed: boolean
@@ -620,7 +622,7 @@ const sameActivity = (a: Activity, b: Activity) =>
620
622
  const folding = (show: ReadonlyArray<AssistantEvent.AppEventSchema>): Folding => {
621
623
  const shown = new Map(show.map((schema) => [AppEvents.tagOf(schema), schema]))
622
624
  let approvals: ReadonlyMap<string, ToolApproval.State> = new Map()
623
- let seq = 0
625
+ let sequence = 0
624
626
  let failed = false
625
627
  let deleted = false
626
628
  let activity = AssistantActivity.empty
@@ -628,6 +630,8 @@ const folding = (show: ReadonlyArray<AssistantEvent.AppEventSchema>): Folding =>
628
630
  // The items in order, and the assistant's messages as stored: their blocks change as the chat goes on.
629
631
  const items = new Map<string, AnyItem>()
630
632
  const sent = new Map<string, AssistantEvent.AssistantMessage>()
633
+ // The results a block can show, by id: a message shows one stored before it.
634
+ const results = new Map<string, Schema.Json>()
631
635
 
632
636
  const blockOf = (message: AssistantEvent.AssistantMessage): Block | undefined => {
633
637
  if (message.block === undefined) return undefined
@@ -635,8 +639,11 @@ const folding = (show: ReadonlyArray<AssistantEvent.AppEventSchema>): Folding =>
635
639
  const state = blocks.get(message.id)
636
640
  const status = state === undefined || state.open ? "open" : state.answer === undefined ? "closed" : "answered"
637
641
  const block: Block = { kind: message.block.kind, props: toJson(message.block.props), status }
642
+ const { resultId } = message.block
643
+ const result = resultId === undefined ? undefined : results.get(resultId)
644
+ const shown = result === undefined ? block : { ...block, result }
638
645
 
639
- return state?.answer === undefined ? block : { ...block, answer: toJson(state.answer.value) }
646
+ return state?.answer === undefined ? shown : { ...shown, answer: toJson(state.answer.value) }
640
647
  }
641
648
 
642
649
  const itemOf = (message: AssistantEvent.AssistantMessage, at: number): AssistantMessage => {
@@ -693,7 +700,13 @@ const folding = (show: ReadonlyArray<AssistantEvent.AppEventSchema>): Folding =>
693
700
  },
694
701
  ModelOutput: () => [],
695
702
  ToolStarted: () => [],
696
- ToolResult: () => [],
703
+ ToolResult: (result) => {
704
+ if (result.resultId !== undefined && result.outcome === "success") {
705
+ results.set(result.resultId, result.full ?? result.result)
706
+ }
707
+
708
+ return []
709
+ },
697
710
  TurnStarted: () => [],
698
711
  TurnCompleted: () => [],
699
712
  SpecialistStarted: () => [],
@@ -716,7 +729,7 @@ const folding = (show: ReadonlyArray<AssistantEvent.AppEventSchema>): Folding =>
716
729
 
717
730
  return Option.map(event, (decoded) => ({
718
731
  _tag: decoded._tag,
719
- id: `app-event:${entry.seq}`,
732
+ id: `app-event:${entry.sequence}`,
720
733
  at: entry.at,
721
734
  event: decoded
722
735
  }))
@@ -756,7 +769,7 @@ const folding = (show: ReadonlyArray<AssistantEvent.AppEventSchema>): Folding =>
756
769
  const previous = { activity: activityOf(activity), failed, deleted }
757
770
  const { event } = entry
758
771
 
759
- seq = entry.seq
772
+ sequence = entry.sequence
760
773
  activity = AssistantActivity.apply(activity, entry)
761
774
 
762
775
  if (!AssistantEvent.isLibraryEvent(event)) {
@@ -764,11 +777,11 @@ const folding = (show: ReadonlyArray<AssistantEvent.AppEventSchema>): Folding =>
764
777
 
765
778
  return Option.match(eventItemOf(entry), {
766
779
  onNone: () =>
767
- sameActivity(previous.activity, now) ? undefined : { seq, items: [], activity: now, failed, deleted },
780
+ sameActivity(previous.activity, now) ? undefined : { sequence, items: [], activity: now, failed, deleted },
768
781
  onSome: (item) => {
769
782
  items.set(item.id, item)
770
783
 
771
- return { seq, items: [item], activity: now, failed, deleted }
784
+ return { sequence, items: [item], activity: now, failed, deleted }
772
785
  }
773
786
  })
774
787
  }
@@ -798,13 +811,13 @@ const folding = (show: ReadonlyArray<AssistantEvent.AppEventSchema>): Folding =>
798
811
  deleted === previous.deleted &&
799
812
  sameActivity(previous.activity, now)
800
813
  ? undefined
801
- : { seq, items: all, activity: now, failed, deleted }
814
+ : { sequence, items: all, activity: now, failed, deleted }
802
815
  }
803
816
 
804
817
  return {
805
818
  step,
806
819
  get snapshot() {
807
- return { seq, items: [...items.values()], activity: activityOf(activity), failed, deleted }
820
+ return { sequence, items: [...items.values()], activity: activityOf(activity), failed, deleted }
808
821
  }
809
822
  }
810
823
  }
@@ -819,7 +832,7 @@ const merge = (changes: ReadonlyArray<AnyChange>): AnyChange | undefined => {
819
832
  for (const change of changes) for (const item of change.items) items.set(item.id, item)
820
833
 
821
834
  return {
822
- seq: last.seq,
835
+ sequence: last.sequence,
823
836
  items: [...items.values()],
824
837
  activity: last.activity,
825
838
  failed: last.failed,
@@ -851,7 +864,7 @@ export interface ChangesOptions<
851
864
  Show extends ReadonlyArray<AssistantEvent.AppEventSchema> = readonly []
852
865
  > extends ShowOptions<Show> {
853
866
  /**
854
- * The `seq` up to which the client already has the timeline. The result contains only the
867
+ * The `sequence` up to which the client already has the timeline. The result contains only the
855
868
  * changes after it. Defaults to `0`, every change.
856
869
  */
857
870
  readonly after?: number | undefined
@@ -876,7 +889,7 @@ function changesWith<E, R>(
876
889
  const changed = chunk.flatMap((entry) => {
877
890
  const change = timeline.step(entry)
878
891
 
879
- return change === undefined || entry.seq <= after ? [] : [change]
892
+ return change === undefined || entry.sequence <= after ? [] : [change]
880
893
  })
881
894
 
882
895
  const merged = merge(changed)
@@ -894,9 +907,9 @@ function changesWith<E, R>(
894
907
  *
895
908
  * The entries must stream from the start of the chat's log, as `assistant.subscribe(chatId)` does
896
909
  * without `after`. The fold reads every entry, because a message depends on earlier entries: its
897
- * block is answered or closed later. `after` only skips emitting the changes up to that `seq`, so a
898
- * client that has the timeline up to a `seq` receives only what changed since. Pass `after` here,
899
- * not to `subscribe`: a subscription that starts after a `seq` gives the fold an incomplete log,
910
+ * block is answered or closed later. `after` only skips emitting the changes up to that `sequence`, so a
911
+ * client that has the timeline up to a `sequence` receives only what changed since. Pass `after` here,
912
+ * not to `subscribe`: a subscription that starts after a `sequence` gives the fold an incomplete log,
900
913
  * and the changes it produces are wrong.
901
914
  *
902
915
  * Each chunk of entries becomes one change; the stored entries, for example, arrive as one chunk.
@@ -915,7 +928,7 @@ function changesWith<E, R>(
915
928
  * import * as Schema from "effect/Schema"
916
929
  * import * as Stream from "effect/Stream"
917
930
  *
918
- * class Summary extends Schema.TaggedClass<Summary>()("Summary", { text: Schema.String, throughSeq: Schema.Int }) {}
931
+ * class Summary extends Schema.TaggedClass<Summary>()("Summary", { text: Schema.String, throughSequence: Schema.Int }) {}
919
932
  * class DailyBudgetReached extends Schema.TaggedClass<DailyBudgetReached>()("DailyBudgetReached", {}) {}
920
933
  *
921
934
  * const MyAssistant = Assistant.make("my-assistant", { events: [Summary, DailyBudgetReached] })
@@ -231,7 +231,7 @@ export const staleApprovals = (
231
231
  * yield* BlockKit.closeStale({ messages: 10 })
232
232
  * yield* ToolApproval.closeStale({ messages: 10 })
233
233
  *
234
- * return turn.loop({ instructions, toolkit: MyTools })
234
+ * return turn.run({ instructions, toolkit: MyTools })
235
235
  * })
236
236
  * )
237
237
  * ```
@@ -110,7 +110,7 @@ const requiring =
110
110
  * const OfficeDiscoveryLive = ToolDiscovery.layer(OfficeTools).pipe(Layer.provide(OfficeToolsLive))
111
111
  *
112
112
  * const MyAssistantLive = MyAssistant.toLayer((turn) =>
113
- * turn.loop({
113
+ * turn.run({
114
114
  * instructions,
115
115
  * toolkit: Toolkit.merge(CommonTools, OfficeDiscovery)
116
116
  * })
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Shapes what the model reads of a tool's result.
3
+ *
4
+ * A tool's success goes into the chat's log in full. {@link modelView} gives the model a smaller
5
+ * view of it, so fields only the app's UI needs (an image URL, a file id) stay out of its context
6
+ * and out of what it might copy into its replies.
7
+ *
8
+ * A tool whose results the assistant's blocks show (`Block.fromToolResult`) also gets a `resultId`
9
+ * in what the model reads. The model passes it in a `tool_result` block to show that result, and
10
+ * the client draws it from the full result in the log.
11
+ *
12
+ * @since 0.5.0
13
+ */
14
+ import type * as Tool from "effect/ai/Tool"
15
+ import * as Context from "effect/Context"
16
+ import { dual } from "effect/Function"
17
+ import type * as Schema from "effect/Schema"
18
+
19
+ // The tool's schemas, as `Tool.Tool` constrains them.
20
+ type Schemas = {
21
+ readonly parameters: Schema.Top
22
+ readonly success: Schema.Top
23
+ readonly failure: Schema.Top
24
+ readonly failureMode: Tool.FailureMode
25
+ }
26
+
27
+ /**
28
+ * Annotation for what the model reads of a tool's success, set with {@link modelView}.
29
+ *
30
+ * @category annotations
31
+ * @since 0.5.0
32
+ */
33
+ export class ModelView extends Context.Service<ModelView, (success: any) => Schema.Json>()(
34
+ "effect-assistant/ToolResult/ModelView"
35
+ ) {}
36
+
37
+ /**
38
+ * Sets what the model reads of a tool's success. The chat's log keeps the whole success.
39
+ *
40
+ * **Details**
41
+ *
42
+ * The view gets the success as the tool's success schema decodes it, and returns the JSON the
43
+ * model reads instead. Failures reach the model as they are.
44
+ *
45
+ * **Example** (Keeping image URLs out of the model's context)
46
+ *
47
+ * ```ts
48
+ * import * as Tool from "effect/ai/Tool"
49
+ * import * as Schema from "effect/Schema"
50
+ * import { ToolResult } from "effect-assistant"
51
+ *
52
+ * const Asset = Schema.Struct({ id: Schema.String, name: Schema.String, imageUrl: Schema.String })
53
+ *
54
+ * export const SearchAssets = ToolResult.modelView(
55
+ * Tool.make("search_assets", {
56
+ * parameters: Schema.Struct({ query: Schema.String }),
57
+ * success: Schema.Struct({ items: Schema.Array(Asset) })
58
+ * }),
59
+ * (result) => ({ items: result.items.map(({ id, name }) => ({ id, name })) })
60
+ * )
61
+ * ```
62
+ *
63
+ * @category annotations
64
+ * @since 0.5.0
65
+ */
66
+ export const modelView: {
67
+ <Config extends Schemas>(
68
+ view: (success: Config["success"]["Type"]) => Schema.Json
69
+ ): <Name extends string, R>(tool: Tool.Tool<Name, Config, R>) => Tool.Tool<Name, Config, R>
70
+ <Name extends string, Config extends Schemas, R>(
71
+ tool: Tool.Tool<Name, Config, R>,
72
+ view: (success: Config["success"]["Type"]) => Schema.Json
73
+ ): Tool.Tool<Name, Config, R>
74
+ } = dual(
75
+ 2,
76
+ <Name extends string, Config extends Schemas, R>(
77
+ tool: Tool.Tool<Name, Config, R>,
78
+ view: (success: Config["success"]["Type"]) => Schema.Json
79
+ ): Tool.Tool<Name, Config, R> => tool.annotate(ModelView, view)
80
+ )
package/src/index.ts CHANGED
@@ -118,3 +118,8 @@ export * as ToolDiscovery from "./ToolDiscovery.ts"
118
118
  * @since 0.1.0
119
119
  */
120
120
  export * as ToolFailure from "./ToolFailure.ts"
121
+
122
+ /**
123
+ * @since 0.5.0
124
+ */
125
+ export * as ToolResult from "./ToolResult.ts"
@@ -87,7 +87,7 @@ const deletedChat = (entries: ReadonlyArray<AssistantEvent.Entry>): Chat => ({
87
87
  submit: () => Effect.void,
88
88
  submitInBackground: () => Effect.void,
89
89
  resume: Effect.void,
90
- stream: (after) => Stream.fromIterable(entries.filter((entry) => entry.seq > after)),
90
+ stream: (after) => Stream.fromIterable(entries.filter((entry) => entry.sequence > after)),
91
91
  delete: Effect.void
92
92
  })
93
93
 
@@ -314,15 +314,15 @@ export const make = Effect.fnUntraced(function* (options: {
314
314
  // Subscribes before it reads, so nothing appended in between is missed.
315
315
  const subscription = yield* PubSub.subscribe(channel)
316
316
  const all = yield* chat.entries
317
- const stored = all.filter((entry) => entry.seq > after)
318
- const last = stored.at(-1)?.seq ?? after
317
+ const stored = all.filter((entry) => entry.sequence > after)
318
+ const last = stored.at(-1)?.sequence ?? after
319
319
 
320
320
  // Deleted: what the reader hasn't seen, then the end. Otherwise the stream ends with the `ChatDeleted`.
321
321
  if (all.some(isChatDeleted)) return Stream.fromIterable(stored)
322
322
 
323
323
  return Stream.concat(
324
324
  Stream.fromIterable(stored),
325
- Stream.fromSubscription(subscription).pipe(Stream.filter((entry) => entry.seq > last))
325
+ Stream.fromSubscription(subscription).pipe(Stream.filter((entry) => entry.sequence > last))
326
326
  ).pipe(Stream.takeUntil(isChatDeleted))
327
327
  })
328
328
  ),
@@ -95,6 +95,6 @@ export type DecodedEntries = ReturnType<typeof decodeEntries>
95
95
 
96
96
  const toEntry = (entry: AssistantEvent.AnyEntry) => ({
97
97
  chatId: entry.chatId,
98
- seq: entry.seq,
98
+ sequence: entry.sequence,
99
99
  at: entry.at
100
100
  })