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.
- package/dist/Assistant.d.ts +10 -10
- package/dist/Assistant.d.ts.map +1 -1
- package/dist/Assistant.js +1 -1
- package/dist/Assistant.js.map +1 -1
- package/dist/AssistantActivity.d.ts +2 -2
- package/dist/AssistantActivity.d.ts.map +1 -1
- package/dist/AssistantActivity.js +5 -5
- package/dist/AssistantActivity.js.map +1 -1
- package/dist/AssistantEngine.d.ts +3 -3
- package/dist/AssistantEngine.js +2 -2
- package/dist/AssistantEngine.js.map +1 -1
- package/dist/AssistantEvent.d.ts +19 -8
- package/dist/AssistantEvent.d.ts.map +1 -1
- package/dist/AssistantEvent.js +20 -9
- package/dist/AssistantEvent.js.map +1 -1
- package/dist/AssistantLoop.d.ts +2 -2
- package/dist/AssistantLoop.js.map +1 -1
- package/dist/Block.d.ts +63 -1
- package/dist/Block.d.ts.map +1 -1
- package/dist/Block.js +63 -2
- package/dist/Block.js.map +1 -1
- package/dist/BlockKit.d.ts +25 -3
- package/dist/BlockKit.d.ts.map +1 -1
- package/dist/BlockKit.js +14 -12
- package/dist/BlockKit.js.map +1 -1
- package/dist/ChatLog.d.ts +9 -9
- package/dist/ChatLog.d.ts.map +1 -1
- package/dist/ChatLog.js +21 -21
- package/dist/ChatLog.js.map +1 -1
- package/dist/Compaction.d.ts +10 -10
- package/dist/Compaction.d.ts.map +1 -1
- package/dist/Compaction.js +19 -19
- package/dist/Compaction.js.map +1 -1
- package/dist/Memory.d.ts +1 -1
- package/dist/Memory.js +1 -1
- package/dist/Memory.js.map +1 -1
- package/dist/Skill.d.ts +1 -1
- package/dist/Skill.js +1 -1
- package/dist/Skill.js.map +1 -1
- package/dist/Specialist.d.ts +1 -1
- package/dist/Specialist.js +1 -1
- package/dist/Specialist.js.map +1 -1
- package/dist/Timeline.d.ts +13 -9
- package/dist/Timeline.d.ts.map +1 -1
- package/dist/Timeline.js +35 -23
- package/dist/Timeline.js.map +1 -1
- package/dist/ToolApproval.d.ts +1 -1
- package/dist/ToolApproval.js +1 -1
- package/dist/ToolApproval.js.map +1 -1
- package/dist/ToolDiscovery.d.ts +1 -1
- package/dist/ToolDiscovery.js +1 -1
- package/dist/ToolDiscovery.js.map +1 -1
- package/dist/ToolResult.d.ts +56 -0
- package/dist/ToolResult.d.ts.map +1 -0
- package/dist/ToolResult.js +49 -0
- package/dist/ToolResult.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +4 -3
- package/dist/internal/chat.js +4 -4
- package/dist/internal/chat.js.map +1 -1
- package/dist/internal/events.js +1 -1
- package/dist/internal/events.js.map +1 -1
- package/dist/internal/history.js +23 -23
- package/dist/internal/history.js.map +1 -1
- package/dist/internal/loop.d.ts +1 -1
- package/dist/internal/loop.d.ts.map +1 -1
- package/dist/internal/loop.js +12 -6
- package/dist/internal/loop.js.map +1 -1
- package/dist/internal/toolkit.d.ts +7 -1
- package/dist/internal/toolkit.d.ts.map +1 -1
- package/dist/internal/toolkit.js +47 -4
- package/dist/internal/toolkit.js.map +1 -1
- package/dist/internal/tools.js +39 -6
- package/dist/internal/tools.js.map +1 -1
- package/dist/internal/turns.d.ts.map +1 -1
- package/dist/internal/turns.js +24 -21
- package/dist/internal/turns.js.map +1 -1
- package/package.json +1 -1
- package/src/Assistant.ts +11 -11
- package/src/AssistantActivity.ts +9 -7
- package/src/AssistantEngine.ts +3 -3
- package/src/AssistantEvent.ts +21 -10
- package/src/AssistantLoop.ts +3 -3
- package/src/Block.ts +82 -1
- package/src/BlockKit.ts +37 -4
- package/src/ChatLog.ts +31 -26
- package/src/Compaction.ts +21 -21
- package/src/Memory.ts +1 -1
- package/src/Skill.ts +1 -1
- package/src/Specialist.ts +1 -1
- package/src/Timeline.ts +38 -25
- package/src/ToolApproval.ts +1 -1
- package/src/ToolDiscovery.ts +1 -1
- package/src/ToolResult.ts +80 -0
- package/src/index.ts +5 -0
- package/src/internal/chat.ts +4 -4
- package/src/internal/events.ts +1 -1
- package/src/internal/history.ts +32 -28
- package/src/internal/loop.ts +24 -7
- package/src/internal/toolkit.ts +55 -5
- package/src/internal/tools.ts +81 -5
- 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 `
|
|
25
|
+
* Represents the fields a summary event must have: its text and the last `sequence` it covers.
|
|
26
26
|
*
|
|
27
|
-
* Entries after `
|
|
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
|
|
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 `
|
|
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
|
-
|
|
67
|
+
throughSequence: number
|
|
68
68
|
): Option.Option<string> => {
|
|
69
|
-
const
|
|
70
|
-
entries.find(({ event }) => found(event))?.
|
|
69
|
+
const sequenceOf = (found: (event: AssistantEvent.AnyEntry["event"]) => boolean) =>
|
|
70
|
+
entries.find(({ event }) => found(event))?.sequence ?? Number.POSITIVE_INFINITY
|
|
71
71
|
|
|
72
|
-
const opened = (
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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 `
|
|
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
|
-
|
|
137
|
+
throughSequence: number
|
|
138
138
|
): Option.Option<string> => {
|
|
139
|
-
const lines = entries.flatMap(({
|
|
140
|
-
|
|
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
|
-
*
|
|
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,
|
|
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.
|
|
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.
|
|
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.
|
|
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(({
|
|
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.
|
|
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.
|
|
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.
|
|
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. `
|
|
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
|
|
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. `
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 ?
|
|
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.
|
|
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
|
-
|
|
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 : {
|
|
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 {
|
|
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
|
-
: {
|
|
814
|
+
: { sequence, items: all, activity: now, failed, deleted }
|
|
802
815
|
}
|
|
803
816
|
|
|
804
817
|
return {
|
|
805
818
|
step,
|
|
806
819
|
get snapshot() {
|
|
807
|
-
return {
|
|
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
|
-
|
|
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 `
|
|
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.
|
|
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 `
|
|
898
|
-
* client that has the timeline up to a `
|
|
899
|
-
* not to `subscribe`: a subscription that starts after a `
|
|
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,
|
|
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] })
|
package/src/ToolApproval.ts
CHANGED
|
@@ -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.
|
|
234
|
+
* return turn.run({ instructions, toolkit: MyTools })
|
|
235
235
|
* })
|
|
236
236
|
* )
|
|
237
237
|
* ```
|
package/src/ToolDiscovery.ts
CHANGED
|
@@ -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.
|
|
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
package/src/internal/chat.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
318
|
-
const last = stored.at(-1)?.
|
|
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.
|
|
325
|
+
Stream.fromSubscription(subscription).pipe(Stream.filter((entry) => entry.sequence > last))
|
|
326
326
|
).pipe(Stream.takeUntil(isChatDeleted))
|
|
327
327
|
})
|
|
328
328
|
),
|