effect-assistant 0.5.0 → 0.7.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 +8 -8
- package/dist/AssistantEvent.js +7 -7
- package/dist/AssistantEvent.js.map +1 -1
- package/dist/AssistantLoop.d.ts +2 -2
- package/dist/AssistantLoop.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 +16 -16
- 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 +2 -2
- package/dist/Skill.js +2 -2
- package/dist/Skill.js.map +1 -1
- package/dist/Specialist.d.ts +17 -16
- package/dist/Specialist.d.ts.map +1 -1
- package/dist/Specialist.js +5 -5
- package/dist/Specialist.js.map +1 -1
- package/dist/Timeline.d.ts +9 -9
- package/dist/Timeline.js +16 -16
- 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/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.js +4 -4
- package/dist/internal/loop.js.map +1 -1
- package/dist/internal/specialists.js +6 -6
- package/dist/internal/specialists.js.map +1 -1
- package/dist/internal/turns.d.ts.map +1 -1
- package/dist/internal/turns.js +18 -18
- 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 +8 -8
- package/src/AssistantLoop.ts +3 -3
- package/src/ChatLog.ts +31 -26
- package/src/Compaction.ts +21 -21
- package/src/Memory.ts +1 -1
- package/src/Skill.ts +2 -2
- package/src/Specialist.ts +19 -18
- package/src/Timeline.ts +22 -22
- package/src/ToolApproval.ts +1 -1
- package/src/ToolDiscovery.ts +1 -1
- 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 +5 -5
- package/src/internal/specialists.ts +7 -7
- package/src/internal/turns.ts +28 -26
package/src/ChatLog.ts
CHANGED
|
@@ -33,7 +33,7 @@ export class ChatLog extends Context.Service<
|
|
|
33
33
|
chatId: ChatId,
|
|
34
34
|
events: ReadonlyArray<AssistantEvent.AssistantEvent | AssistantEvent.StoredAppEvent>
|
|
35
35
|
) => Effect.Effect<ReadonlyArray<AssistantEvent.Entry>>
|
|
36
|
-
/** Reads a chat's stored entries in order, either all of them or only those after a `
|
|
36
|
+
/** Reads a chat's stored entries in order, either all of them or only those after a `sequence`. */
|
|
37
37
|
readonly read: (chatId: ChatId, options?: ReadOptions) => Effect.Effect<ReadonlyArray<AssistantEvent.Entry>>
|
|
38
38
|
/**
|
|
39
39
|
* Returns the chats whose last entry ends neither a turn nor the chat. After a restart, these
|
|
@@ -50,14 +50,14 @@ export class ChatLog extends Context.Service<
|
|
|
50
50
|
* @since 0.1.0
|
|
51
51
|
*/
|
|
52
52
|
export interface ReadOptions {
|
|
53
|
-
/** Reads only the entries after this `
|
|
53
|
+
/** Reads only the entries after this `sequence`. */
|
|
54
54
|
readonly after?: number | undefined
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
/**
|
|
58
58
|
* Represents the storage that a `ChatLog` is built on.
|
|
59
59
|
*
|
|
60
|
-
* A storage reads a chat's entries, finds a chat's last `
|
|
60
|
+
* A storage reads a chat's entries, finds a chat's last `sequence`, writes new entries, and finds every
|
|
61
61
|
* chat's last entry. Entries are never removed, so a deleted chat keeps its log, which ends in
|
|
62
62
|
* `ChatDeleted`.
|
|
63
63
|
*
|
|
@@ -66,7 +66,7 @@ export interface ReadOptions {
|
|
|
66
66
|
*/
|
|
67
67
|
export interface Storage {
|
|
68
68
|
readonly read: (chatId: ChatId, options?: ReadOptions) => Effect.Effect<ReadonlyArray<AssistantEvent.Entry>>
|
|
69
|
-
readonly
|
|
69
|
+
readonly lastSequence: (chatId: ChatId) => Effect.Effect<number>
|
|
70
70
|
readonly write: (chatId: ChatId, entries: ReadonlyArray<AssistantEvent.Entry>) => Effect.Effect<void>
|
|
71
71
|
/** Returns the last entry of every chat. */
|
|
72
72
|
readonly lastEntries: Effect.Effect<ReadonlyArray<AssistantEvent.Entry>>
|
|
@@ -74,7 +74,7 @@ export interface Storage {
|
|
|
74
74
|
|
|
75
75
|
/**
|
|
76
76
|
* Creates a `ChatLog` from a {@link Storage}. `append` numbers new entries after the chat's last
|
|
77
|
-
* `
|
|
77
|
+
* `sequence` and stamps them with the current time.
|
|
78
78
|
*
|
|
79
79
|
* @category constructors
|
|
80
80
|
* @since 0.1.0
|
|
@@ -88,9 +88,12 @@ export const make = (storage: Storage): ChatLog["Service"] =>
|
|
|
88
88
|
) {
|
|
89
89
|
if (events.length === 0) return []
|
|
90
90
|
|
|
91
|
-
const
|
|
91
|
+
const sequence = yield* storage.lastSequence(chatId)
|
|
92
92
|
const at = yield* Clock.currentTimeMillis
|
|
93
|
-
|
|
93
|
+
|
|
94
|
+
const entries = events.map((event, n) =>
|
|
95
|
+
AssistantEvent.Entry.make({ chatId, sequence: sequence + n + 1, at, event })
|
|
96
|
+
)
|
|
94
97
|
|
|
95
98
|
yield* storage.write(chatId, entries)
|
|
96
99
|
|
|
@@ -121,12 +124,12 @@ export const layerMemory: Layer.Layer<ChatLog> = Layer.effect(
|
|
|
121
124
|
read: Effect.fnUntraced(function* (chatId: ChatId, options?: ReadOptions) {
|
|
122
125
|
const all = yield* Ref.get(chats)
|
|
123
126
|
|
|
124
|
-
return (all.get(chatId) ?? []).filter((entry) => entry.
|
|
127
|
+
return (all.get(chatId) ?? []).filter((entry) => entry.sequence > (options?.after ?? 0))
|
|
125
128
|
}),
|
|
126
|
-
|
|
129
|
+
lastSequence: Effect.fnUntraced(function* (chatId: ChatId) {
|
|
127
130
|
const all = yield* Ref.get(chats)
|
|
128
131
|
|
|
129
|
-
return all.get(chatId)?.at(-1)?.
|
|
132
|
+
return all.get(chatId)?.at(-1)?.sequence ?? 0
|
|
130
133
|
}),
|
|
131
134
|
write: (chatId, entries) =>
|
|
132
135
|
Ref.update(chats, (all) => new Map(all).set(chatId, [...(all.get(chatId) ?? []), ...entries])),
|
|
@@ -144,17 +147,19 @@ const EventJson = Schema.fromJsonString(Schema.Union([AssistantEvent.AssistantEv
|
|
|
144
147
|
// Postgres returns BIGINT and DOUBLE PRECISION columns as strings; other databases as numbers.
|
|
145
148
|
const SqlNumber = Schema.Union([Schema.Finite, Schema.FiniteFromString])
|
|
146
149
|
|
|
147
|
-
const Row = Schema.Struct({ chat_id: ChatId,
|
|
150
|
+
const Row = Schema.Struct({ chat_id: ChatId, sequence: SqlNumber, at: SqlNumber, event: EventJson })
|
|
148
151
|
|
|
149
|
-
const
|
|
152
|
+
const decodeLastSequence = Schema.decodeUnknownEffect(
|
|
153
|
+
Schema.Array(Schema.Struct({ sequence: Schema.NullOr(SqlNumber) }))
|
|
154
|
+
)
|
|
150
155
|
|
|
151
156
|
/**
|
|
152
157
|
* Layer that stores chats in a SQL table named `chat_log`.
|
|
153
158
|
*
|
|
154
159
|
* **Details**
|
|
155
160
|
*
|
|
156
|
-
* The table must already exist. It has the columns `chat_id`, `
|
|
157
|
-
* primary key `(chat_id,
|
|
161
|
+
* The table must already exist. It has the columns `chat_id`, `sequence`, `at`, and `event`, with the
|
|
162
|
+
* primary key `(chat_id, sequence)`. The layer requires a `SqlClient`.
|
|
158
163
|
*
|
|
159
164
|
* **Example** (Storing chats in Postgres)
|
|
160
165
|
*
|
|
@@ -174,10 +179,10 @@ const decodeLastSeq = Schema.decodeUnknownEffect(Schema.Array(Schema.Struct({ se
|
|
|
174
179
|
* ```sql
|
|
175
180
|
* CREATE TABLE chat_log (
|
|
176
181
|
* chat_id TEXT NOT NULL,
|
|
177
|
-
*
|
|
182
|
+
* sequence INTEGER NOT NULL,
|
|
178
183
|
* at DOUBLE PRECISION NOT NULL,
|
|
179
184
|
* event TEXT NOT NULL,
|
|
180
|
-
* PRIMARY KEY (chat_id,
|
|
185
|
+
* PRIMARY KEY (chat_id, sequence)
|
|
181
186
|
* );
|
|
182
187
|
* ```
|
|
183
188
|
*
|
|
@@ -195,7 +200,7 @@ export const layerSql: Layer.Layer<ChatLog, never, SqlClient.SqlClient> = Layer.
|
|
|
195
200
|
return decoded.map((row) =>
|
|
196
201
|
AssistantEvent.Entry.make({
|
|
197
202
|
chatId: row.chat_id,
|
|
198
|
-
|
|
203
|
+
sequence: row.sequence,
|
|
199
204
|
at: row.at,
|
|
200
205
|
event: row.event
|
|
201
206
|
})
|
|
@@ -207,19 +212,19 @@ export const layerSql: Layer.Layer<ChatLog, never, SqlClient.SqlClient> = Layer.
|
|
|
207
212
|
return make({
|
|
208
213
|
read: Effect.fnUntraced(function* (chatId: ChatId, options?: ReadOptions) {
|
|
209
214
|
const rows = yield* sql`
|
|
210
|
-
SELECT chat_id,
|
|
215
|
+
SELECT chat_id, sequence, at, event FROM chat_log WHERE chat_id = ${chatId} AND sequence > ${options?.after ?? 0} ORDER BY sequence
|
|
211
216
|
`
|
|
212
217
|
|
|
213
218
|
return yield* toEntries(rows)
|
|
214
219
|
}, Effect.orDie),
|
|
215
|
-
|
|
220
|
+
lastSequence: Effect.fnUntraced(function* (chatId: ChatId) {
|
|
216
221
|
const result = yield* sql`
|
|
217
|
-
SELECT MAX(
|
|
222
|
+
SELECT MAX(sequence) AS sequence FROM chat_log WHERE chat_id = ${chatId}
|
|
218
223
|
`
|
|
219
224
|
|
|
220
|
-
const rows = yield*
|
|
225
|
+
const rows = yield* decodeLastSequence(result)
|
|
221
226
|
|
|
222
|
-
return rows[0]?.
|
|
227
|
+
return rows[0]?.sequence ?? 0
|
|
223
228
|
}, Effect.orDie),
|
|
224
229
|
write: Effect.fnUntraced(function* (chatId: ChatId, entries: ReadonlyArray<AssistantEvent.Entry>) {
|
|
225
230
|
const rows = yield* Effect.forEach(
|
|
@@ -227,7 +232,7 @@ export const layerSql: Layer.Layer<ChatLog, never, SqlClient.SqlClient> = Layer.
|
|
|
227
232
|
Effect.fnUntraced(function* (entry: AssistantEvent.Entry) {
|
|
228
233
|
const event = yield* encodeEvent(entry.event)
|
|
229
234
|
|
|
230
|
-
return { chat_id: chatId,
|
|
235
|
+
return { chat_id: chatId, sequence: entry.sequence, at: entry.at, event }
|
|
231
236
|
})
|
|
232
237
|
)
|
|
233
238
|
|
|
@@ -235,9 +240,9 @@ export const layerSql: Layer.Layer<ChatLog, never, SqlClient.SqlClient> = Layer.
|
|
|
235
240
|
}, Effect.orDie),
|
|
236
241
|
lastEntries: Effect.gen(function* () {
|
|
237
242
|
const rows = yield* sql`
|
|
238
|
-
SELECT e.chat_id, e.
|
|
239
|
-
JOIN (SELECT chat_id, MAX(
|
|
240
|
-
ON last.chat_id = e.chat_id AND last.
|
|
243
|
+
SELECT e.chat_id, e.sequence, e.at, e.event FROM chat_log e
|
|
244
|
+
JOIN (SELECT chat_id, MAX(sequence) AS sequence FROM chat_log GROUP BY chat_id) last
|
|
245
|
+
ON last.chat_id = e.chat_id AND last.sequence = e.sequence
|
|
241
246
|
`
|
|
242
247
|
|
|
243
248
|
return yield* toEntries(rows)
|
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
|
*
|
|
@@ -186,7 +186,7 @@ function toolkitOf(skills: ReadonlyArray<Skill<unknown>>): Toolkit<unknown> {
|
|
|
186
186
|
* const skills = Skill.toolkit(allSkills.filter((skill) => allowed(role, skill)))
|
|
187
187
|
*
|
|
188
188
|
* return turn
|
|
189
|
-
* .
|
|
189
|
+
* .run({ instructions, toolkit: Toolkit.merge(MyTools, skills) })
|
|
190
190
|
* .pipe(Stream.provide(skills.layer))
|
|
191
191
|
* })
|
|
192
192
|
* )
|
package/src/Specialist.ts
CHANGED
|
@@ -40,12 +40,12 @@ export interface Specialist<Name extends string = string> {
|
|
|
40
40
|
/** The key of the specialist's handler, as `toLayer` provides it. */
|
|
41
41
|
readonly Handler: Context.Key<SpecialistHandler<Name>, Built>
|
|
42
42
|
/**
|
|
43
|
-
* Provides the specialist's handler. The handler may use the {@link
|
|
43
|
+
* Provides the specialist's handler. The handler may use the {@link TurnServices} and the
|
|
44
44
|
* services its build Effect required (see {@link CheckServices}).
|
|
45
45
|
*/
|
|
46
46
|
readonly toLayer: <R = never, RE = never, E = never, RX = never>(
|
|
47
47
|
build: (Handler<R, RE> | Effect.Effect<Handler<R, RE>, E, RX>) &
|
|
48
|
-
CheckServices<Exclude<R | RE,
|
|
48
|
+
CheckServices<Exclude<R | RE, TurnServices | RX | Tool.Handler<string>>>
|
|
49
49
|
) => Layer.Layer<SpecialistHandler<Name>, never, Exclude<RX, Scope.Scope | CurrentChat> | ToolHandlers<R | RE>>
|
|
50
50
|
}
|
|
51
51
|
|
|
@@ -108,21 +108,21 @@ export interface SpecialistHandler<Name extends string> {
|
|
|
108
108
|
export type HandlerOf<S> = S extends Specialist<infer Name> ? SpecialistHandler<Name> : never
|
|
109
109
|
|
|
110
110
|
/**
|
|
111
|
-
* The services a
|
|
111
|
+
* The services a turn provides to its handler, which are its chat, the chat's runner, and the
|
|
112
112
|
* model.
|
|
113
113
|
*
|
|
114
114
|
* @category utility types
|
|
115
115
|
* @since 0.1.0
|
|
116
116
|
*/
|
|
117
|
-
export type
|
|
117
|
+
export type TurnServices = CurrentChat | Runner | LanguageModel.LanguageModel
|
|
118
118
|
|
|
119
119
|
/**
|
|
120
|
-
* Represents the options for `run
|
|
120
|
+
* Represents the options for `turn.run`.
|
|
121
121
|
*
|
|
122
122
|
* @category models
|
|
123
123
|
* @since 0.1.0
|
|
124
124
|
*/
|
|
125
|
-
export interface
|
|
125
|
+
export interface RunOptions<T extends Record<string, Tool.Any> = Record<never, never>> {
|
|
126
126
|
readonly instructions: string | Instructions.Resolved
|
|
127
127
|
/**
|
|
128
128
|
* The loop's tools, as a native toolkit whose handlers come from the layer. Merge in a
|
|
@@ -134,12 +134,13 @@ export interface LoopOptions<T extends Record<string, Tool.Any> = Record<never,
|
|
|
134
134
|
}
|
|
135
135
|
|
|
136
136
|
/**
|
|
137
|
-
* Represents one
|
|
137
|
+
* Represents one turn of a specialist's run, as its handler sees it. The first turn works on the
|
|
138
|
+
* brief, and each later one on the messages sent to the run since (follow-ups and steering).
|
|
138
139
|
*
|
|
139
140
|
* @category models
|
|
140
141
|
* @since 0.1.0
|
|
141
142
|
*/
|
|
142
|
-
export interface
|
|
143
|
+
export interface Turn {
|
|
143
144
|
readonly runId: RunId
|
|
144
145
|
/** The chat the run belongs to, which is the chat of the turn that spawned it. */
|
|
145
146
|
readonly chatId: ChatId
|
|
@@ -150,8 +151,8 @@ export interface Run {
|
|
|
150
151
|
*/
|
|
151
152
|
readonly history: Effect.Effect<ReadonlyArray<Prompt.Message>>
|
|
152
153
|
/** Runs the agent loop over the run's history, with the given instructions and tools. */
|
|
153
|
-
readonly
|
|
154
|
-
options:
|
|
154
|
+
readonly run: <T extends Record<string, Tool.Any> = Record<never, never>>(
|
|
155
|
+
options: RunOptions<T>
|
|
155
156
|
) => Stream.Stream<
|
|
156
157
|
AssistantLoop.Event,
|
|
157
158
|
AiError.AiError,
|
|
@@ -170,7 +171,7 @@ export interface Run {
|
|
|
170
171
|
* @since 0.1.0
|
|
171
172
|
*/
|
|
172
173
|
export type Handler<R = never, RE = R> = (
|
|
173
|
-
|
|
174
|
+
turn: Turn
|
|
174
175
|
) =>
|
|
175
176
|
| Stream.Stream<AssistantLoop.Event, unknown, R>
|
|
176
177
|
| Effect.Effect<Stream.Stream<AssistantLoop.Event, unknown, R>, unknown, RE>
|
|
@@ -186,7 +187,7 @@ export type Handler<R = never, RE = R> = (
|
|
|
186
187
|
*/
|
|
187
188
|
export interface Built {
|
|
188
189
|
readonly open: Effect.Effect<
|
|
189
|
-
(
|
|
190
|
+
(turn: Turn) => Stream.Stream<AssistantLoop.Event, unknown, TurnServices>,
|
|
190
191
|
never,
|
|
191
192
|
Scope.Scope | CurrentChat
|
|
192
193
|
>
|
|
@@ -203,7 +204,7 @@ const toStream = <R, RE>(
|
|
|
203
204
|
*
|
|
204
205
|
* **Details**
|
|
205
206
|
*
|
|
206
|
-
* The specialist's `toLayer` provides its handler, which is usually `run
|
|
207
|
+
* The specialist's `toLayer` provides its handler, which is usually `turn.run` with the
|
|
207
208
|
* specialist's own instructions and tools. List the specialist in `Assistant.make`'s
|
|
208
209
|
* `specialists`, and provide its layer to the assistant's.
|
|
209
210
|
*
|
|
@@ -217,8 +218,8 @@ const toStream = <R, RE>(
|
|
|
217
218
|
* description: "Researches past campaigns and orders. Give it the question and any context."
|
|
218
219
|
* })
|
|
219
220
|
*
|
|
220
|
-
* const ResearchLive = Research.toLayer((
|
|
221
|
-
* run
|
|
221
|
+
* const ResearchLive = Research.toLayer((turn) =>
|
|
222
|
+
* turn.run({
|
|
222
223
|
* instructions: "Answer with the findings and numbers the colleague needs, in a few lines.",
|
|
223
224
|
* toolkit: ResearchTools
|
|
224
225
|
* })
|
|
@@ -226,7 +227,7 @@ const toStream = <R, RE>(
|
|
|
226
227
|
*
|
|
227
228
|
* export const MyAssistant = Assistant.make("my-assistant", { specialists: [Research] })
|
|
228
229
|
*
|
|
229
|
-
* const MyAssistantLive = MyAssistant.toLayer((turn) => turn.
|
|
230
|
+
* const MyAssistantLive = MyAssistant.toLayer((turn) => turn.run({ instructions })).pipe(
|
|
230
231
|
* Layer.provide(ResearchLive)
|
|
231
232
|
* )
|
|
232
233
|
* ```
|
|
@@ -248,7 +249,7 @@ export const make = <const Name extends string>(
|
|
|
248
249
|
// services.
|
|
249
250
|
function toLayer<R = never, RE = never, E = never, RX = never>(
|
|
250
251
|
build: (Handler<R, RE> | Effect.Effect<Handler<R, RE>, E, RX>) &
|
|
251
|
-
CheckServices<Exclude<R | RE,
|
|
252
|
+
CheckServices<Exclude<R | RE, TurnServices | RX | Tool.Handler<string>>>
|
|
252
253
|
): Layer.Layer<SpecialistHandler<Name>, never, Exclude<RX, Scope.Scope | CurrentChat> | ToolHandlers<R | RE>>
|
|
253
254
|
function toLayer(
|
|
254
255
|
build: Handler<any, any> | Effect.Effect<Handler<any, any>, unknown, any>
|
|
@@ -264,7 +265,7 @@ export const make = <const Name extends string>(
|
|
|
264
265
|
open: Effect.gen(function* () {
|
|
265
266
|
const handler = Effect.isEffect(build) ? yield* build : build
|
|
266
267
|
|
|
267
|
-
return (
|
|
268
|
+
return (turn: Turn) => Stream.provideContext(toStream(handler(turn)), context)
|
|
268
269
|
}).pipe(Effect.provideContext(context), Effect.orDie)
|
|
269
270
|
}
|
|
270
271
|
})
|
package/src/Timeline.ts
CHANGED
|
@@ -246,13 +246,13 @@ export type Activity = typeof Activity.Type
|
|
|
246
246
|
/**
|
|
247
247
|
* Represents a chat's timeline: the chat as its user sees it, typed with the app's events it shows.
|
|
248
248
|
* It holds the chat's items in order, what is working in it, whether the last turn failed, and
|
|
249
|
-
* 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.
|
|
250
250
|
*
|
|
251
251
|
* @category models
|
|
252
252
|
* @since 0.3.0
|
|
253
253
|
*/
|
|
254
254
|
export interface Timeline<Events = never> {
|
|
255
|
-
readonly
|
|
255
|
+
readonly sequence: number
|
|
256
256
|
readonly items: ReadonlyArray<Item<Events>>
|
|
257
257
|
readonly activity: Activity
|
|
258
258
|
/** Whether the last turn failed with no message after it. */
|
|
@@ -268,14 +268,14 @@ export interface Timeline<Events = never> {
|
|
|
268
268
|
*
|
|
269
269
|
* A change holds each item that is new or changed (for example, a message whose block was
|
|
270
270
|
* answered or closed, or a call the user approved), in full. It also holds the current activity, whether the last turn failed,
|
|
271
|
-
* 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
|
|
272
272
|
* to.
|
|
273
273
|
*
|
|
274
274
|
* @category models
|
|
275
275
|
* @since 0.3.0
|
|
276
276
|
*/
|
|
277
277
|
export interface Change<Events = never> {
|
|
278
|
-
readonly
|
|
278
|
+
readonly sequence: number
|
|
279
279
|
readonly items: ReadonlyArray<Item<Events>>
|
|
280
280
|
readonly activity: Activity
|
|
281
281
|
readonly failed: boolean
|
|
@@ -298,7 +298,7 @@ function makeChange(events: ReadonlyArray<AssistantEvent.AppEventSchema> = []):
|
|
|
298
298
|
const encodeItem = Schema.encodeUnknownSync(AnyItem)
|
|
299
299
|
|
|
300
300
|
return Schema.Struct({
|
|
301
|
-
|
|
301
|
+
sequence: Schema.Int,
|
|
302
302
|
// An item for an event this schema doesn't name, or that doesn't decode (from a newer server, say), is left out
|
|
303
303
|
// rather than failing the whole change.
|
|
304
304
|
items: Schema.Array(Schema.Json).pipe(
|
|
@@ -361,7 +361,7 @@ export const Change: <const Events extends ReadonlyArray<AssistantEvent.AppEvent
|
|
|
361
361
|
* @since 0.1.0
|
|
362
362
|
*/
|
|
363
363
|
export const empty: Timeline = {
|
|
364
|
-
|
|
364
|
+
sequence: 0,
|
|
365
365
|
items: [],
|
|
366
366
|
activity: { working: false, specialists: [] },
|
|
367
367
|
failed: false,
|
|
@@ -407,7 +407,7 @@ export const apply = <Events>(timeline: Timeline<Events>, change: Change<Events>
|
|
|
407
407
|
const known = new Set(timeline.items.map((item) => item.id))
|
|
408
408
|
|
|
409
409
|
return {
|
|
410
|
-
|
|
410
|
+
sequence: Math.max(timeline.sequence, change.sequence),
|
|
411
411
|
items: [
|
|
412
412
|
...timeline.items.map((item) => changed.get(item.id) ?? item),
|
|
413
413
|
...change.items.filter((item) => !known.has(item.id))
|
|
@@ -465,7 +465,7 @@ type AnyItem = UserMessage | AssistantMessage | AnyEventItem | ApprovalItem
|
|
|
465
465
|
|
|
466
466
|
// What the fold below builds: the overloads that use it type its items with the events shown.
|
|
467
467
|
interface AnyChange {
|
|
468
|
-
readonly
|
|
468
|
+
readonly sequence: number
|
|
469
469
|
readonly items: ReadonlyArray<AnyItem>
|
|
470
470
|
readonly activity: Activity
|
|
471
471
|
readonly failed: boolean
|
|
@@ -622,7 +622,7 @@ const sameActivity = (a: Activity, b: Activity) =>
|
|
|
622
622
|
const folding = (show: ReadonlyArray<AssistantEvent.AppEventSchema>): Folding => {
|
|
623
623
|
const shown = new Map(show.map((schema) => [AppEvents.tagOf(schema), schema]))
|
|
624
624
|
let approvals: ReadonlyMap<string, ToolApproval.State> = new Map()
|
|
625
|
-
let
|
|
625
|
+
let sequence = 0
|
|
626
626
|
let failed = false
|
|
627
627
|
let deleted = false
|
|
628
628
|
let activity = AssistantActivity.empty
|
|
@@ -729,7 +729,7 @@ const folding = (show: ReadonlyArray<AssistantEvent.AppEventSchema>): Folding =>
|
|
|
729
729
|
|
|
730
730
|
return Option.map(event, (decoded) => ({
|
|
731
731
|
_tag: decoded._tag,
|
|
732
|
-
id: `app-event:${entry.
|
|
732
|
+
id: `app-event:${entry.sequence}`,
|
|
733
733
|
at: entry.at,
|
|
734
734
|
event: decoded
|
|
735
735
|
}))
|
|
@@ -769,7 +769,7 @@ const folding = (show: ReadonlyArray<AssistantEvent.AppEventSchema>): Folding =>
|
|
|
769
769
|
const previous = { activity: activityOf(activity), failed, deleted }
|
|
770
770
|
const { event } = entry
|
|
771
771
|
|
|
772
|
-
|
|
772
|
+
sequence = entry.sequence
|
|
773
773
|
activity = AssistantActivity.apply(activity, entry)
|
|
774
774
|
|
|
775
775
|
if (!AssistantEvent.isLibraryEvent(event)) {
|
|
@@ -777,11 +777,11 @@ const folding = (show: ReadonlyArray<AssistantEvent.AppEventSchema>): Folding =>
|
|
|
777
777
|
|
|
778
778
|
return Option.match(eventItemOf(entry), {
|
|
779
779
|
onNone: () =>
|
|
780
|
-
sameActivity(previous.activity, now) ? undefined : {
|
|
780
|
+
sameActivity(previous.activity, now) ? undefined : { sequence, items: [], activity: now, failed, deleted },
|
|
781
781
|
onSome: (item) => {
|
|
782
782
|
items.set(item.id, item)
|
|
783
783
|
|
|
784
|
-
return {
|
|
784
|
+
return { sequence, items: [item], activity: now, failed, deleted }
|
|
785
785
|
}
|
|
786
786
|
})
|
|
787
787
|
}
|
|
@@ -811,13 +811,13 @@ const folding = (show: ReadonlyArray<AssistantEvent.AppEventSchema>): Folding =>
|
|
|
811
811
|
deleted === previous.deleted &&
|
|
812
812
|
sameActivity(previous.activity, now)
|
|
813
813
|
? undefined
|
|
814
|
-
: {
|
|
814
|
+
: { sequence, items: all, activity: now, failed, deleted }
|
|
815
815
|
}
|
|
816
816
|
|
|
817
817
|
return {
|
|
818
818
|
step,
|
|
819
819
|
get snapshot() {
|
|
820
|
-
return {
|
|
820
|
+
return { sequence, items: [...items.values()], activity: activityOf(activity), failed, deleted }
|
|
821
821
|
}
|
|
822
822
|
}
|
|
823
823
|
}
|
|
@@ -832,7 +832,7 @@ const merge = (changes: ReadonlyArray<AnyChange>): AnyChange | undefined => {
|
|
|
832
832
|
for (const change of changes) for (const item of change.items) items.set(item.id, item)
|
|
833
833
|
|
|
834
834
|
return {
|
|
835
|
-
|
|
835
|
+
sequence: last.sequence,
|
|
836
836
|
items: [...items.values()],
|
|
837
837
|
activity: last.activity,
|
|
838
838
|
failed: last.failed,
|
|
@@ -864,7 +864,7 @@ export interface ChangesOptions<
|
|
|
864
864
|
Show extends ReadonlyArray<AssistantEvent.AppEventSchema> = readonly []
|
|
865
865
|
> extends ShowOptions<Show> {
|
|
866
866
|
/**
|
|
867
|
-
* The `
|
|
867
|
+
* The `sequence` up to which the client already has the timeline. The result contains only the
|
|
868
868
|
* changes after it. Defaults to `0`, every change.
|
|
869
869
|
*/
|
|
870
870
|
readonly after?: number | undefined
|
|
@@ -889,7 +889,7 @@ function changesWith<E, R>(
|
|
|
889
889
|
const changed = chunk.flatMap((entry) => {
|
|
890
890
|
const change = timeline.step(entry)
|
|
891
891
|
|
|
892
|
-
return change === undefined || entry.
|
|
892
|
+
return change === undefined || entry.sequence <= after ? [] : [change]
|
|
893
893
|
})
|
|
894
894
|
|
|
895
895
|
const merged = merge(changed)
|
|
@@ -907,9 +907,9 @@ function changesWith<E, R>(
|
|
|
907
907
|
*
|
|
908
908
|
* The entries must stream from the start of the chat's log, as `assistant.subscribe(chatId)` does
|
|
909
909
|
* without `after`. The fold reads every entry, because a message depends on earlier entries: its
|
|
910
|
-
* block is answered or closed later. `after` only skips emitting the changes up to that `
|
|
911
|
-
* client that has the timeline up to a `
|
|
912
|
-
* 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,
|
|
913
913
|
* and the changes it produces are wrong.
|
|
914
914
|
*
|
|
915
915
|
* Each chunk of entries becomes one change; the stored entries, for example, arrive as one chunk.
|
|
@@ -928,7 +928,7 @@ function changesWith<E, R>(
|
|
|
928
928
|
* import * as Schema from "effect/Schema"
|
|
929
929
|
* import * as Stream from "effect/Stream"
|
|
930
930
|
*
|
|
931
|
-
* class Summary extends Schema.TaggedClass<Summary>()("Summary", { text: Schema.String,
|
|
931
|
+
* class Summary extends Schema.TaggedClass<Summary>()("Summary", { text: Schema.String, throughSequence: Schema.Int }) {}
|
|
932
932
|
* class DailyBudgetReached extends Schema.TaggedClass<DailyBudgetReached>()("DailyBudgetReached", {}) {}
|
|
933
933
|
*
|
|
934
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
|
* })
|