effect-assistant 0.1.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/README.md +1 -1
- 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 +52 -70
- package/dist/AssistantEvent.d.ts.map +1 -1
- package/dist/AssistantEvent.js +28 -57
- package/dist/AssistantEvent.js.map +1 -1
- package/dist/AssistantLoop.d.ts +7 -5
- package/dist/AssistantLoop.d.ts.map +1 -1
- package/dist/AssistantLoop.js +6 -3
- package/dist/AssistantLoop.js.map +1 -1
- package/dist/Block.js.map +1 -1
- package/dist/BlockKit.d.ts +14 -18
- package/dist/BlockKit.d.ts.map +1 -1
- package/dist/BlockKit.js +4 -7
- package/dist/BlockKit.js.map +1 -1
- package/dist/ChatLog.js +1 -1
- package/dist/Compaction.d.ts +8 -4
- package/dist/Compaction.d.ts.map +1 -1
- package/dist/Compaction.js +12 -15
- 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/ToolDiscovery.d.ts +4 -5
- package/dist/ToolDiscovery.d.ts.map +1 -1
- package/dist/ToolDiscovery.js +5 -4
- package/dist/ToolDiscovery.js.map +1 -1
- package/dist/ToolFailure.d.ts +1 -1
- package/dist/ToolFailure.js +1 -1
- package/dist/ToolFailure.js.map +1 -1
- package/dist/index.d.ts +2 -5
- package/dist/index.js +4 -7
- package/dist/internal/chat.js +11 -37
- 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 +26 -58
- 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.d.ts +2 -2
- package/dist/internal/loop.d.ts.map +1 -1
- package/dist/internal/loop.js +21 -56
- package/dist/internal/loop.js.map +1 -1
- package/dist/internal/skills.js +6 -1
- package/dist/internal/skills.js.map +1 -1
- package/dist/internal/specialists.js +7 -7
- package/dist/internal/specialists.js.map +1 -1
- package/dist/internal/toolkit.d.ts +6 -20
- package/dist/internal/toolkit.d.ts.map +1 -1
- package/dist/internal/toolkit.js +10 -31
- package/dist/internal/toolkit.js.map +1 -1
- package/dist/internal/tools.js +2 -21
- package/dist/internal/tools.js.map +1 -1
- package/dist/internal/turns.d.ts.map +1 -1
- package/dist/internal/turns.js +24 -43
- package/dist/internal/turns.js.map +1 -1
- package/package.json +3 -2
- package/src/Assistant.ts +75 -29
- package/src/AssistantActivity.ts +11 -18
- package/src/AssistantEngine.ts +110 -43
- package/src/AssistantEvent.ts +46 -74
- package/src/AssistantLoop.ts +7 -5
- package/src/Block.ts +1 -1
- package/src/BlockKit.ts +18 -31
- package/src/Compaction.ts +8 -21
- 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/ToolDiscovery.ts +5 -13
- package/src/ToolFailure.ts +1 -1
- package/src/index.ts +5 -20
- package/src/internal/chat.ts +8 -57
- package/src/internal/events.ts +43 -43
- package/src/internal/history.ts +36 -101
- package/src/internal/input.ts +3 -0
- package/src/internal/loop.ts +31 -101
- package/src/internal/skills.ts +11 -0
- package/src/internal/specialists.ts +6 -9
- package/src/internal/toolkit.ts +17 -60
- package/src/internal/tools.ts +2 -53
- package/src/internal/turns.ts +37 -79
- 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 -459
- package/dist/ChatView.d.ts.map +0 -1
- package/dist/ChatView.js +0 -496
- package/dist/ChatView.js.map +0 -1
- package/dist/Decision.d.ts +0 -145
- package/dist/Decision.d.ts.map +0 -1
- package/dist/Decision.js +0 -178
- package/dist/Decision.js.map +0 -1
- package/dist/DecisionId.d.ts +0 -37
- package/dist/DecisionId.d.ts.map +0 -1
- package/dist/DecisionId.js +0 -44
- package/dist/DecisionId.js.map +0 -1
- package/dist/internal/decisions.js +0 -66
- package/dist/internal/decisions.js.map +0 -1
- package/src/AppEvent.ts +0 -134
- package/src/ChatView.ts +0 -658
- package/src/Decision.ts +0 -241
- package/src/DecisionId.ts +0 -46
- package/src/internal/decisions.ts +0 -131
package/src/ToolDiscovery.ts
CHANGED
|
@@ -94,8 +94,8 @@ const requiring =
|
|
|
94
94
|
* the tools' handlers. A catalog tool's services are `execute_tool`'s, so the turn provides them
|
|
95
95
|
* as it does for any tool.
|
|
96
96
|
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
97
|
+
* A catalog tool with Effect's `needsApproval` set fails the layer when it is built, as it would
|
|
98
|
+
* fail a loop: the library doesn't run calls that wait for approval.
|
|
99
99
|
*
|
|
100
100
|
* **Example** (Finding rarely used tools when needed)
|
|
101
101
|
*
|
|
@@ -146,6 +146,9 @@ export const layer = <T extends Record<string, Tool.Any>>(catalog: Toolkit.Toolk
|
|
|
146
146
|
Effect.gen(function* () {
|
|
147
147
|
const resolved = yield* catalog
|
|
148
148
|
const definitions = Object.values(resolved.tools)
|
|
149
|
+
const needingApproval = Toolkits.needingApproval(definitions)
|
|
150
|
+
|
|
151
|
+
if (needingApproval.length > 0) return yield* Effect.die(Toolkits.needsApprovalMessage(needingApproval))
|
|
149
152
|
|
|
150
153
|
return {
|
|
151
154
|
search_tools: ({ query }) =>
|
|
@@ -167,17 +170,6 @@ export const layer = <T extends Record<string, Tool.Any>>(catalog: Toolkit.Toolk
|
|
|
167
170
|
providerExecuted: false
|
|
168
171
|
})
|
|
169
172
|
|
|
170
|
-
// A call that needs the user's decision parks only as a call the model made itself,
|
|
171
|
-
// where it can ask for it.
|
|
172
|
-
if (yield* Toolkits.needsDecision(resolved, call, [])) {
|
|
173
|
-
return yield* Effect.fail(
|
|
174
|
-
Toolkits.failed(
|
|
175
|
-
`${name} needs the user's decision, so it can't run through execute_tool.`,
|
|
176
|
-
"NeedsDecision"
|
|
177
|
-
).result
|
|
178
|
-
)
|
|
179
|
-
}
|
|
180
|
-
|
|
181
173
|
const { outcome, result } = yield* Toolkits.runNative(resolved, call)
|
|
182
174
|
|
|
183
175
|
return outcome === "failure" ? yield* Effect.fail(result) : result
|
package/src/ToolFailure.ts
CHANGED
|
@@ -15,7 +15,7 @@ import * as Schema from "effect/Schema"
|
|
|
15
15
|
*
|
|
16
16
|
* It holds a message that says what went wrong and, optionally, a `reason` the model can tell
|
|
17
17
|
* apart, such as `"NotFound"` or `"OverBudget"`. The library's own reasons are PascalCase, like
|
|
18
|
-
* `"
|
|
18
|
+
* `"ToolNotFoundError"`.
|
|
19
19
|
*
|
|
20
20
|
* @category errors
|
|
21
21
|
* @since 0.1.0
|
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
|
*/
|
|
@@ -79,16 +69,6 @@ export * as Compaction from "./Compaction.ts"
|
|
|
79
69
|
*/
|
|
80
70
|
export * as CurrentChat from "./CurrentChat.ts"
|
|
81
71
|
|
|
82
|
-
/**
|
|
83
|
-
* @since 0.1.0
|
|
84
|
-
*/
|
|
85
|
-
export * as Decision from "./Decision.ts"
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* @since 0.1.0
|
|
89
|
-
*/
|
|
90
|
-
export * as DecisionId from "./DecisionId.ts"
|
|
91
|
-
|
|
92
72
|
/**
|
|
93
73
|
* @since 0.1.0
|
|
94
74
|
*/
|
|
@@ -119,6 +99,11 @@ export * as Skill from "./Skill.ts"
|
|
|
119
99
|
*/
|
|
120
100
|
export * as Specialist from "./Specialist.ts"
|
|
121
101
|
|
|
102
|
+
/**
|
|
103
|
+
* @since 0.3.0
|
|
104
|
+
*/
|
|
105
|
+
export * as Timeline from "./Timeline.ts"
|
|
106
|
+
|
|
122
107
|
/**
|
|
123
108
|
* @since 0.1.0
|
|
124
109
|
*/
|
package/src/internal/chat.ts
CHANGED
|
@@ -5,7 +5,6 @@ import * as Effect from "effect/Effect"
|
|
|
5
5
|
import * as Exit from "effect/Exit"
|
|
6
6
|
import * as Fiber from "effect/Fiber"
|
|
7
7
|
import * as FiberSet from "effect/FiberSet"
|
|
8
|
-
import * as Option from "effect/Option"
|
|
9
8
|
import * as Predicate from "effect/Predicate"
|
|
10
9
|
import * as PubSub from "effect/PubSub"
|
|
11
10
|
import * as Ref from "effect/Ref"
|
|
@@ -17,10 +16,8 @@ import * as AssistantEvent from "../AssistantEvent.ts"
|
|
|
17
16
|
import type { ChatId } from "../ChatId.ts"
|
|
18
17
|
import { ChatLog } from "../ChatLog.ts"
|
|
19
18
|
import { CurrentChat } from "../CurrentChat.ts"
|
|
20
|
-
import type { InputId } from "../InputId.ts"
|
|
21
19
|
import * as Chats from "../Chats.ts"
|
|
22
20
|
import * as SpecialistService from "../Specialist.ts"
|
|
23
|
-
import * as Decisions from "./decisions.ts"
|
|
24
21
|
import * as AppEvents from "./events.ts"
|
|
25
22
|
import * as History from "./history.ts"
|
|
26
23
|
import * as Specialists from "./specialists.ts"
|
|
@@ -104,7 +101,6 @@ export const make = Effect.fnUntraced(function* (options: {
|
|
|
104
101
|
}) {
|
|
105
102
|
const { registration, chatId } = options
|
|
106
103
|
const { app } = registration.definition
|
|
107
|
-
const { wakes } = app
|
|
108
104
|
const storage = yield* ChatLog
|
|
109
105
|
const chats = yield* Chats.Chats
|
|
110
106
|
const initial = yield* storage.read(chatId)
|
|
@@ -113,8 +109,6 @@ export const make = Effect.fnUntraced(function* (options: {
|
|
|
113
109
|
|
|
114
110
|
const channel = yield* makeChannelPubSub
|
|
115
111
|
const writes = yield* Semaphore.make(1)
|
|
116
|
-
// Inputs are stored one at a time with the decisions they make: an answer checks the decisions the last one made.
|
|
117
|
-
const storing = yield* Semaphore.make(1)
|
|
118
112
|
const turnsLock = yield* Semaphore.make(1)
|
|
119
113
|
const holding = yield* makeHolding(options.keepAlive)
|
|
120
114
|
const deleted = yield* Deferred.make<undefined>()
|
|
@@ -179,13 +173,6 @@ export const make = Effect.fnUntraced(function* (options: {
|
|
|
179
173
|
append: (...events) => Effect.asVoid(Effect.flatMap(Effect.forEach(events, toStored), chat.append))
|
|
180
174
|
}
|
|
181
175
|
|
|
182
|
-
// An approved call runs at the turn's next step.
|
|
183
|
-
const resolve = Effect.fnUntraced(function* (decided: ReadonlyArray<Decisions.Decided>, answer: InputId) {
|
|
184
|
-
yield* chat.append(
|
|
185
|
-
decided.map(({ request, outcome }) => AssistantEvent.DecisionResolved.make({ id: request.id, outcome, answer }))
|
|
186
|
-
)
|
|
187
|
-
})
|
|
188
|
-
|
|
189
176
|
// A chat the app deleted is deleted here too, which stops what works in it: a turn that checks stops there.
|
|
190
177
|
const checkAvailable: Effect.Effect<boolean> = chats.get(chatId).pipe(
|
|
191
178
|
Effect.as(true),
|
|
@@ -199,51 +186,25 @@ export const make = Effect.fnUntraced(function* (options: {
|
|
|
199
186
|
)
|
|
200
187
|
)
|
|
201
188
|
|
|
202
|
-
// Under `storing`: an answer's decisions are resolved before the next input is stored.
|
|
203
189
|
const store = Effect.fnUntraced(function* (input: AssistantEvent.Input | AssistantEvent.StoredAppEvent) {
|
|
204
190
|
if (yield* isDeleted) return yield* refused(input, "is deleted")
|
|
205
191
|
|
|
206
192
|
if (!(yield* checkAvailable)) return yield* refused(input, "is unavailable")
|
|
207
193
|
|
|
208
194
|
// Checked under the write lock: a retry that arrives while the first submit is storing is stored once.
|
|
209
|
-
|
|
195
|
+
yield* Semaphore.withPermit(writes)(
|
|
210
196
|
Effect.gen(function* () {
|
|
211
|
-
if (yield* isDeleted)
|
|
212
|
-
yield* refused(input, "is deleted")
|
|
197
|
+
if (yield* isDeleted) return yield* refused(input, "is deleted")
|
|
213
198
|
|
|
214
|
-
return Option.none()
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
const stored = yield* Ref.get(entries)
|
|
218
199
|
const id = idOf(input)
|
|
219
200
|
|
|
220
|
-
if (id !== undefined &&
|
|
221
|
-
|
|
222
|
-
const verdict = Decisions.verdict(stored, input)
|
|
201
|
+
if (id !== undefined && (yield* Ref.get(entries)).some(({ event }) => idOf(event) === id)) return
|
|
223
202
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
return Option.none()
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
yield* write([input])
|
|
231
|
-
|
|
232
|
-
return Option.some(verdict)
|
|
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)])
|
|
233
205
|
})
|
|
234
206
|
)
|
|
235
|
-
|
|
236
|
-
if (Option.isSome(verdict) && Predicate.isTagged(verdict.value, "Decide")) {
|
|
237
|
-
yield* resolve(verdict.value.decided, verdict.value.answer)
|
|
238
|
-
}
|
|
239
|
-
}, Semaphore.withPermit(storing))
|
|
240
|
-
|
|
241
|
-
// A restart between storing an answer and resolving it: resolve it now, before anything reads the chat.
|
|
242
|
-
yield* Effect.forEach(
|
|
243
|
-
Decisions.unresolved(yield* Ref.get(entries)),
|
|
244
|
-
(decided) => resolve([decided], decided.answer),
|
|
245
|
-
{ discard: true }
|
|
246
|
-
)
|
|
207
|
+
})
|
|
247
208
|
|
|
248
209
|
// The handlers' builds run in the chat's working scope: what they set up lives until the chat closes or is deleted.
|
|
249
210
|
const handler = yield* inWorking(Effect.provideService(registration.open(handle), CurrentChat, handle))
|
|
@@ -271,7 +232,7 @@ export const make = Effect.fnUntraced(function* (options: {
|
|
|
271
232
|
|
|
272
233
|
const drain: Effect.Effect<void> = Semaphore.withPermit(turnsLock)(
|
|
273
234
|
Effect.gen(function* () {
|
|
274
|
-
while (!(yield* isDeleted) && History.needsTurn(yield* chat.entries
|
|
235
|
+
while (!(yield* isDeleted) && History.needsTurn(yield* chat.entries)) {
|
|
275
236
|
yield* Turns.run({ registration, handler, chat, services, checkOpen: Effect.asVoid(checkAvailable) })
|
|
276
237
|
}
|
|
277
238
|
})
|
|
@@ -293,16 +254,6 @@ export const make = Effect.fnUntraced(function* (options: {
|
|
|
293
254
|
|
|
294
255
|
const services = Context.make(CurrentChat, handle).pipe(
|
|
295
256
|
Context.add(SpecialistService.Runner, runner),
|
|
296
|
-
Context.add(
|
|
297
|
-
Decisions.TurnDecisions,
|
|
298
|
-
Option.some({
|
|
299
|
-
approved: Effect.gen(function* () {
|
|
300
|
-
const all = yield* chat.entries
|
|
301
|
-
|
|
302
|
-
return Decisions.approvedCalls(all)
|
|
303
|
-
})
|
|
304
|
-
})
|
|
305
|
-
),
|
|
306
257
|
Context.merge(registration.model)
|
|
307
258
|
)
|
|
308
259
|
|
|
@@ -345,7 +296,7 @@ export const make = Effect.fnUntraced(function* (options: {
|
|
|
345
296
|
yield* inBackground(drain)
|
|
346
297
|
}),
|
|
347
298
|
resume: Effect.gen(function* () {
|
|
348
|
-
if (History.needsTurn(yield* chat.entries
|
|
299
|
+
if (History.needsTurn(yield* chat.entries)) yield* inBackground(drain)
|
|
349
300
|
}),
|
|
350
301
|
stream: (after) =>
|
|
351
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
|
@@ -5,7 +5,6 @@ import * as Option from "effect/Option"
|
|
|
5
5
|
import * as Predicate from "effect/Predicate"
|
|
6
6
|
import * as Schema from "effect/Schema"
|
|
7
7
|
import * as AssistantEvent from "../AssistantEvent.ts"
|
|
8
|
-
import * as Decision from "../Decision.ts"
|
|
9
8
|
import { toJson, toJsonText } from "./schema.ts"
|
|
10
9
|
import * as Uuid from "./uuid.ts"
|
|
11
10
|
|
|
@@ -108,73 +107,23 @@ const finishedText = (
|
|
|
108
107
|
const answerText = (answer: AssistantEvent.BlockAnswer) =>
|
|
109
108
|
`<answer message="${answer.messageId}" kind="${answer.kind}">\nThe user answered with the block on your message ${answer.messageId}.\n${toJsonText(answer.value)}\n</answer>`
|
|
110
109
|
|
|
111
|
-
/** @internal */
|
|
112
|
-
export const parkedResult = (
|
|
113
|
-
request: AssistantEvent.DecisionRequested,
|
|
114
|
-
origin: AssistantEvent.CallOrigin
|
|
115
|
-
): Prompt.ToolMessage =>
|
|
116
|
-
Prompt.toolMessage({
|
|
117
|
-
content: [
|
|
118
|
-
Prompt.toolResultPart({
|
|
119
|
-
id: origin.callId,
|
|
120
|
-
name: origin.tool,
|
|
121
|
-
isFailure: false,
|
|
122
|
-
result: {
|
|
123
|
-
status: "waiting_for_decision",
|
|
124
|
-
decisionId: request.id,
|
|
125
|
-
next: "Not run: it runs only if the user approves it. Send them a message with a decision block for this decisionId that says what they approve (the call's key facts). Their answer runs it, or declines it."
|
|
126
|
-
},
|
|
127
|
-
providerExecuted: false
|
|
128
|
-
})
|
|
129
|
-
]
|
|
130
|
-
})
|
|
131
|
-
|
|
132
|
-
const whatDecided = (request: AssistantEvent.DecisionRequested, outcome: AssistantEvent.DecisionOutcome) =>
|
|
133
|
-
outcome === "approved"
|
|
134
|
-
? `approved: ${request.origin.tool} runs now, and its result follows.`
|
|
135
|
-
: `declined: ${request.origin.tool} didn't run, and won't.`
|
|
136
|
-
|
|
137
|
-
const decodeDecisionAnswer = Schema.decodeUnknownOption(Decision.Answer)
|
|
138
|
-
|
|
139
|
-
const decisionAnswerText = (
|
|
140
|
-
answer: AssistantEvent.BlockAnswer,
|
|
141
|
-
requests: ReadonlyMap<string, AssistantEvent.DecisionRequested>
|
|
142
|
-
) =>
|
|
143
|
-
Option.match(decodeDecisionAnswer(answer.value), {
|
|
144
|
-
onNone: () => answerText(answer),
|
|
145
|
-
onSome: ({ decisions }) => {
|
|
146
|
-
const lines = Object.entries(decisions).map(([id, outcome]) => {
|
|
147
|
-
const request = requests.get(id)
|
|
148
|
-
|
|
149
|
-
return `- ${id}: ${request === undefined ? outcome : whatDecided(request, outcome)}`
|
|
150
|
-
})
|
|
151
|
-
|
|
152
|
-
return `<answer message="${answer.messageId}" kind="${answer.kind}">\nThe user decided with the block on your message ${answer.messageId}:\n${lines.join("\n")}\n</answer>`
|
|
153
|
-
}
|
|
154
|
-
})
|
|
155
|
-
|
|
156
110
|
const resultText = (result: AssistantEvent.ToolResult) =>
|
|
157
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>`
|
|
158
112
|
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
tool: string,
|
|
162
|
-
result: AssistantEvent.ToolResult
|
|
163
|
-
) =>
|
|
164
|
-
`<decision id="${request.id}" tool="${tool}" outcome="approved">\nThe user approved it, and ${tool} ran: ${result.outcome}.\n${toJsonText(result.result)}\n</decision>`
|
|
113
|
+
const isEntry = (item: AssistantEvent.AnyEntry | Prompt.Message): item is AssistantEvent.AnyEntry =>
|
|
114
|
+
Predicate.hasProperty(item, "seq")
|
|
165
115
|
|
|
166
|
-
// In the order the model saw the entries, not the order they were stored.
|
|
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.
|
|
167
119
|
/** @internal */
|
|
168
120
|
export const fromEntries = (
|
|
169
|
-
|
|
170
|
-
readApp: (event: { readonly _tag: string }) => string | undefined
|
|
121
|
+
items: ReadonlyArray<AssistantEvent.AnyEntry | Prompt.Message>
|
|
171
122
|
): ReadonlyArray<Prompt.Message> => {
|
|
123
|
+
const entries = items.filter(isEntry)
|
|
172
124
|
const history: Array<Prompt.Message> = []
|
|
173
125
|
const held: Array<Prompt.Message> = []
|
|
174
126
|
const open = new Set<string>()
|
|
175
|
-
const requests = new Map<string, AssistantEvent.DecisionRequested>()
|
|
176
|
-
// Parked calls by call id: their result came with the parking, so a later one reads as the decision's.
|
|
177
|
-
const parked = new Map<string, AssistantEvent.DecisionRequested>()
|
|
178
127
|
const called = new Set<string>()
|
|
179
128
|
|
|
180
129
|
const flushIfClosed = () => {
|
|
@@ -189,31 +138,35 @@ export const fromEntries = (
|
|
|
189
138
|
: undefined
|
|
190
139
|
}
|
|
191
140
|
|
|
192
|
-
|
|
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) => {
|
|
193
143
|
const seen = nextSeen(index)
|
|
194
144
|
|
|
195
|
-
if (open.size > 0 || (seen !== undefined && seen <
|
|
145
|
+
if (open.size > 0 || (seen !== undefined && seen < seq)) held.push(item)
|
|
196
146
|
else history.push(item)
|
|
197
147
|
}
|
|
198
148
|
|
|
199
|
-
|
|
200
|
-
if (!AssistantEvent.isLibraryEvent(event)) {
|
|
201
|
-
const text = readApp(event)
|
|
149
|
+
let index = -1
|
|
202
150
|
|
|
203
|
-
|
|
151
|
+
for (const item of items) {
|
|
152
|
+
if (!isEntry(item)) {
|
|
153
|
+
input(index, (entries[index]?.seq ?? 0) + 0.5, item)
|
|
204
154
|
|
|
205
|
-
|
|
155
|
+
continue
|
|
206
156
|
}
|
|
207
157
|
|
|
158
|
+
index += 1
|
|
159
|
+
|
|
160
|
+
const { event, seq } = item
|
|
161
|
+
const at = index
|
|
162
|
+
|
|
163
|
+
if (!AssistantEvent.isLibraryEvent(event)) continue
|
|
164
|
+
|
|
208
165
|
Match.value(event).pipe(
|
|
209
166
|
Match.tagsExhaustive({
|
|
210
167
|
UserMessage: (message) =>
|
|
211
|
-
input(
|
|
212
|
-
BlockAnswer: (answer) =>
|
|
213
|
-
input(
|
|
214
|
-
index,
|
|
215
|
-
userText(answer.kind === Decision.kind ? decisionAnswerText(answer, requests) : answerText(answer))
|
|
216
|
-
),
|
|
168
|
+
input(at, seq, userText(AssistantEvent.textWithAttachments(message.text, message.attachments))),
|
|
169
|
+
BlockAnswer: (answer) => input(at, seq, userText(answerText(answer))),
|
|
217
170
|
ModelOutput: (output) => {
|
|
218
171
|
history.push(...Prompt.fromResponseParts(output.parts).content)
|
|
219
172
|
|
|
@@ -227,46 +180,34 @@ export const fromEntries = (
|
|
|
227
180
|
flushIfClosed()
|
|
228
181
|
},
|
|
229
182
|
ToolResult: (result) => {
|
|
230
|
-
const request = parked.get(result.callId)
|
|
231
|
-
|
|
232
|
-
if (request !== undefined) return input(index, userText(approvedResultText(request, result.tool, result)))
|
|
233
|
-
|
|
234
183
|
// A view that starts after the call (since a summary, say) keeps its result as text: a tool result needs its
|
|
235
|
-
// call right before it.
|
|
236
|
-
if (!called.has(result.callId)) return input(
|
|
184
|
+
// call right before it.
|
|
185
|
+
if (!called.has(result.callId)) return input(at, seq, userText(resultText(result)))
|
|
237
186
|
|
|
238
187
|
history.push(toolMessage(result))
|
|
239
188
|
open.delete(result.callId)
|
|
240
189
|
flushIfClosed()
|
|
241
190
|
},
|
|
242
|
-
DecisionRequested: (request) => {
|
|
243
|
-
requests.set(request.id, request)
|
|
244
|
-
parked.set(request.origin.callId, request)
|
|
245
|
-
history.push(parkedResult(request, request.origin))
|
|
246
|
-
open.delete(request.origin.callId)
|
|
247
|
-
flushIfClosed()
|
|
248
|
-
},
|
|
249
|
-
DecisionResolved: () => undefined,
|
|
250
191
|
AssistantMessage: () => undefined,
|
|
251
192
|
ToolStarted: () => undefined,
|
|
252
193
|
TurnStarted: () => undefined,
|
|
253
194
|
TurnCompleted: () => undefined,
|
|
254
195
|
TurnFailed: () => undefined,
|
|
255
|
-
SpecialistFinished: (finished) => input(
|
|
196
|
+
SpecialistFinished: (finished) => input(at, seq, userText(finishedText(entries, at, finished))),
|
|
256
197
|
// Every run starts in a call of the model's own (a spawn or a follow-up), whose result says so.
|
|
257
198
|
SpecialistStarted: () => undefined,
|
|
258
199
|
BlocksClosed: () => undefined,
|
|
259
200
|
ChatDeleted: () => undefined
|
|
260
201
|
})
|
|
261
202
|
)
|
|
262
|
-
}
|
|
203
|
+
}
|
|
263
204
|
|
|
264
205
|
return [...history, ...held]
|
|
265
206
|
}
|
|
266
207
|
|
|
267
208
|
/** @internal */
|
|
268
|
-
export const isInput = (event: { readonly _tag: string }
|
|
269
|
-
AssistantEvent.isInput(event) ||
|
|
209
|
+
export const isInput = (event: { readonly _tag: string }): boolean =>
|
|
210
|
+
AssistantEvent.isInput(event) || AssistantEvent.isAppInput(event)
|
|
270
211
|
|
|
271
212
|
/** @internal */
|
|
272
213
|
export const handledThrough = (entries: ReadonlyArray<AssistantEvent.AnyEntry>): number => {
|
|
@@ -278,21 +219,18 @@ export const handledThrough = (entries: ReadonlyArray<AssistantEvent.AnyEntry>):
|
|
|
278
219
|
// A run the assistant stopped isn't an input here: it wakes nothing.
|
|
279
220
|
/** @internal */
|
|
280
221
|
export const unhandledInputs = (
|
|
281
|
-
entries: ReadonlyArray<AssistantEvent.AnyEntry
|
|
282
|
-
wakes: ReadonlySet<string>
|
|
222
|
+
entries: ReadonlyArray<AssistantEvent.AnyEntry>
|
|
283
223
|
): ReadonlyArray<{ readonly _tag: string }> => {
|
|
284
224
|
const seen = handledThrough(entries)
|
|
285
225
|
|
|
286
226
|
return entries.flatMap(({ event, seq }) =>
|
|
287
|
-
seq > seen && isInput(event
|
|
288
|
-
? [event]
|
|
289
|
-
: []
|
|
227
|
+
seq > seen && isInput(event) && !(AssistantEvent.isInput(event) && !AssistantEvent.isWaking(event)) ? [event] : []
|
|
290
228
|
)
|
|
291
229
|
}
|
|
292
230
|
|
|
293
231
|
/** @internal */
|
|
294
|
-
export const needsTurn = (entries: ReadonlyArray<AssistantEvent.AnyEntry
|
|
295
|
-
unhandledInputs(entries
|
|
232
|
+
export const needsTurn = (entries: ReadonlyArray<AssistantEvent.AnyEntry>): boolean =>
|
|
233
|
+
unhandledInputs(entries).length > 0
|
|
296
234
|
|
|
297
235
|
// A failure the user was shown counts as a reply.
|
|
298
236
|
/** @internal */
|
|
@@ -359,8 +297,5 @@ export const owesDelivery = (entries: ReadonlyArray<AssistantEvent.AnyEntry>, th
|
|
|
359
297
|
}
|
|
360
298
|
|
|
361
299
|
/** @internal */
|
|
362
|
-
export const unreadAfter = (
|
|
363
|
-
entries
|
|
364
|
-
throughSeq: number,
|
|
365
|
-
wakes: ReadonlySet<string>
|
|
366
|
-
): 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))
|