effect-assistant 0.2.0 → 0.3.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 +74 -28
- package/dist/Assistant.d.ts.map +1 -1
- package/dist/Assistant.js +11 -4
- package/dist/Assistant.js.map +1 -1
- package/dist/AssistantActivity.d.ts +10 -16
- package/dist/AssistantActivity.d.ts.map +1 -1
- package/dist/AssistantActivity.js +11 -4
- package/dist/AssistantActivity.js.map +1 -1
- package/dist/AssistantEngine.d.ts +39 -1
- package/dist/AssistantEngine.d.ts.map +1 -1
- package/dist/AssistantEngine.js +81 -28
- package/dist/AssistantEngine.js.map +1 -1
- package/dist/AssistantEvent.d.ts +50 -5
- package/dist/AssistantEvent.d.ts.map +1 -1
- package/dist/AssistantEvent.js +28 -7
- package/dist/AssistantEvent.js.map +1 -1
- package/dist/BlockKit.d.ts +4 -4
- package/dist/BlockKit.js +1 -1
- package/dist/BlockKit.js.map +1 -1
- package/dist/ChatLog.js +1 -1
- package/dist/Compaction.d.ts +6 -2
- package/dist/Compaction.d.ts.map +1 -1
- package/dist/Compaction.js +6 -2
- package/dist/Compaction.js.map +1 -1
- package/dist/CurrentChat.d.ts +2 -1
- package/dist/CurrentChat.d.ts.map +1 -1
- package/dist/CurrentChat.js.map +1 -1
- package/dist/Instructions.js +1 -1
- package/dist/Skill.d.ts +33 -0
- package/dist/Skill.d.ts.map +1 -1
- package/dist/Skill.js +27 -1
- package/dist/Skill.js.map +1 -1
- package/dist/Specialist.d.ts +4 -1
- package/dist/Specialist.d.ts.map +1 -1
- package/dist/Specialist.js.map +1 -1
- package/dist/Timeline.d.ts +482 -0
- package/dist/Timeline.d.ts.map +1 -0
- package/dist/Timeline.js +572 -0
- package/dist/Timeline.js.map +1 -0
- package/dist/index.d.ts +2 -3
- package/dist/index.js +2 -3
- package/dist/internal/chat.js +5 -6
- package/dist/internal/chat.js.map +1 -1
- package/dist/internal/events.d.ts +15 -16
- package/dist/internal/events.d.ts.map +1 -1
- package/dist/internal/events.js +29 -28
- package/dist/internal/events.js.map +1 -1
- package/dist/internal/history.js +25 -19
- package/dist/internal/history.js.map +1 -1
- package/dist/internal/input.js +7 -0
- package/dist/internal/input.js.map +1 -0
- package/dist/internal/loop.js +1 -1
- package/dist/internal/skills.js +6 -1
- package/dist/internal/skills.js.map +1 -1
- package/dist/internal/specialists.js +5 -4
- package/dist/internal/specialists.js.map +1 -1
- package/dist/internal/tools.js +1 -1
- package/dist/internal/turns.d.ts.map +1 -1
- package/dist/internal/turns.js +23 -35
- package/dist/internal/turns.js.map +1 -1
- package/package.json +1 -1
- package/src/Assistant.ts +75 -29
- package/src/AssistantActivity.ts +11 -18
- package/src/AssistantEngine.ts +110 -43
- package/src/AssistantEvent.ts +46 -8
- package/src/BlockKit.ts +6 -6
- package/src/Compaction.ts +6 -2
- package/src/CurrentChat.ts +2 -1
- package/src/Skill.ts +34 -1
- package/src/Specialist.ts +4 -1
- package/src/Timeline.ts +861 -0
- package/src/index.ts +5 -10
- package/src/internal/chat.ts +4 -4
- package/src/internal/events.ts +43 -43
- package/src/internal/history.ts +36 -29
- package/src/internal/input.ts +3 -0
- package/src/internal/skills.ts +11 -0
- package/src/internal/specialists.ts +3 -2
- package/src/internal/turns.ts +30 -53
- package/dist/AppEvent.d.ts +0 -111
- package/dist/AppEvent.d.ts.map +0 -1
- package/dist/AppEvent.js +0 -71
- package/dist/AppEvent.js.map +0 -1
- package/dist/ChatView.d.ts +0 -408
- package/dist/ChatView.d.ts.map +0 -1
- package/dist/ChatView.js +0 -443
- package/dist/ChatView.js.map +0 -1
- package/src/AppEvent.ts +0 -134
- package/src/ChatView.ts +0 -587
package/src/index.ts
CHANGED
|
@@ -4,11 +4,6 @@
|
|
|
4
4
|
* @since 0.1.0
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
/**
|
|
8
|
-
* @since 0.1.0
|
|
9
|
-
*/
|
|
10
|
-
export * as AppEvent from "./AppEvent.ts"
|
|
11
|
-
|
|
12
7
|
/**
|
|
13
8
|
* @since 0.1.0
|
|
14
9
|
*/
|
|
@@ -64,11 +59,6 @@ export * as ChatLog from "./ChatLog.ts"
|
|
|
64
59
|
*/
|
|
65
60
|
export * as Chats from "./Chats.ts"
|
|
66
61
|
|
|
67
|
-
/**
|
|
68
|
-
* @since 0.1.0
|
|
69
|
-
*/
|
|
70
|
-
export * as ChatView from "./ChatView.ts"
|
|
71
|
-
|
|
72
62
|
/**
|
|
73
63
|
* @since 0.1.0
|
|
74
64
|
*/
|
|
@@ -109,6 +99,11 @@ export * as Skill from "./Skill.ts"
|
|
|
109
99
|
*/
|
|
110
100
|
export * as Specialist from "./Specialist.ts"
|
|
111
101
|
|
|
102
|
+
/**
|
|
103
|
+
* @since 0.3.0
|
|
104
|
+
*/
|
|
105
|
+
export * as Timeline from "./Timeline.ts"
|
|
106
|
+
|
|
112
107
|
/**
|
|
113
108
|
* @since 0.1.0
|
|
114
109
|
*/
|
package/src/internal/chat.ts
CHANGED
|
@@ -101,7 +101,6 @@ export const make = Effect.fnUntraced(function* (options: {
|
|
|
101
101
|
}) {
|
|
102
102
|
const { registration, chatId } = options
|
|
103
103
|
const { app } = registration.definition
|
|
104
|
-
const { wakes } = app
|
|
105
104
|
const storage = yield* ChatLog
|
|
106
105
|
const chats = yield* Chats.Chats
|
|
107
106
|
const initial = yield* storage.read(chatId)
|
|
@@ -201,7 +200,8 @@ export const make = Effect.fnUntraced(function* (options: {
|
|
|
201
200
|
|
|
202
201
|
if (id !== undefined && (yield* Ref.get(entries)).some(({ event }) => idOf(event) === id)) return
|
|
203
202
|
|
|
204
|
-
|
|
203
|
+
// Stored as an input: it came in from outside a turn, so it wakes one, also after a restart.
|
|
204
|
+
yield* write([AssistantEvent.isLibraryEvent(input) ? input : AppEvents.markInput(input)])
|
|
205
205
|
})
|
|
206
206
|
)
|
|
207
207
|
})
|
|
@@ -232,7 +232,7 @@ export const make = Effect.fnUntraced(function* (options: {
|
|
|
232
232
|
|
|
233
233
|
const drain: Effect.Effect<void> = Semaphore.withPermit(turnsLock)(
|
|
234
234
|
Effect.gen(function* () {
|
|
235
|
-
while (!(yield* isDeleted) && History.needsTurn(yield* chat.entries
|
|
235
|
+
while (!(yield* isDeleted) && History.needsTurn(yield* chat.entries)) {
|
|
236
236
|
yield* Turns.run({ registration, handler, chat, services, checkOpen: Effect.asVoid(checkAvailable) })
|
|
237
237
|
}
|
|
238
238
|
})
|
|
@@ -296,7 +296,7 @@ export const make = Effect.fnUntraced(function* (options: {
|
|
|
296
296
|
yield* inBackground(drain)
|
|
297
297
|
}),
|
|
298
298
|
resume: Effect.gen(function* () {
|
|
299
|
-
if (History.needsTurn(yield* chat.entries
|
|
299
|
+
if (History.needsTurn(yield* chat.entries)) yield* inBackground(drain)
|
|
300
300
|
}),
|
|
301
301
|
stream: (after) =>
|
|
302
302
|
Stream.unwrap(
|
package/src/internal/events.ts
CHANGED
|
@@ -1,27 +1,30 @@
|
|
|
1
1
|
import * as Effect from "effect/Effect"
|
|
2
2
|
import * as Option from "effect/Option"
|
|
3
|
+
import * as Predicate from "effect/Predicate"
|
|
4
|
+
import * as Record from "effect/Record"
|
|
3
5
|
import * as Schema from "effect/Schema"
|
|
4
|
-
import
|
|
6
|
+
import * as SchemaAST from "effect/SchemaAST"
|
|
5
7
|
import type * as Assistant from "../Assistant.ts"
|
|
6
8
|
import * as AssistantEvent from "../AssistantEvent.ts"
|
|
7
|
-
import {
|
|
9
|
+
import { inputKey } from "./input.ts"
|
|
8
10
|
|
|
9
11
|
/** @internal */
|
|
10
12
|
export interface AppEvents {
|
|
11
|
-
readonly byTag: ReadonlyMap<string,
|
|
12
|
-
readonly wakes: ReadonlySet<string>
|
|
13
|
+
readonly byTag: ReadonlyMap<string, AssistantEvent.AppEventSchema>
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
/** @internal */
|
|
17
|
+
export const tagOf = (schema: AssistantEvent.AppEventSchema): string => {
|
|
18
|
+
const ast = schema.fields._tag.ast
|
|
19
|
+
|
|
20
|
+
if (SchemaAST.isLiteral(ast) && Predicate.isString(ast.literal)) return ast.literal
|
|
21
|
+
|
|
22
|
+
throw new Error("An app event is a tagged struct or class.")
|
|
18
23
|
}
|
|
19
24
|
|
|
20
25
|
/** @internal */
|
|
21
|
-
export const make = (events: ReadonlyArray<
|
|
22
|
-
const byTag = new Map(
|
|
23
|
-
events.map((event): [string, Registered] => [event.tag, { schema: event.schema, render: event.render }])
|
|
24
|
-
)
|
|
26
|
+
export const make = (events: ReadonlyArray<AssistantEvent.AppEventSchema>): AppEvents => {
|
|
27
|
+
const byTag = new Map(events.map((schema): [string, AssistantEvent.AppEventSchema] => [tagOf(schema), schema]))
|
|
25
28
|
|
|
26
29
|
for (const tag of byTag.keys()) {
|
|
27
30
|
if (AssistantEvent.isLibraryEvent({ _tag: tag })) throw new Error(`${tag} is one of the library's events.`)
|
|
@@ -29,17 +32,17 @@ export const make = (events: ReadonlyArray<AppEvent.Any>): AppEvents => {
|
|
|
29
32
|
|
|
30
33
|
if (byTag.size !== events.length) throw new Error("An assistant's events have different tags.")
|
|
31
34
|
|
|
32
|
-
return { byTag
|
|
35
|
+
return { byTag }
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
/** @internal */
|
|
36
39
|
export const encode = Effect.fnUntraced(function* (app: AppEvents, event: { readonly _tag: string }) {
|
|
37
|
-
const
|
|
40
|
+
const schema = app.byTag.get(event._tag)
|
|
38
41
|
|
|
39
42
|
// The types only let registered events through, so one with no schema here is a defect.
|
|
40
|
-
if (
|
|
43
|
+
if (schema === undefined) return yield* Effect.die(`${event._tag} isn't one of this assistant's events.`)
|
|
41
44
|
|
|
42
|
-
const json = yield* Schema.encodeUnknownEffect(Schema.toCodecJson(
|
|
45
|
+
const json = yield* Schema.encodeUnknownEffect(Schema.toCodecJson(schema))(event)
|
|
43
46
|
|
|
44
47
|
return yield* Schema.decodeUnknownEffect(AssistantEvent.StoredAppEvent)(json)
|
|
45
48
|
}, Effect.orDie)
|
|
@@ -47,6 +50,30 @@ export const encode = Effect.fnUntraced(function* (app: AppEvents, event: { read
|
|
|
47
50
|
/** @internal */
|
|
48
51
|
export const isRegistered = (app: AppEvents, event: { readonly _tag: string }): boolean => app.byTag.has(event._tag)
|
|
49
52
|
|
|
53
|
+
// The app's schema decodes the event as the app wrote it.
|
|
54
|
+
const unmarked = (event: { readonly _tag: string }) =>
|
|
55
|
+
Predicate.hasProperty(event, inputKey) ? Record.remove(event, inputKey) : event
|
|
56
|
+
|
|
57
|
+
// An event as the log stores it, decoded with its schema.
|
|
58
|
+
/** @internal */
|
|
59
|
+
export const decodeWith = (
|
|
60
|
+
schema: AssistantEvent.AppEventSchema,
|
|
61
|
+
event: { readonly _tag: string }
|
|
62
|
+
): Option.Option<{ readonly _tag: string }> => Schema.decodeUnknownOption(Schema.toCodecJson(schema))(unmarked(event))
|
|
63
|
+
|
|
64
|
+
const decodeEvent = (app: AppEvents, event: { readonly _tag: string }): Option.Option<{ readonly _tag: string }> => {
|
|
65
|
+
const schema = app.byTag.get(event._tag)
|
|
66
|
+
|
|
67
|
+
return schema === undefined ? Option.none() : decodeWith(schema, event)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// What came in through `submit`, from outside a turn, is an input: it wakes a turn.
|
|
71
|
+
/** @internal */
|
|
72
|
+
export const markInput = (event: AssistantEvent.StoredAppEvent): AssistantEvent.StoredAppEvent => ({
|
|
73
|
+
...event,
|
|
74
|
+
[inputKey]: true
|
|
75
|
+
})
|
|
76
|
+
|
|
50
77
|
// An app event that doesn't decode (say, one from an older version of its schema) is left out.
|
|
51
78
|
/** @internal */
|
|
52
79
|
export const decodeEntry = (app: AppEvents, entry: AssistantEvent.AnyEntry): Option.Option<Assistant.Entry<any>> => {
|
|
@@ -54,14 +81,7 @@ export const decodeEntry = (app: AppEvents, entry: AssistantEvent.AnyEntry): Opt
|
|
|
54
81
|
|
|
55
82
|
if (AssistantEvent.isLibraryEvent(event)) return Option.some({ ...toEntry(entry), event })
|
|
56
83
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if (registered === undefined) return Option.none()
|
|
60
|
-
|
|
61
|
-
return Option.map(Schema.decodeUnknownOption(Schema.toCodecJson(registered.schema))(event), (decoded) => ({
|
|
62
|
-
...toEntry(entry),
|
|
63
|
-
event: decoded
|
|
64
|
-
}))
|
|
84
|
+
return Option.map(decodeEvent(app, event), (decoded) => ({ ...toEntry(entry), event: decoded }))
|
|
65
85
|
}
|
|
66
86
|
|
|
67
87
|
/** @internal */
|
|
@@ -73,28 +93,8 @@ export const decodeEntries = (
|
|
|
73
93
|
/** @internal */
|
|
74
94
|
export type DecodedEntries = ReturnType<typeof decodeEntries>
|
|
75
95
|
|
|
76
|
-
const decodeStored = Schema.decodeUnknownOption(AssistantEvent.StoredAppEvent)
|
|
77
|
-
|
|
78
96
|
const toEntry = (entry: AssistantEvent.AnyEntry) => ({
|
|
79
97
|
chatId: entry.chatId,
|
|
80
98
|
seq: entry.seq,
|
|
81
99
|
at: entry.at
|
|
82
100
|
})
|
|
83
|
-
|
|
84
|
-
/** @internal */
|
|
85
|
-
export const read = (app: AppEvents, event: { readonly _tag: string }): string | undefined => {
|
|
86
|
-
const registered = app.byTag.get(event._tag)
|
|
87
|
-
|
|
88
|
-
if (registered?.render === undefined) return undefined
|
|
89
|
-
|
|
90
|
-
if (registered.render !== "json") return registered.render(event)
|
|
91
|
-
|
|
92
|
-
const stored = Schema.encodeUnknownOption(Schema.toCodecJson(registered.schema))(event).pipe(
|
|
93
|
-
Option.flatMap(decodeStored)
|
|
94
|
-
)
|
|
95
|
-
|
|
96
|
-
return Option.match(stored, {
|
|
97
|
-
onNone: () => undefined,
|
|
98
|
-
onSome: ({ _tag, ...fields }) => `<${_tag}>\n${toJsonText(fields)}\n</${_tag}>`
|
|
99
|
-
})
|
|
100
|
-
}
|
package/src/internal/history.ts
CHANGED
|
@@ -110,12 +110,17 @@ const answerText = (answer: AssistantEvent.BlockAnswer) =>
|
|
|
110
110
|
const resultText = (result: AssistantEvent.ToolResult) =>
|
|
111
111
|
`<result tool="${result.tool}" call="${result.callId}" outcome="${result.outcome}">\nThe result of a call from earlier in this chat.\n${toJsonText(result.result)}\n</result>`
|
|
112
112
|
|
|
113
|
-
|
|
113
|
+
const isEntry = (item: AssistantEvent.AnyEntry | Prompt.Message): item is AssistantEvent.AnyEntry =>
|
|
114
|
+
Predicate.hasProperty(item, "seq")
|
|
115
|
+
|
|
116
|
+
// In the order the model saw the entries, not the order they were stored. A message a history view put in stands
|
|
117
|
+
// 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
|
|
118
|
+
// read; the app's own events reach the model only as such messages.
|
|
114
119
|
/** @internal */
|
|
115
120
|
export const fromEntries = (
|
|
116
|
-
|
|
117
|
-
readApp: (event: { readonly _tag: string }) => string | undefined
|
|
121
|
+
items: ReadonlyArray<AssistantEvent.AnyEntry | Prompt.Message>
|
|
118
122
|
): ReadonlyArray<Prompt.Message> => {
|
|
123
|
+
const entries = items.filter(isEntry)
|
|
119
124
|
const history: Array<Prompt.Message> = []
|
|
120
125
|
const held: Array<Prompt.Message> = []
|
|
121
126
|
const open = new Set<string>()
|
|
@@ -133,27 +138,35 @@ export const fromEntries = (
|
|
|
133
138
|
: undefined
|
|
134
139
|
}
|
|
135
140
|
|
|
136
|
-
|
|
141
|
+
// `index` is the last entry at or before the item, and `seq` where the item stands.
|
|
142
|
+
const input = (index: number, seq: number, item: Prompt.Message) => {
|
|
137
143
|
const seen = nextSeen(index)
|
|
138
144
|
|
|
139
|
-
if (open.size > 0 || (seen !== undefined && seen <
|
|
145
|
+
if (open.size > 0 || (seen !== undefined && seen < seq)) held.push(item)
|
|
140
146
|
else history.push(item)
|
|
141
147
|
}
|
|
142
148
|
|
|
143
|
-
|
|
144
|
-
if (!AssistantEvent.isLibraryEvent(event)) {
|
|
145
|
-
const text = readApp(event)
|
|
149
|
+
let index = -1
|
|
146
150
|
|
|
147
|
-
|
|
151
|
+
for (const item of items) {
|
|
152
|
+
if (!isEntry(item)) {
|
|
153
|
+
input(index, (entries[index]?.seq ?? 0) + 0.5, item)
|
|
148
154
|
|
|
149
|
-
|
|
155
|
+
continue
|
|
150
156
|
}
|
|
151
157
|
|
|
158
|
+
index += 1
|
|
159
|
+
|
|
160
|
+
const { event, seq } = item
|
|
161
|
+
const at = index
|
|
162
|
+
|
|
163
|
+
if (!AssistantEvent.isLibraryEvent(event)) continue
|
|
164
|
+
|
|
152
165
|
Match.value(event).pipe(
|
|
153
166
|
Match.tagsExhaustive({
|
|
154
167
|
UserMessage: (message) =>
|
|
155
|
-
input(
|
|
156
|
-
BlockAnswer: (answer) => input(
|
|
168
|
+
input(at, seq, userText(AssistantEvent.textWithAttachments(message.text, message.attachments))),
|
|
169
|
+
BlockAnswer: (answer) => input(at, seq, userText(answerText(answer))),
|
|
157
170
|
ModelOutput: (output) => {
|
|
158
171
|
history.push(...Prompt.fromResponseParts(output.parts).content)
|
|
159
172
|
|
|
@@ -169,7 +182,7 @@ export const fromEntries = (
|
|
|
169
182
|
ToolResult: (result) => {
|
|
170
183
|
// A view that starts after the call (since a summary, say) keeps its result as text: a tool result needs its
|
|
171
184
|
// call right before it.
|
|
172
|
-
if (!called.has(result.callId)) return input(
|
|
185
|
+
if (!called.has(result.callId)) return input(at, seq, userText(resultText(result)))
|
|
173
186
|
|
|
174
187
|
history.push(toolMessage(result))
|
|
175
188
|
open.delete(result.callId)
|
|
@@ -180,21 +193,21 @@ export const fromEntries = (
|
|
|
180
193
|
TurnStarted: () => undefined,
|
|
181
194
|
TurnCompleted: () => undefined,
|
|
182
195
|
TurnFailed: () => undefined,
|
|
183
|
-
SpecialistFinished: (finished) => input(
|
|
196
|
+
SpecialistFinished: (finished) => input(at, seq, userText(finishedText(entries, at, finished))),
|
|
184
197
|
// Every run starts in a call of the model's own (a spawn or a follow-up), whose result says so.
|
|
185
198
|
SpecialistStarted: () => undefined,
|
|
186
199
|
BlocksClosed: () => undefined,
|
|
187
200
|
ChatDeleted: () => undefined
|
|
188
201
|
})
|
|
189
202
|
)
|
|
190
|
-
}
|
|
203
|
+
}
|
|
191
204
|
|
|
192
205
|
return [...history, ...held]
|
|
193
206
|
}
|
|
194
207
|
|
|
195
208
|
/** @internal */
|
|
196
|
-
export const isInput = (event: { readonly _tag: string }
|
|
197
|
-
AssistantEvent.isInput(event) ||
|
|
209
|
+
export const isInput = (event: { readonly _tag: string }): boolean =>
|
|
210
|
+
AssistantEvent.isInput(event) || AssistantEvent.isAppInput(event)
|
|
198
211
|
|
|
199
212
|
/** @internal */
|
|
200
213
|
export const handledThrough = (entries: ReadonlyArray<AssistantEvent.AnyEntry>): number => {
|
|
@@ -206,21 +219,18 @@ export const handledThrough = (entries: ReadonlyArray<AssistantEvent.AnyEntry>):
|
|
|
206
219
|
// A run the assistant stopped isn't an input here: it wakes nothing.
|
|
207
220
|
/** @internal */
|
|
208
221
|
export const unhandledInputs = (
|
|
209
|
-
entries: ReadonlyArray<AssistantEvent.AnyEntry
|
|
210
|
-
wakes: ReadonlySet<string>
|
|
222
|
+
entries: ReadonlyArray<AssistantEvent.AnyEntry>
|
|
211
223
|
): ReadonlyArray<{ readonly _tag: string }> => {
|
|
212
224
|
const seen = handledThrough(entries)
|
|
213
225
|
|
|
214
226
|
return entries.flatMap(({ event, seq }) =>
|
|
215
|
-
seq > seen && isInput(event
|
|
216
|
-
? [event]
|
|
217
|
-
: []
|
|
227
|
+
seq > seen && isInput(event) && !(AssistantEvent.isInput(event) && !AssistantEvent.isWaking(event)) ? [event] : []
|
|
218
228
|
)
|
|
219
229
|
}
|
|
220
230
|
|
|
221
231
|
/** @internal */
|
|
222
|
-
export const needsTurn = (entries: ReadonlyArray<AssistantEvent.AnyEntry
|
|
223
|
-
unhandledInputs(entries
|
|
232
|
+
export const needsTurn = (entries: ReadonlyArray<AssistantEvent.AnyEntry>): boolean =>
|
|
233
|
+
unhandledInputs(entries).length > 0
|
|
224
234
|
|
|
225
235
|
// A failure the user was shown counts as a reply.
|
|
226
236
|
/** @internal */
|
|
@@ -287,8 +297,5 @@ export const owesDelivery = (entries: ReadonlyArray<AssistantEvent.AnyEntry>, th
|
|
|
287
297
|
}
|
|
288
298
|
|
|
289
299
|
/** @internal */
|
|
290
|
-
export const unreadAfter = (
|
|
291
|
-
entries
|
|
292
|
-
throughSeq: number,
|
|
293
|
-
wakes: ReadonlySet<string>
|
|
294
|
-
): boolean => entries.some(({ event, seq }) => seq > throughSeq && isInput(event, wakes))
|
|
300
|
+
export const unreadAfter = (entries: ReadonlyArray<AssistantEvent.AnyEntry>, throughSeq: number): boolean =>
|
|
301
|
+
entries.some(({ event, seq }) => seq > throughSeq && isInput(event))
|
package/src/internal/skills.ts
CHANGED
|
@@ -8,6 +8,17 @@ import type { Skill } from "../Skill.ts"
|
|
|
8
8
|
import * as ToolFailure from "../ToolFailure.ts"
|
|
9
9
|
import * as Toolkits from "./toolkit.ts"
|
|
10
10
|
|
|
11
|
+
// A skill whose tools the loop doesn't have would send the model after calls it can't make.
|
|
12
|
+
/** @internal */
|
|
13
|
+
export const available = <R>(
|
|
14
|
+
skills: ReadonlyArray<Skill<R>>,
|
|
15
|
+
definitions: ReadonlyArray<Tool.Any>
|
|
16
|
+
): ReadonlyArray<Skill<R>> => {
|
|
17
|
+
const names = new Set(definitions.map((tool) => tool.name))
|
|
18
|
+
|
|
19
|
+
return skills.filter((skill) => (skill.tools ?? []).every((tool) => names.has(tool.name)))
|
|
20
|
+
}
|
|
21
|
+
|
|
11
22
|
/** @internal */
|
|
12
23
|
export const toTools = <R>(skills: ReadonlyArray<Skill<R>>): Toolkits.Tools<R> => {
|
|
13
24
|
if (skills.length === 0) return Toolkits.empty
|
|
@@ -84,11 +84,12 @@ export const runBrief = Effect.fnUntraced(
|
|
|
84
84
|
brief: options.brief,
|
|
85
85
|
history: current,
|
|
86
86
|
loop: (loop) => {
|
|
87
|
-
const
|
|
87
|
+
const own = Toolkits.fromToolkitOrEmpty(loop.toolkit)
|
|
88
|
+
const skills = Skills.available(loop.skills ?? [], own.definitions)
|
|
88
89
|
|
|
89
90
|
return Loop.run({
|
|
90
91
|
instructions: mapText(loop.instructions, (text) => Skill.withCatalog(text, skills)),
|
|
91
|
-
toolkit: Toolkits.merge(Skills.toTools(skills),
|
|
92
|
+
toolkit: Toolkits.merge(Skills.toTools(skills), own),
|
|
92
93
|
history: current,
|
|
93
94
|
maxSteps: loop.maxSteps ?? 8
|
|
94
95
|
})
|
package/src/internal/turns.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type * as LanguageModel from "effect/ai/LanguageModel"
|
|
2
2
|
import type * as Tool from "effect/ai/Tool"
|
|
3
|
-
import type * as Prompt from "effect/ai/Prompt"
|
|
4
3
|
import * as Cause from "effect/Cause"
|
|
5
4
|
import * as Context from "effect/Context"
|
|
6
5
|
import * as Effect from "effect/Effect"
|
|
@@ -8,6 +7,7 @@ import * as Exit from "effect/Exit"
|
|
|
8
7
|
import { identity } from "effect/Function"
|
|
9
8
|
import * as Option from "effect/Option"
|
|
10
9
|
import * as Predicate from "effect/Predicate"
|
|
10
|
+
import * as Ref from "effect/Ref"
|
|
11
11
|
import * as Schema from "effect/Schema"
|
|
12
12
|
import type * as Scope from "effect/Scope"
|
|
13
13
|
import * as Stream from "effect/Stream"
|
|
@@ -127,34 +127,6 @@ const failureMessage = (cause: Cause.Cause<unknown>): string =>
|
|
|
127
127
|
|
|
128
128
|
const lastSeq = (entries: ReadonlyArray<AssistantEvent.AnyEntry>, otherwise: number) => entries.at(-1)?.seq ?? otherwise
|
|
129
129
|
|
|
130
|
-
const toHistory = (
|
|
131
|
-
viewed: ReadonlyArray<Assistant.Entry<any> | Prompt.Message>,
|
|
132
|
-
app: AppEvents.AppEvents
|
|
133
|
-
): ReadonlyArray<Prompt.Message> => {
|
|
134
|
-
const out: Array<Prompt.Message> = []
|
|
135
|
-
let run: Array<AssistantEvent.AnyEntry> = []
|
|
136
|
-
|
|
137
|
-
const flush = () => {
|
|
138
|
-
out.push(...History.fromEntries(run, (event) => AppEvents.read(app, event)))
|
|
139
|
-
run = []
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
for (const item of viewed) {
|
|
143
|
-
if (isEntry(item)) run.push(item)
|
|
144
|
-
else {
|
|
145
|
-
flush()
|
|
146
|
-
out.push(item)
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
flush()
|
|
151
|
-
|
|
152
|
-
return out
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const isEntry = (item: Assistant.Entry<any> | Prompt.Message): item is Assistant.Entry<any> =>
|
|
156
|
-
Predicate.hasProperty(item, "seq")
|
|
157
|
-
|
|
158
130
|
// The handler's stream can end between a step's output and its tools, or while they run. An interrupted turn (a
|
|
159
131
|
// shutdown) leaves its calls open instead: the next turn runs them again.
|
|
160
132
|
const closeOpenCalls = Effect.fnUntraced(function* (chat: Writer, startSeq: number) {
|
|
@@ -164,7 +136,7 @@ const closeOpenCalls = Effect.fnUntraced(function* (chat: Writer, startSeq: numb
|
|
|
164
136
|
entries.flatMap(({ event }) => (Predicate.isTagged(event, "ToolStarted") ? [event.callId] : []))
|
|
165
137
|
)
|
|
166
138
|
|
|
167
|
-
const open = History.unansweredCalls(History.fromEntries(entries
|
|
139
|
+
const open = History.unansweredCalls(History.fromEntries(entries))
|
|
168
140
|
|
|
169
141
|
if (open.length === 0) return
|
|
170
142
|
|
|
@@ -196,13 +168,13 @@ const handledThrough = (entries: ReadonlyArray<AssistantEvent.AnyEntry>, startSe
|
|
|
196
168
|
startSeq
|
|
197
169
|
)
|
|
198
170
|
|
|
199
|
-
//
|
|
200
|
-
// next turn, which replies.
|
|
201
|
-
const finishTurn = Effect.fnUntraced(function* (chat: Writer, read: number,
|
|
171
|
+
// A turn that ran the loop fails visibly when the user's latest input got no reply; an input that arrived after what
|
|
172
|
+
// its model read gets the next turn, which replies. A handler that ended without the loop decided that itself.
|
|
173
|
+
const finishTurn = Effect.fnUntraced(function* (chat: Writer, read: number, ranLoop: boolean) {
|
|
202
174
|
const entries = yield* chat.entries
|
|
203
175
|
|
|
204
176
|
const end =
|
|
205
|
-
History.owesReply(entries, read) && !History.unreadAfter(entries, read
|
|
177
|
+
ranLoop && History.owesReply(entries, read) && !History.unreadAfter(entries, read)
|
|
206
178
|
? AssistantEvent.TurnFailed.make({ throughSeq: read, message: silentTurnMessage })
|
|
207
179
|
: AssistantEvent.TurnCompleted.make({ throughSeq: read })
|
|
208
180
|
|
|
@@ -222,11 +194,15 @@ const makeTurnApi = (
|
|
|
222
194
|
registration: Registration,
|
|
223
195
|
chat: Writer,
|
|
224
196
|
startSeq: number,
|
|
225
|
-
checkOpen: Effect.Effect<void
|
|
197
|
+
checkOpen: Effect.Effect<void>,
|
|
198
|
+
ranLoop: Ref.Ref<boolean>
|
|
226
199
|
): Assistant.Turn<any> => {
|
|
227
200
|
const { definition, builtins } = registration
|
|
228
201
|
const { app } = definition
|
|
229
202
|
|
|
203
|
+
const running = <A, E, R>(stream: Stream.Stream<A, E, R>) =>
|
|
204
|
+
Stream.unwrap(Ref.set(ranLoop, true).pipe(Effect.as(stream)))
|
|
205
|
+
|
|
230
206
|
// With how far into the chat it goes: the step records it on the model's output.
|
|
231
207
|
// Read before every model call: a chat the app deleted meanwhile stops here.
|
|
232
208
|
const history = Effect.fnUntraced(function* (view: Assistant.HistoryView<any> = identity) {
|
|
@@ -235,7 +211,7 @@ const makeTurnApi = (
|
|
|
235
211
|
const all = yield* chat.entries
|
|
236
212
|
|
|
237
213
|
const read: AssistantLoop.ChatHistory = {
|
|
238
|
-
messages:
|
|
214
|
+
messages: History.fromEntries(view(AppEvents.decodeEntries(app, all))),
|
|
239
215
|
throughSeq: lastSeq(all, startSeq)
|
|
240
216
|
}
|
|
241
217
|
|
|
@@ -258,7 +234,7 @@ const makeTurnApi = (
|
|
|
258
234
|
const through = handledThrough(all, startSeq)
|
|
259
235
|
|
|
260
236
|
// Something arrived after what the loop's model read: the next turn reads it with the full loop, and replies.
|
|
261
|
-
if (History.unreadAfter(all, through
|
|
237
|
+
if (History.unreadAfter(all, through)) return Stream.empty
|
|
262
238
|
|
|
263
239
|
const owed = History.owesReply(all, through)
|
|
264
240
|
const undelivered = !owed && History.owesDelivery(all, through)
|
|
@@ -282,7 +258,7 @@ const makeTurnApi = (
|
|
|
282
258
|
|
|
283
259
|
return {
|
|
284
260
|
messages: [
|
|
285
|
-
...
|
|
261
|
+
...History.fromEntries(options.view(AppEvents.decodeEntries(app, entries))),
|
|
286
262
|
History.userText(nudge)
|
|
287
263
|
],
|
|
288
264
|
throughSeq: lastSeq(entries, startSeq)
|
|
@@ -295,22 +271,22 @@ const makeTurnApi = (
|
|
|
295
271
|
)
|
|
296
272
|
|
|
297
273
|
const replyIfOwed = (options: Assistant.ReplyOptions) =>
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
274
|
+
running(
|
|
275
|
+
replyPass({
|
|
276
|
+
instructions: instructions(options.instructions),
|
|
277
|
+
toolkit: builtins,
|
|
278
|
+
view: identity,
|
|
279
|
+
nudge: options.nudge
|
|
280
|
+
})
|
|
281
|
+
)
|
|
304
282
|
|
|
305
283
|
const prepare = <T extends Record<string, Tool.Any>, RS>(options: Assistant.LoopOptions<T, RS, any>) => {
|
|
306
|
-
const
|
|
284
|
+
const own = Toolkits.fromToolkitOrEmpty(options.toolkit)
|
|
285
|
+
const skills = Skills.available(options.skills ?? [], [...builtins.definitions, ...own.definitions])
|
|
307
286
|
|
|
308
287
|
return {
|
|
309
288
|
system: mapText(instructions(options.instructions), (text) => Skill.withCatalog(text, skills)),
|
|
310
|
-
toolkit: Toolkits.merge(
|
|
311
|
-
Toolkits.merge(builtins, Skills.toTools(skills)),
|
|
312
|
-
Toolkits.fromToolkitOrEmpty(options.toolkit)
|
|
313
|
-
),
|
|
289
|
+
toolkit: Toolkits.merge(Toolkits.merge(builtins, Skills.toTools(skills)), own),
|
|
314
290
|
view: options.history ?? identity
|
|
315
291
|
}
|
|
316
292
|
}
|
|
@@ -334,7 +310,7 @@ const makeTurnApi = (
|
|
|
334
310
|
maxSteps: options.maxSteps ?? 16
|
|
335
311
|
})
|
|
336
312
|
|
|
337
|
-
return Stream.concat(loop, replyPass({ instructions: system, toolkit, view, nudge: options.replyNudge }))
|
|
313
|
+
return running(Stream.concat(loop, replyPass({ instructions: system, toolkit, view, nudge: options.replyNudge })))
|
|
338
314
|
}
|
|
339
315
|
}
|
|
340
316
|
}
|
|
@@ -353,7 +329,7 @@ export const run = Effect.fnUntraced(
|
|
|
353
329
|
const { chat } = options
|
|
354
330
|
const before = yield* chat.entries
|
|
355
331
|
|
|
356
|
-
yield* Telemetry.agentInput(History.unhandledInputs(before
|
|
332
|
+
yield* Telemetry.agentInput(History.unhandledInputs(before))
|
|
357
333
|
|
|
358
334
|
const assistant = options.registration.definition.name
|
|
359
335
|
const traceId = yield* Telemetry.currentTraceId
|
|
@@ -368,7 +344,8 @@ export const run = Effect.fnUntraced(
|
|
|
368
344
|
])
|
|
369
345
|
|
|
370
346
|
const startSeq = lastSeq(started, 0)
|
|
371
|
-
const
|
|
347
|
+
const ranLoop = yield* Ref.make(false)
|
|
348
|
+
const turn = makeTurnApi(options.registration, chat, startSeq, options.checkOpen, ranLoop)
|
|
372
349
|
|
|
373
350
|
const record = (event: AssistantLoop.Event) => chat.append([event])
|
|
374
351
|
|
|
@@ -385,7 +362,7 @@ export const run = Effect.fnUntraced(
|
|
|
385
362
|
onSuccess: Effect.fnUntraced(function* () {
|
|
386
363
|
const all = yield* chat.entries
|
|
387
364
|
|
|
388
|
-
yield* finishTurn(chat, handledThrough(all, startSeq),
|
|
365
|
+
yield* finishTurn(chat, handledThrough(all, startSeq), yield* Ref.get(ranLoop))
|
|
389
366
|
}),
|
|
390
367
|
onFailure: (cause) => failTurn(chat, cause)
|
|
391
368
|
})
|