effect-assistant 0.5.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 +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 +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 +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/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 +1 -1
- package/src/Specialist.ts +1 -1
- 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/turns.ts +28 -26
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
|
* })
|
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
|
),
|
package/src/internal/events.ts
CHANGED
package/src/internal/history.ts
CHANGED
|
@@ -140,7 +140,7 @@ const awaitingMessage = (request: AssistantEvent.ApprovalRequested): Prompt.Tool
|
|
|
140
140
|
})
|
|
141
141
|
|
|
142
142
|
const isEntry = (item: AssistantEvent.AnyEntry | Prompt.Message): item is AssistantEvent.AnyEntry =>
|
|
143
|
-
Predicate.hasProperty(item, "
|
|
143
|
+
Predicate.hasProperty(item, "sequence")
|
|
144
144
|
|
|
145
145
|
// In the order the model saw the entries, not the order they were stored. A message a history view put in stands
|
|
146
146
|
// where it is, right after the entry before it, and waits like an input for a call's result or for the model's next
|
|
@@ -166,15 +166,15 @@ export const fromEntries = (
|
|
|
166
166
|
const next = entries.slice(from + 1).find(({ event }) => Predicate.isTagged(event, "ModelOutput"))?.event
|
|
167
167
|
|
|
168
168
|
return next !== undefined && AssistantEvent.isLibraryEvent(next) && Predicate.isTagged(next, "ModelOutput")
|
|
169
|
-
? next.
|
|
169
|
+
? next.seenThroughSequence
|
|
170
170
|
: undefined
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
-
// `index` is the last entry at or before the item, and `
|
|
174
|
-
const input = (index: number,
|
|
173
|
+
// `index` is the last entry at or before the item, and `sequence` where the item stands.
|
|
174
|
+
const input = (index: number, sequence: number, item: Prompt.Message) => {
|
|
175
175
|
const seen = nextSeen(index)
|
|
176
176
|
|
|
177
|
-
if (open.size > 0 || (seen !== undefined && seen <
|
|
177
|
+
if (open.size > 0 || (seen !== undefined && seen < sequence)) held.push(item)
|
|
178
178
|
else history.push(item)
|
|
179
179
|
}
|
|
180
180
|
|
|
@@ -182,14 +182,14 @@ export const fromEntries = (
|
|
|
182
182
|
|
|
183
183
|
for (const item of items) {
|
|
184
184
|
if (!isEntry(item)) {
|
|
185
|
-
input(index, (entries[index]?.
|
|
185
|
+
input(index, (entries[index]?.sequence ?? 0) + 0.5, item)
|
|
186
186
|
|
|
187
187
|
continue
|
|
188
188
|
}
|
|
189
189
|
|
|
190
190
|
index += 1
|
|
191
191
|
|
|
192
|
-
const { event,
|
|
192
|
+
const { event, sequence } = item
|
|
193
193
|
const at = index
|
|
194
194
|
|
|
195
195
|
if (!AssistantEvent.isLibraryEvent(event)) continue
|
|
@@ -197,8 +197,8 @@ export const fromEntries = (
|
|
|
197
197
|
Match.value(event).pipe(
|
|
198
198
|
Match.tagsExhaustive({
|
|
199
199
|
UserMessage: (message) =>
|
|
200
|
-
input(at,
|
|
201
|
-
BlockAnswer: (answer) => input(at,
|
|
200
|
+
input(at, sequence, userText(AssistantEvent.textWithAttachments(message.text, message.attachments))),
|
|
201
|
+
BlockAnswer: (answer) => input(at, sequence, userText(answerText(answer))),
|
|
202
202
|
ModelOutput: (output) => {
|
|
203
203
|
history.push(...Prompt.fromResponseParts(output.parts).content)
|
|
204
204
|
|
|
@@ -212,11 +212,11 @@ export const fromEntries = (
|
|
|
212
212
|
flushIfClosed()
|
|
213
213
|
},
|
|
214
214
|
ToolResult: (result) => {
|
|
215
|
-
if (requests.has(result.callId)) return input(at,
|
|
215
|
+
if (requests.has(result.callId)) return input(at, sequence, userText(approvedText(result)))
|
|
216
216
|
|
|
217
217
|
// A view that starts after the call (since a summary, say) keeps its result as text: a tool result needs its
|
|
218
218
|
// call right before it.
|
|
219
|
-
if (!called.has(result.callId)) return input(at,
|
|
219
|
+
if (!called.has(result.callId)) return input(at, sequence, userText(resultText(result)))
|
|
220
220
|
|
|
221
221
|
history.push(toolMessage(result))
|
|
222
222
|
open.delete(result.callId)
|
|
@@ -227,7 +227,7 @@ export const fromEntries = (
|
|
|
227
227
|
TurnStarted: () => undefined,
|
|
228
228
|
TurnCompleted: () => undefined,
|
|
229
229
|
TurnFailed: () => undefined,
|
|
230
|
-
SpecialistFinished: (finished) => input(at,
|
|
230
|
+
SpecialistFinished: (finished) => input(at, sequence, userText(finishedText(entries, at, finished))),
|
|
231
231
|
// Every run starts in a call of the model's own (a spawn or a follow-up), whose result says so.
|
|
232
232
|
SpecialistStarted: () => undefined,
|
|
233
233
|
BlocksClosed: () => undefined,
|
|
@@ -235,7 +235,7 @@ export const fromEntries = (
|
|
|
235
235
|
ApprovalRequested: (request) => {
|
|
236
236
|
requests.set(request.callId, request)
|
|
237
237
|
|
|
238
|
-
if (!called.has(request.callId)) return input(at,
|
|
238
|
+
if (!called.has(request.callId)) return input(at, sequence, userText(awaitingText(request)))
|
|
239
239
|
|
|
240
240
|
history.push(awaitingMessage(request))
|
|
241
241
|
open.delete(request.callId)
|
|
@@ -244,10 +244,10 @@ export const fromEntries = (
|
|
|
244
244
|
// An approved call's result says so.
|
|
245
245
|
ApprovalDecision: (decision) =>
|
|
246
246
|
decision.decision === "rejected"
|
|
247
|
-
? input(at,
|
|
247
|
+
? input(at, sequence, userText(rejectedText(decision, toolOf(decision.callId))))
|
|
248
248
|
: undefined,
|
|
249
249
|
ApprovalsExpired: (expired) =>
|
|
250
|
-
expired.callIds.forEach((callId) => input(at,
|
|
250
|
+
expired.callIds.forEach((callId) => input(at, sequence, userText(expiredText(callId, toolOf(callId))))),
|
|
251
251
|
ChatDeleted: () => undefined
|
|
252
252
|
})
|
|
253
253
|
)
|
|
@@ -264,7 +264,7 @@ export const isInput = (event: { readonly _tag: string }): boolean =>
|
|
|
264
264
|
export const handledThrough = (entries: ReadonlyArray<AssistantEvent.AnyEntry>): number => {
|
|
265
265
|
const last = entries.findLast(({ event }) => AssistantEvent.isTurnEnd(event))?.event
|
|
266
266
|
|
|
267
|
-
return last !== undefined && AssistantEvent.isTurnEnd(last) ? last.
|
|
267
|
+
return last !== undefined && AssistantEvent.isTurnEnd(last) ? last.throughSequence : 0
|
|
268
268
|
}
|
|
269
269
|
|
|
270
270
|
// A run the assistant stopped isn't an input here: it wakes nothing.
|
|
@@ -274,8 +274,10 @@ export const unhandledInputs = (
|
|
|
274
274
|
): ReadonlyArray<{ readonly _tag: string }> => {
|
|
275
275
|
const seen = handledThrough(entries)
|
|
276
276
|
|
|
277
|
-
return entries.flatMap(({ event,
|
|
278
|
-
|
|
277
|
+
return entries.flatMap(({ event, sequence }) =>
|
|
278
|
+
sequence > seen && isInput(event) && !(AssistantEvent.isInput(event) && !AssistantEvent.isWaking(event))
|
|
279
|
+
? [event]
|
|
280
|
+
: []
|
|
279
281
|
)
|
|
280
282
|
}
|
|
281
283
|
|
|
@@ -285,16 +287,17 @@ export const needsTurn = (entries: ReadonlyArray<AssistantEvent.AnyEntry>): bool
|
|
|
285
287
|
|
|
286
288
|
// A failure the user was shown counts as a reply, and so does a call they're asked to approve.
|
|
287
289
|
/** @internal */
|
|
288
|
-
export const owesReply = (entries: ReadonlyArray<AssistantEvent.AnyEntry>,
|
|
290
|
+
export const owesReply = (entries: ReadonlyArray<AssistantEvent.AnyEntry>, throughSequence: number): boolean => {
|
|
289
291
|
const latest = entries.findLast(
|
|
290
|
-
({ event,
|
|
292
|
+
({ event, sequence }) =>
|
|
293
|
+
sequence <= throughSequence && AssistantEvent.isInput(event) && AssistantEvent.isUserInput(event)
|
|
291
294
|
)
|
|
292
295
|
|
|
293
296
|
return (
|
|
294
297
|
latest !== undefined &&
|
|
295
298
|
!entries.some(
|
|
296
|
-
({ event,
|
|
297
|
-
|
|
299
|
+
({ event, sequence }) =>
|
|
300
|
+
sequence > latest.sequence &&
|
|
298
301
|
(Predicate.isTagged(event, "AssistantMessage") ||
|
|
299
302
|
Predicate.isTagged(event, "TurnFailed") ||
|
|
300
303
|
Predicate.isTagged(event, "ApprovalRequested"))
|
|
@@ -321,15 +324,16 @@ const progressMessages = (entries: ReadonlyArray<AssistantEvent.AnyEntry>): Read
|
|
|
321
324
|
// The model said it was on it and stopped without the result: only progress messages since the user's latest input.
|
|
322
325
|
// A specialist still working doesn't count, since its report is what comes next.
|
|
323
326
|
/** @internal */
|
|
324
|
-
export const owesDelivery = (entries: ReadonlyArray<AssistantEvent.AnyEntry>,
|
|
327
|
+
export const owesDelivery = (entries: ReadonlyArray<AssistantEvent.AnyEntry>, throughSequence: number): boolean => {
|
|
325
328
|
const latest = entries.findLast(
|
|
326
|
-
({ event,
|
|
329
|
+
({ event, sequence }) =>
|
|
330
|
+
sequence <= throughSequence && AssistantEvent.isInput(event) && AssistantEvent.isUserInput(event)
|
|
327
331
|
)
|
|
328
332
|
|
|
329
333
|
if (latest === undefined) return false
|
|
330
334
|
|
|
331
|
-
const after = entries.flatMap(({
|
|
332
|
-
|
|
335
|
+
const after = entries.flatMap(({ sequence, event }) =>
|
|
336
|
+
sequence > latest.sequence && AssistantEvent.isLibraryEvent(event) ? [event] : []
|
|
333
337
|
)
|
|
334
338
|
|
|
335
339
|
const messages = after.flatMap((event) => (Predicate.isTagged(event, "AssistantMessage") ? [event] : []))
|
|
@@ -352,5 +356,5 @@ export const owesDelivery = (entries: ReadonlyArray<AssistantEvent.AnyEntry>, th
|
|
|
352
356
|
}
|
|
353
357
|
|
|
354
358
|
/** @internal */
|
|
355
|
-
export const unreadAfter = (entries: ReadonlyArray<AssistantEvent.AnyEntry>,
|
|
356
|
-
entries.some(({ event,
|
|
359
|
+
export const unreadAfter = (entries: ReadonlyArray<AssistantEvent.AnyEntry>, throughSequence: number): boolean =>
|
|
360
|
+
entries.some(({ event, sequence }) => sequence > throughSequence && isInput(event))
|
package/src/internal/loop.ts
CHANGED
|
@@ -44,13 +44,13 @@ export const TurnTools = Context.Reference<Option.Option<Toolkits.Tools>>("effec
|
|
|
44
44
|
/** @internal */
|
|
45
45
|
export interface ChatHistory {
|
|
46
46
|
readonly messages: ReadonlyArray<Prompt.Message>
|
|
47
|
-
readonly
|
|
47
|
+
readonly throughSequence: number
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
const readHistory = (read: ReadonlyArray<Prompt.Message> | ChatHistory) =>
|
|
51
51
|
"messages" in read
|
|
52
|
-
? { messages: read.messages,
|
|
53
|
-
: { messages: read,
|
|
52
|
+
? { messages: read.messages, throughSequence: read.throughSequence }
|
|
53
|
+
: { messages: read, throughSequence: undefined }
|
|
54
54
|
|
|
55
55
|
// `AssistantLoop` takes native toolkits; turns and specialist runs come here with their tools already merged.
|
|
56
56
|
/** @internal */
|
|
@@ -281,7 +281,7 @@ export const step = <R, S, E2 = never, R2 = never>(
|
|
|
281
281
|
|
|
282
282
|
return Stream.unwrap(
|
|
283
283
|
Effect.gen(function* () {
|
|
284
|
-
const { messages: history,
|
|
284
|
+
const { messages: history, throughSequence } = readHistory(yield* options.history)
|
|
285
285
|
// Calls a crash cut off, with no result: they run again, with the same ids, before the model goes on.
|
|
286
286
|
const cutOff = History.unansweredCalls(history)
|
|
287
287
|
const approvals = yield* TurnApprovals
|
|
@@ -318,7 +318,7 @@ export const step = <R, S, E2 = never, R2 = never>(
|
|
|
318
318
|
|
|
319
319
|
const output = AssistantEvent.ModelOutput.make({
|
|
320
320
|
parts: modelParts(response.content),
|
|
321
|
-
|
|
321
|
+
seenThroughSequence: throughSequence,
|
|
322
322
|
model: metadata?.modelId ?? Option.getOrUndefined(name),
|
|
323
323
|
responseId: metadata?.id,
|
|
324
324
|
usage: response.usage,
|
package/src/internal/turns.ts
CHANGED
|
@@ -140,12 +140,13 @@ const approvedCalls = (entries: ReadonlyArray<AssistantEvent.AnyEntry>): Readonl
|
|
|
140
140
|
: []
|
|
141
141
|
)
|
|
142
142
|
|
|
143
|
-
const
|
|
143
|
+
const lastSequence = (entries: ReadonlyArray<AssistantEvent.AnyEntry>, otherwise: number) =>
|
|
144
|
+
entries.at(-1)?.sequence ?? otherwise
|
|
144
145
|
|
|
145
146
|
// The handler's stream can end between a step's output and its tools, or while they run. An interrupted turn (a
|
|
146
147
|
// shutdown) leaves its calls open instead: the next turn runs them again.
|
|
147
|
-
const closeOpenCalls = Effect.fnUntraced(function* (chat: Writer,
|
|
148
|
-
const entries = (yield* chat.entries).filter(({
|
|
148
|
+
const closeOpenCalls = Effect.fnUntraced(function* (chat: Writer, startSequence: number) {
|
|
149
|
+
const entries = (yield* chat.entries).filter(({ sequence }) => sequence > startSequence)
|
|
149
150
|
|
|
150
151
|
const started = new Set(
|
|
151
152
|
entries.flatMap(({ event }) => (Predicate.isTagged(event, "ToolStarted") ? [event.callId] : []))
|
|
@@ -169,18 +170,18 @@ const closeOpenCalls = Effect.fnUntraced(function* (chat: Writer, startSeq: numb
|
|
|
169
170
|
)
|
|
170
171
|
})
|
|
171
172
|
|
|
172
|
-
// The inputs before the turn's `TurnStarted` (at `
|
|
173
|
+
// The inputs before the turn's `TurnStarted` (at `startSequence`) woke it; past that, what its model outputs record
|
|
173
174
|
// reading.
|
|
174
|
-
const handledThrough = (entries: ReadonlyArray<AssistantEvent.AnyEntry>,
|
|
175
|
+
const handledThrough = (entries: ReadonlyArray<AssistantEvent.AnyEntry>, startSequence: number) =>
|
|
175
176
|
entries.reduce(
|
|
176
|
-
(through, {
|
|
177
|
-
|
|
177
|
+
(through, { sequence, event }) =>
|
|
178
|
+
sequence > startSequence &&
|
|
178
179
|
AssistantEvent.isLibraryEvent(event) &&
|
|
179
180
|
Predicate.isTagged(event, "ModelOutput") &&
|
|
180
|
-
event.
|
|
181
|
-
? Math.max(through, event.
|
|
181
|
+
event.seenThroughSequence !== undefined
|
|
182
|
+
? Math.max(through, event.seenThroughSequence)
|
|
182
183
|
: through,
|
|
183
|
-
|
|
184
|
+
startSequence
|
|
184
185
|
)
|
|
185
186
|
|
|
186
187
|
// A turn that ran the loop fails visibly when the user's latest input got no reply; an input that arrived after what
|
|
@@ -190,8 +191,8 @@ const finishTurn = Effect.fnUntraced(function* (chat: Writer, read: number, ranL
|
|
|
190
191
|
|
|
191
192
|
const end =
|
|
192
193
|
ranLoop && History.owesReply(entries, read) && !History.unreadAfter(entries, read)
|
|
193
|
-
? AssistantEvent.TurnFailed.make({
|
|
194
|
-
: AssistantEvent.TurnCompleted.make({
|
|
194
|
+
? AssistantEvent.TurnFailed.make({ throughSequence: read, message: silentTurnMessage })
|
|
195
|
+
: AssistantEvent.TurnCompleted.make({ throughSequence: read })
|
|
195
196
|
|
|
196
197
|
yield* chat.append([end])
|
|
197
198
|
})
|
|
@@ -200,15 +201,15 @@ const finishTurn = Effect.fnUntraced(function* (chat: Writer, read: number, ranL
|
|
|
200
201
|
const failTurn = Effect.fnUntraced(function* (chat: Writer, cause: Cause.Cause<unknown>) {
|
|
201
202
|
yield* Effect.logError("assistant turn failed", cause)
|
|
202
203
|
|
|
203
|
-
const last =
|
|
204
|
+
const last = lastSequence(yield* chat.entries, 0)
|
|
204
205
|
|
|
205
|
-
yield* chat.append([AssistantEvent.TurnFailed.make({
|
|
206
|
+
yield* chat.append([AssistantEvent.TurnFailed.make({ throughSequence: last, message: failureMessage(cause) })])
|
|
206
207
|
})
|
|
207
208
|
|
|
208
209
|
const makeTurnApi = (
|
|
209
210
|
registration: Registration,
|
|
210
211
|
chat: Writer,
|
|
211
|
-
|
|
212
|
+
startSequence: number,
|
|
212
213
|
checkOpen: Effect.Effect<void>,
|
|
213
214
|
ranLoop: Ref.Ref<boolean>
|
|
214
215
|
): Assistant.Turn<any> => {
|
|
@@ -228,7 +229,7 @@ const makeTurnApi = (
|
|
|
228
229
|
|
|
229
230
|
const read: AssistantLoop.ChatHistory = {
|
|
230
231
|
messages: History.fromEntries(view(AppEvents.decodeEntries(app, all))),
|
|
231
|
-
|
|
232
|
+
throughSequence: lastSequence(all, startSequence)
|
|
232
233
|
}
|
|
233
234
|
|
|
234
235
|
return read
|
|
@@ -247,7 +248,7 @@ const makeTurnApi = (
|
|
|
247
248
|
Stream.unwrap(
|
|
248
249
|
Effect.gen(function* () {
|
|
249
250
|
const all = yield* chat.entries
|
|
250
|
-
const through = handledThrough(all,
|
|
251
|
+
const through = handledThrough(all, startSequence)
|
|
251
252
|
|
|
252
253
|
// Something arrived after what the loop's model read: the next turn reads it with the full loop, and replies.
|
|
253
254
|
if (History.unreadAfter(all, through)) return Stream.empty
|
|
@@ -277,7 +278,7 @@ const makeTurnApi = (
|
|
|
277
278
|
...History.fromEntries(options.view(AppEvents.decodeEntries(app, entries))),
|
|
278
279
|
History.userText(nudge)
|
|
279
280
|
],
|
|
280
|
-
|
|
281
|
+
throughSequence: lastSequence(entries, startSequence)
|
|
281
282
|
}
|
|
282
283
|
}),
|
|
283
284
|
// A second step, for a reply after a call it was refused.
|
|
@@ -300,7 +301,7 @@ const makeTurnApi = (
|
|
|
300
301
|
})
|
|
301
302
|
)
|
|
302
303
|
|
|
303
|
-
const prepare = <T extends Record<string, Tool.Any>>(options: Assistant.
|
|
304
|
+
const prepare = <T extends Record<string, Tool.Any>>(options: Assistant.RunOptions<T, any>) => {
|
|
304
305
|
const own = Toolkits.showing(Toolkits.fromToolkitOrEmpty(options.toolkit), shownTools)
|
|
305
306
|
|
|
306
307
|
return {
|
|
@@ -315,7 +316,7 @@ const makeTurnApi = (
|
|
|
315
316
|
history,
|
|
316
317
|
instructions,
|
|
317
318
|
replyIfOwed,
|
|
318
|
-
|
|
319
|
+
run: (options) => {
|
|
319
320
|
const { system, toolkit, view } = prepare(options)
|
|
320
321
|
const twice = Toolkits.duplicateNames(toolkit)
|
|
321
322
|
|
|
@@ -362,15 +363,15 @@ export const run = Effect.fnUntraced(
|
|
|
362
363
|
)
|
|
363
364
|
])
|
|
364
365
|
|
|
365
|
-
const
|
|
366
|
+
const startSequence = lastSequence(started, 0)
|
|
366
367
|
const ranLoop = yield* Ref.make(false)
|
|
367
|
-
const turn = makeTurnApi(options.registration, chat,
|
|
368
|
+
const turn = makeTurnApi(options.registration, chat, startSequence, options.checkOpen, ranLoop)
|
|
368
369
|
|
|
369
370
|
const record = (event: AssistantLoop.Event) => chat.append([event])
|
|
370
371
|
|
|
371
372
|
yield* options.handler(turn).pipe(
|
|
372
373
|
Stream.runForEach(record),
|
|
373
|
-
Effect.onExit((exit) => (Exit.hasInterrupts(exit) ? Effect.void : closeOpenCalls(chat,
|
|
374
|
+
Effect.onExit((exit) => (Exit.hasInterrupts(exit) ? Effect.void : closeOpenCalls(chat, startSequence))),
|
|
374
375
|
// A loop the app writes gets the assistant's own tools too, with this turn's services.
|
|
375
376
|
Effect.provideService(
|
|
376
377
|
Loop.TurnTools,
|
|
@@ -383,15 +384,16 @@ export const run = Effect.fnUntraced(
|
|
|
383
384
|
onSuccess: Effect.fnUntraced(function* () {
|
|
384
385
|
const all = yield* chat.entries
|
|
385
386
|
|
|
386
|
-
yield* finishTurn(chat, handledThrough(all,
|
|
387
|
+
yield* finishTurn(chat, handledThrough(all, startSequence), yield* Ref.get(ranLoop))
|
|
387
388
|
}),
|
|
388
389
|
onFailure: (cause) => failTurn(chat, cause)
|
|
389
390
|
})
|
|
390
391
|
)
|
|
391
392
|
|
|
392
393
|
// The trace's output is what the user saw this turn, and a failure if any.
|
|
393
|
-
const shown = (yield* chat.entries).flatMap(({ event,
|
|
394
|
-
|
|
394
|
+
const shown = (yield* chat.entries).flatMap(({ event, sequence }) =>
|
|
395
|
+
sequence > startSequence &&
|
|
396
|
+
(Predicate.isTagged(event, "AssistantMessage") || Predicate.isTagged(event, "TurnFailed"))
|
|
395
397
|
? [event]
|
|
396
398
|
: []
|
|
397
399
|
)
|