effect-assistant 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Assistant.d.ts +10 -10
- package/dist/Assistant.d.ts.map +1 -1
- package/dist/Assistant.js +1 -1
- package/dist/Assistant.js.map +1 -1
- package/dist/AssistantActivity.d.ts +2 -2
- package/dist/AssistantActivity.d.ts.map +1 -1
- package/dist/AssistantActivity.js +5 -5
- package/dist/AssistantActivity.js.map +1 -1
- package/dist/AssistantEngine.d.ts +3 -3
- package/dist/AssistantEngine.js +2 -2
- package/dist/AssistantEngine.js.map +1 -1
- package/dist/AssistantEvent.d.ts +19 -8
- package/dist/AssistantEvent.d.ts.map +1 -1
- package/dist/AssistantEvent.js +20 -9
- package/dist/AssistantEvent.js.map +1 -1
- package/dist/AssistantLoop.d.ts +2 -2
- package/dist/AssistantLoop.js.map +1 -1
- package/dist/Block.d.ts +63 -1
- package/dist/Block.d.ts.map +1 -1
- package/dist/Block.js +63 -2
- package/dist/Block.js.map +1 -1
- package/dist/BlockKit.d.ts +25 -3
- package/dist/BlockKit.d.ts.map +1 -1
- package/dist/BlockKit.js +14 -12
- package/dist/BlockKit.js.map +1 -1
- package/dist/ChatLog.d.ts +9 -9
- package/dist/ChatLog.d.ts.map +1 -1
- package/dist/ChatLog.js +21 -21
- package/dist/ChatLog.js.map +1 -1
- package/dist/Compaction.d.ts +10 -10
- package/dist/Compaction.d.ts.map +1 -1
- package/dist/Compaction.js +19 -19
- package/dist/Compaction.js.map +1 -1
- package/dist/Memory.d.ts +1 -1
- package/dist/Memory.js +1 -1
- package/dist/Memory.js.map +1 -1
- package/dist/Skill.d.ts +1 -1
- package/dist/Skill.js +1 -1
- package/dist/Skill.js.map +1 -1
- package/dist/Specialist.d.ts +1 -1
- package/dist/Specialist.js +1 -1
- package/dist/Specialist.js.map +1 -1
- package/dist/Timeline.d.ts +13 -9
- package/dist/Timeline.d.ts.map +1 -1
- package/dist/Timeline.js +35 -23
- package/dist/Timeline.js.map +1 -1
- package/dist/ToolApproval.d.ts +1 -1
- package/dist/ToolApproval.js +1 -1
- package/dist/ToolApproval.js.map +1 -1
- package/dist/ToolDiscovery.d.ts +1 -1
- package/dist/ToolDiscovery.js +1 -1
- package/dist/ToolDiscovery.js.map +1 -1
- package/dist/ToolResult.d.ts +56 -0
- package/dist/ToolResult.d.ts.map +1 -0
- package/dist/ToolResult.js +49 -0
- package/dist/ToolResult.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +4 -3
- package/dist/internal/chat.js +4 -4
- package/dist/internal/chat.js.map +1 -1
- package/dist/internal/events.js +1 -1
- package/dist/internal/events.js.map +1 -1
- package/dist/internal/history.js +23 -23
- package/dist/internal/history.js.map +1 -1
- package/dist/internal/loop.d.ts +1 -1
- package/dist/internal/loop.d.ts.map +1 -1
- package/dist/internal/loop.js +12 -6
- package/dist/internal/loop.js.map +1 -1
- package/dist/internal/toolkit.d.ts +7 -1
- package/dist/internal/toolkit.d.ts.map +1 -1
- package/dist/internal/toolkit.js +47 -4
- package/dist/internal/toolkit.js.map +1 -1
- package/dist/internal/tools.js +39 -6
- package/dist/internal/tools.js.map +1 -1
- package/dist/internal/turns.d.ts.map +1 -1
- package/dist/internal/turns.js +24 -21
- package/dist/internal/turns.js.map +1 -1
- package/package.json +1 -1
- package/src/Assistant.ts +11 -11
- package/src/AssistantActivity.ts +9 -7
- package/src/AssistantEngine.ts +3 -3
- package/src/AssistantEvent.ts +21 -10
- package/src/AssistantLoop.ts +3 -3
- package/src/Block.ts +82 -1
- package/src/BlockKit.ts +37 -4
- package/src/ChatLog.ts +31 -26
- package/src/Compaction.ts +21 -21
- package/src/Memory.ts +1 -1
- package/src/Skill.ts +1 -1
- package/src/Specialist.ts +1 -1
- package/src/Timeline.ts +38 -25
- package/src/ToolApproval.ts +1 -1
- package/src/ToolDiscovery.ts +1 -1
- package/src/ToolResult.ts +80 -0
- package/src/index.ts +5 -0
- package/src/internal/chat.ts +4 -4
- package/src/internal/events.ts +1 -1
- package/src/internal/history.ts +32 -28
- package/src/internal/loop.ts +24 -7
- package/src/internal/toolkit.ts +55 -5
- package/src/internal/tools.ts +81 -5
- package/src/internal/turns.ts +35 -29
package/src/Assistant.ts
CHANGED
|
@@ -48,12 +48,12 @@ const TypeId = "~effect-assistant/Assistant" as const
|
|
|
48
48
|
export type TurnServices = CurrentChat | Specialist.Runner
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
|
-
* Represents the options accepted by `turn.
|
|
51
|
+
* Represents the options accepted by `turn.run`.
|
|
52
52
|
*
|
|
53
53
|
* @category models
|
|
54
54
|
* @since 0.1.0
|
|
55
55
|
*/
|
|
56
|
-
export interface
|
|
56
|
+
export interface RunOptions<T extends Record<string, Tool.Any> = Record<never, never>, Events = never> {
|
|
57
57
|
/** The assistant's role and rules. The loop places `Instructions.protocol` before them. */
|
|
58
58
|
readonly instructions: string | Instructions.Resolved
|
|
59
59
|
/**
|
|
@@ -115,7 +115,7 @@ export interface LoopOptions<T extends Record<string, Tool.Any> = Record<never,
|
|
|
115
115
|
*
|
|
116
116
|
* // An `OrderPlaced` that the API submits wakes a turn, which reads it as this note.
|
|
117
117
|
* const MyAssistantLive = MyAssistant.toLayer((turn) =>
|
|
118
|
-
* turn.
|
|
118
|
+
* turn.run({ instructions: "Be brief.", history: withOrders })
|
|
119
119
|
* )
|
|
120
120
|
* ```
|
|
121
121
|
*
|
|
@@ -134,7 +134,7 @@ export type HistoryView<Events = never> = (
|
|
|
134
134
|
*/
|
|
135
135
|
export interface Entry<Events = never> {
|
|
136
136
|
readonly chatId: ChatId
|
|
137
|
-
readonly
|
|
137
|
+
readonly sequence: number
|
|
138
138
|
readonly at: number
|
|
139
139
|
readonly event: AssistantEvent.AssistantEvent | Events
|
|
140
140
|
}
|
|
@@ -146,7 +146,7 @@ export interface Entry<Events = never> {
|
|
|
146
146
|
* @since 0.1.0
|
|
147
147
|
*/
|
|
148
148
|
export interface ReplyOptions {
|
|
149
|
-
/** The assistant's instructions, as passed to `turn.
|
|
149
|
+
/** The assistant's instructions, as passed to `turn.run`. `Instructions.protocol` is added. */
|
|
150
150
|
readonly instructions: string | Instructions.Resolved
|
|
151
151
|
/**
|
|
152
152
|
* The message given to the model after the history. Defaults to a message saying that it
|
|
@@ -185,8 +185,8 @@ export interface Turn<Events = never> {
|
|
|
185
185
|
* tools, and the instructions and tools in `options`. Every step is a tool call, and
|
|
186
186
|
* the loop ends with `replyIfOwed`.
|
|
187
187
|
*/
|
|
188
|
-
readonly
|
|
189
|
-
options:
|
|
188
|
+
readonly run: <T extends Record<string, Tool.Any> = Record<never, never>>(
|
|
189
|
+
options: RunOptions<T, Events>
|
|
190
190
|
) => Stream.Stream<
|
|
191
191
|
AssistantLoop.Event,
|
|
192
192
|
AiError.AiError,
|
|
@@ -197,7 +197,7 @@ export interface Turn<Events = never> {
|
|
|
197
197
|
* message. In this pass the model can only reply.
|
|
198
198
|
*
|
|
199
199
|
* The stream is empty when no reply is owed, or when an input arrived that the next turn will
|
|
200
|
-
* read. `
|
|
200
|
+
* read. `run` runs this pass with its own prompt and tools, so the pass reuses the cached
|
|
201
201
|
* prompt.
|
|
202
202
|
*/
|
|
203
203
|
readonly replyIfOwed: (
|
|
@@ -309,7 +309,7 @@ export interface Service<Events = never> {
|
|
|
309
309
|
/**
|
|
310
310
|
* Streams the chat's stored entries, then live ones, with the assistant's own events decoded.
|
|
311
311
|
* A reader that already has the earlier entries passes `after` and receives only the entries
|
|
312
|
-
* after that `
|
|
312
|
+
* after that `sequence`.
|
|
313
313
|
*
|
|
314
314
|
* For a chat client, pipe the whole log into `Timeline.changes(MyAssistant, { after })`, which
|
|
315
315
|
* streams the chat's timeline, as its user sees it. Its `after` goes there, not here: a message
|
|
@@ -382,7 +382,7 @@ export interface Assistant<
|
|
|
382
382
|
* export const MyAssistant = Assistant.make("my-assistant")
|
|
383
383
|
*
|
|
384
384
|
* const MyAssistantLive = MyAssistant.toLayer((turn) =>
|
|
385
|
-
* turn.
|
|
385
|
+
* turn.run({ instructions: "You help a small team. Be brief." })
|
|
386
386
|
* )
|
|
387
387
|
*
|
|
388
388
|
* const AppLive = MyAssistantLive.pipe(
|
|
@@ -458,7 +458,7 @@ function entrySchema(events: ReadonlyArray<AssistantEvent.AppEventSchema>): Sche
|
|
|
458
458
|
return Schema.toCodecJson(
|
|
459
459
|
Schema.Struct({
|
|
460
460
|
chatId: ChatId,
|
|
461
|
-
|
|
461
|
+
sequence: Schema.Int,
|
|
462
462
|
at: Schema.Finite,
|
|
463
463
|
event: Schema.Union([AssistantEvent.AssistantEvent, ...events])
|
|
464
464
|
})
|
package/src/AssistantActivity.ts
CHANGED
|
@@ -43,8 +43,8 @@ export interface State {
|
|
|
43
43
|
* to start.
|
|
44
44
|
*/
|
|
45
45
|
readonly queued: boolean
|
|
46
|
-
/** The `
|
|
47
|
-
readonly
|
|
46
|
+
/** The `sequence` of the newest input. */
|
|
47
|
+
readonly lastInputSequence: number
|
|
48
48
|
/** Specialist runs that are working, by run id. */
|
|
49
49
|
readonly specialists: ReadonlyMap<RunId, SpecialistRun>
|
|
50
50
|
}
|
|
@@ -58,7 +58,7 @@ export interface State {
|
|
|
58
58
|
export const empty: State = {
|
|
59
59
|
turnSince: undefined,
|
|
60
60
|
queued: false,
|
|
61
|
-
|
|
61
|
+
lastInputSequence: 0,
|
|
62
62
|
specialists: new Map()
|
|
63
63
|
}
|
|
64
64
|
|
|
@@ -89,14 +89,16 @@ export const apply = (activity: State, entry: AssistantEvent.AnyEntry): State =>
|
|
|
89
89
|
const event = entry.event
|
|
90
90
|
|
|
91
91
|
if (!AssistantEvent.isLibraryEvent(event)) {
|
|
92
|
-
return AssistantEvent.isAppInput(event)
|
|
92
|
+
return AssistantEvent.isAppInput(event)
|
|
93
|
+
? { ...activity, queued: true, lastInputSequence: entry.sequence }
|
|
94
|
+
: activity
|
|
93
95
|
}
|
|
94
96
|
|
|
95
97
|
if (AssistantEvent.isInput(event)) {
|
|
96
98
|
// A finished run's report is an input, and ends the run.
|
|
97
99
|
const current = Predicate.isTagged(event, "SpecialistFinished") ? withoutRun(activity, event.runId) : activity
|
|
98
100
|
|
|
99
|
-
return AssistantEvent.isWaking(event) ? { ...current, queued: true,
|
|
101
|
+
return AssistantEvent.isWaking(event) ? { ...current, queued: true, lastInputSequence: entry.sequence } : current
|
|
100
102
|
}
|
|
101
103
|
|
|
102
104
|
return Match.value(event).pipe(
|
|
@@ -105,7 +107,7 @@ export const apply = (activity: State, entry: AssistantEvent.AnyEntry): State =>
|
|
|
105
107
|
Match.tag("TurnCompleted", "TurnFailed", (ended): State => ({
|
|
106
108
|
...activity,
|
|
107
109
|
turnSince: undefined,
|
|
108
|
-
queued: activity.
|
|
110
|
+
queued: activity.lastInputSequence > ended.throughSequence
|
|
109
111
|
})),
|
|
110
112
|
Match.tag("SpecialistStarted", (started): State => ({
|
|
111
113
|
...activity,
|
|
@@ -117,7 +119,7 @@ export const apply = (activity: State, entry: AssistantEvent.AnyEntry): State =>
|
|
|
117
119
|
})
|
|
118
120
|
})),
|
|
119
121
|
// Deleting a chat stops everything in it, and nothing starts again.
|
|
120
|
-
Match.tag("ChatDeleted", (): State => ({ ...empty,
|
|
122
|
+
Match.tag("ChatDeleted", (): State => ({ ...empty, lastInputSequence: activity.lastInputSequence })),
|
|
121
123
|
Match.orElse(() => activity)
|
|
122
124
|
)
|
|
123
125
|
}
|
package/src/AssistantEngine.ts
CHANGED
|
@@ -54,7 +54,7 @@ export class AssistantEngine extends Context.Service<
|
|
|
54
54
|
chatId: ChatId,
|
|
55
55
|
input: AssistantEvent.Input | AssistantEvent.StoredAppEvent
|
|
56
56
|
) => Effect.Effect<void>
|
|
57
|
-
/** Streams the chat's stored entries after the `after`
|
|
57
|
+
/** Streams the chat's stored entries after the `after` sequence, then live ones. */
|
|
58
58
|
readonly subscribe: (assistant: string, chatId: ChatId, after: number) => Stream.Stream<AssistantEvent.Entry>
|
|
59
59
|
/**
|
|
60
60
|
* Deletes one of the assistant's chats. The chat's log records the deletion, and everything
|
|
@@ -90,7 +90,7 @@ const notRegistered = (assistant: string) =>
|
|
|
90
90
|
*
|
|
91
91
|
* const MyAssistant = Assistant.make("my-assistant")
|
|
92
92
|
*
|
|
93
|
-
* const AppLive = MyAssistant.toLayer((turn) => turn.
|
|
93
|
+
* const AppLive = MyAssistant.toLayer((turn) => turn.run({ instructions: "Be brief." })).pipe(
|
|
94
94
|
* Layer.provide(ModelLive),
|
|
95
95
|
* Layer.provideMerge(AssistantEngine.layerMemory),
|
|
96
96
|
* Layer.provideMerge(ChatLog.layerMemory)
|
|
@@ -289,7 +289,7 @@ const makeClusterClient = Effect.gen(function* () {
|
|
|
289
289
|
*
|
|
290
290
|
* const MyAssistant = Assistant.make("my-assistant")
|
|
291
291
|
*
|
|
292
|
-
* const AppLive = MyAssistant.toLayer((turn) => turn.
|
|
292
|
+
* const AppLive = MyAssistant.toLayer((turn) => turn.run({ instructions: "Be brief." })).pipe(
|
|
293
293
|
* Layer.provide(ModelLive),
|
|
294
294
|
* Layer.provideMerge(AssistantEngine.layerCluster),
|
|
295
295
|
* Layer.provideMerge(
|
package/src/AssistantEvent.ts
CHANGED
|
@@ -197,12 +197,16 @@ export type UserInput = typeof UserInput.Type
|
|
|
197
197
|
* Represents the UI shown with a message, as a block kind from the assistant's `BlockKit` and its
|
|
198
198
|
* props.
|
|
199
199
|
*
|
|
200
|
+
* A block that shows a tool's result (`Block.fromToolResult`) has the tool's name as its kind and
|
|
201
|
+
* the result's `resultId`.
|
|
202
|
+
*
|
|
200
203
|
* @category schemas
|
|
201
204
|
* @since 0.1.0
|
|
202
205
|
*/
|
|
203
206
|
export class MessageBlock extends Schema.Class<MessageBlock>("effect-assistant/MessageBlock")({
|
|
204
207
|
kind: Schema.String,
|
|
205
|
-
props: Schema.Unknown
|
|
208
|
+
props: Schema.Unknown,
|
|
209
|
+
resultId: Schema.optional(Schema.String)
|
|
206
210
|
}) {}
|
|
207
211
|
|
|
208
212
|
/**
|
|
@@ -276,7 +280,7 @@ export class ModelOutput extends Schema.TaggedClass<ModelOutput>()("ModelOutput"
|
|
|
276
280
|
* arrived while the model was working. A turn has handled what its
|
|
277
281
|
* outputs had read.
|
|
278
282
|
*/
|
|
279
|
-
|
|
283
|
+
seenThroughSequence: Schema.optional(Schema.Int),
|
|
280
284
|
/** The model that wrote it, as the provider's id for it or the name the context gave it. */
|
|
281
285
|
model: Schema.optional(Schema.String),
|
|
282
286
|
/** The provider's id for the response, when it gives one. */
|
|
@@ -325,6 +329,10 @@ export type ToolOutcome = typeof ToolOutcome.Type
|
|
|
325
329
|
* failure includes its reason when it has one, which is a `ToolFailure`'s `reason` or the
|
|
326
330
|
* failure's tag.
|
|
327
331
|
*
|
|
332
|
+
* When the model reads less than the whole success (a `ToolResult.modelView`, or the `resultId`
|
|
333
|
+
* added for a tool the assistant's blocks show), `full` holds the whole success, encoded by the
|
|
334
|
+
* tool's success schema.
|
|
335
|
+
*
|
|
328
336
|
* @category schemas
|
|
329
337
|
* @since 0.1.0
|
|
330
338
|
*/
|
|
@@ -333,7 +341,10 @@ export class ToolResult extends Schema.TaggedClass<ToolResult>()("ToolResult", {
|
|
|
333
341
|
tool: Schema.String,
|
|
334
342
|
outcome: ToolOutcome,
|
|
335
343
|
result: Schema.Json,
|
|
336
|
-
reason: Schema.optional(Schema.String)
|
|
344
|
+
reason: Schema.optional(Schema.String),
|
|
345
|
+
full: Schema.optional(Schema.Json),
|
|
346
|
+
/** The id the model passes in a `tool_result` block to show this result. */
|
|
347
|
+
resultId: Schema.optional(Schema.String)
|
|
337
348
|
}) {}
|
|
338
349
|
|
|
339
350
|
/**
|
|
@@ -354,26 +365,26 @@ export class TurnStarted extends Schema.TaggedClass<TurnStarted>()("TurnStarted"
|
|
|
354
365
|
/**
|
|
355
366
|
* Represents a turn that finished.
|
|
356
367
|
*
|
|
357
|
-
* `
|
|
368
|
+
* `throughSequence` is the last entry the turn read. Inputs after it still need a turn.
|
|
358
369
|
*
|
|
359
370
|
* @category schemas
|
|
360
371
|
* @since 0.1.0
|
|
361
372
|
*/
|
|
362
373
|
export class TurnCompleted extends Schema.TaggedClass<TurnCompleted>()("TurnCompleted", {
|
|
363
|
-
|
|
374
|
+
throughSequence: Schema.Int
|
|
364
375
|
}) {}
|
|
365
376
|
|
|
366
377
|
/**
|
|
367
378
|
* Represents a turn that failed.
|
|
368
379
|
*
|
|
369
|
-
* `
|
|
380
|
+
* `throughSequence` covers everything stored when the turn failed, so a failing input isn't retried
|
|
370
381
|
* forever.
|
|
371
382
|
*
|
|
372
383
|
* @category schemas
|
|
373
384
|
* @since 0.1.0
|
|
374
385
|
*/
|
|
375
386
|
export class TurnFailed extends Schema.TaggedClass<TurnFailed>()("TurnFailed", {
|
|
376
|
-
|
|
387
|
+
throughSequence: Schema.Int,
|
|
377
388
|
message: Schema.String
|
|
378
389
|
}) {}
|
|
379
390
|
|
|
@@ -574,7 +585,7 @@ export const isAppInput = (event: { readonly _tag: string }): boolean =>
|
|
|
574
585
|
*/
|
|
575
586
|
export interface AnyEntry {
|
|
576
587
|
readonly chatId: ChatId
|
|
577
|
-
readonly
|
|
588
|
+
readonly sequence: number
|
|
578
589
|
readonly at: number
|
|
579
590
|
readonly event: { readonly _tag: string }
|
|
580
591
|
}
|
|
@@ -582,7 +593,7 @@ export interface AnyEntry {
|
|
|
582
593
|
/**
|
|
583
594
|
* Schema for a stored event with its position in the chat.
|
|
584
595
|
*
|
|
585
|
-
* `
|
|
596
|
+
* `sequence` starts at 1 and has no gaps. `at` is the time the event was stored, in epoch
|
|
586
597
|
* milliseconds.
|
|
587
598
|
*
|
|
588
599
|
* @category schemas
|
|
@@ -590,7 +601,7 @@ export interface AnyEntry {
|
|
|
590
601
|
*/
|
|
591
602
|
export const Entry = Schema.Struct({
|
|
592
603
|
chatId: ChatId,
|
|
593
|
-
|
|
604
|
+
sequence: Schema.Int,
|
|
594
605
|
at: Schema.Finite,
|
|
595
606
|
event: Schema.Union([AssistantEvent, StoredAppEvent])
|
|
596
607
|
})
|
package/src/AssistantLoop.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Runs the agent loop, which calls the model, runs its tool calls, and repeats.
|
|
3
3
|
*
|
|
4
|
-
* Most handlers use `turn.
|
|
4
|
+
* Most handlers use `turn.run` or `run.loop`. This module is for a loop of your own: {@link run}
|
|
5
5
|
* runs the loop until the model is done, and {@link step} is one step, as a body for
|
|
6
6
|
* {@link loop}.
|
|
7
7
|
*
|
|
@@ -37,7 +37,7 @@ export type Event = Loop.Event
|
|
|
37
37
|
/**
|
|
38
38
|
* Represents a chat's history as the model reads it.
|
|
39
39
|
*
|
|
40
|
-
* It holds the messages, and the last `
|
|
40
|
+
* It holds the messages, and the last `sequence` of the chat they cover.
|
|
41
41
|
*
|
|
42
42
|
* @category models
|
|
43
43
|
* @since 0.1.0
|
|
@@ -122,7 +122,7 @@ export interface StepOptions<T extends Record<string, Tool.Any> = NoTools> {
|
|
|
122
122
|
readonly toolkit?: Toolkit.Toolkit<T> | undefined
|
|
123
123
|
/**
|
|
124
124
|
* The current history, read at the start of the step. It is either messages, or a chat's
|
|
125
|
-
* history (`turn.history()`), whose `
|
|
125
|
+
* history (`turn.history()`), whose `throughSequence` the step's `ModelOutput` records as how far
|
|
126
126
|
* the model had read.
|
|
127
127
|
*/
|
|
128
128
|
readonly history: Effect.Effect<ReadonlyArray<Prompt.Message> | ChatHistory>
|
package/src/Block.ts
CHANGED
|
@@ -5,9 +5,14 @@
|
|
|
5
5
|
* pick from, a yes or no, or a small input. The assistant fills in its `props`, and
|
|
6
6
|
* using the block sends back a value typed by its `answer` schema.
|
|
7
7
|
*
|
|
8
|
+
* A block made with {@link fromToolResult} shows a tool's result instead: the assistant picks the
|
|
9
|
+
* result, and the client draws it from what the tool returned.
|
|
10
|
+
*
|
|
8
11
|
* @since 0.1.0
|
|
9
12
|
*/
|
|
10
|
-
import type * as
|
|
13
|
+
import type * as Tool from "effect/ai/Tool"
|
|
14
|
+
import * as Predicate from "effect/Predicate"
|
|
15
|
+
import * as Schema from "effect/Schema"
|
|
11
16
|
|
|
12
17
|
/**
|
|
13
18
|
* Schema that a block's props or answer can use.
|
|
@@ -82,3 +87,79 @@ export const make = <const Kind extends string, Props extends Codec, Answer exte
|
|
|
82
87
|
props: options.props,
|
|
83
88
|
answer: options.answer
|
|
84
89
|
})
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Schema for the props of a block that shows a tool's result: none, since the result is the block.
|
|
93
|
+
*
|
|
94
|
+
* @category schemas
|
|
95
|
+
* @since 0.5.0
|
|
96
|
+
*/
|
|
97
|
+
export const NoProps = Schema.Struct({})
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Represents a block that shows a tool's results, made with {@link fromToolResult}. Its kind is the
|
|
101
|
+
* tool's name.
|
|
102
|
+
*
|
|
103
|
+
* @category models
|
|
104
|
+
* @since 0.5.0
|
|
105
|
+
*/
|
|
106
|
+
export interface ToolResultBlock<T extends Tool.Any = Tool.Any> extends Block<
|
|
107
|
+
Tool.Name<T>,
|
|
108
|
+
typeof NoProps,
|
|
109
|
+
Schema.Never
|
|
110
|
+
> {
|
|
111
|
+
readonly tool: T
|
|
112
|
+
/** The tool's success schema, which the client decodes the result with. */
|
|
113
|
+
readonly result: Codec
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Creates a block that shows a tool's results.
|
|
118
|
+
*
|
|
119
|
+
* **Details**
|
|
120
|
+
*
|
|
121
|
+
* Put it in the assistant's `BlockKit`. The tool's results then carry a `resultId` in what the model
|
|
122
|
+
* reads, and the model shows one with a `tool_result` block that names it: it never sees the
|
|
123
|
+
* block's kind or how the client draws it. Sending the block fails back to the model when no
|
|
124
|
+
* successful result of a shown tool has that id.
|
|
125
|
+
*
|
|
126
|
+
* On the client, `BlockKit.toClient` gives the block's renderer the result, decoded by the tool's
|
|
127
|
+
* success schema. The tool's definition is shared with the client for that, so it can't need
|
|
128
|
+
* server-only services: add those on the server with `Tool.addDependency`.
|
|
129
|
+
*
|
|
130
|
+
* **Example** (Showing search results as cards)
|
|
131
|
+
*
|
|
132
|
+
* ```ts
|
|
133
|
+
* import { Block, BlockKit } from "effect-assistant"
|
|
134
|
+
* import { SearchAssets } from "./tools.ts"
|
|
135
|
+
*
|
|
136
|
+
* export const Blocks = BlockKit.make([PickOne, Block.fromToolResult(SearchAssets)])
|
|
137
|
+
*
|
|
138
|
+
* // Client
|
|
139
|
+
* const renderBlock = BlockKit.toClient<typeof Blocks, ReactNode>(Blocks, {
|
|
140
|
+
* pick_one: ({ props, respond }) => <Options options={props.options} onPick={respond} />,
|
|
141
|
+
* search_assets: ({ result }) => <AssetCards assets={result.items} />
|
|
142
|
+
* })
|
|
143
|
+
* ```
|
|
144
|
+
*
|
|
145
|
+
* @category constructors
|
|
146
|
+
* @since 0.5.0
|
|
147
|
+
*/
|
|
148
|
+
export const fromToolResult = <T extends Tool.Any>(
|
|
149
|
+
tool: T & { readonly successSchema: Codec }
|
|
150
|
+
): ToolResultBlock<T> => ({
|
|
151
|
+
kind: tool.name,
|
|
152
|
+
description: `Shows a ${tool.name} result.`,
|
|
153
|
+
props: NoProps,
|
|
154
|
+
answer: undefined,
|
|
155
|
+
tool,
|
|
156
|
+
result: tool.successSchema
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Returns whether a block shows a tool's results.
|
|
161
|
+
*
|
|
162
|
+
* @category guards
|
|
163
|
+
* @since 0.5.0
|
|
164
|
+
*/
|
|
165
|
+
export const isToolResult = (block: Any): block is ToolResultBlock => Predicate.hasProperty(block, "tool")
|
package/src/BlockKit.ts
CHANGED
|
@@ -8,12 +8,13 @@
|
|
|
8
8
|
*
|
|
9
9
|
* @since 0.1.0
|
|
10
10
|
*/
|
|
11
|
+
import type * as Tool from "effect/ai/Tool"
|
|
11
12
|
import * as Effect from "effect/Effect"
|
|
12
13
|
import * as Option from "effect/Option"
|
|
13
14
|
import * as Predicate from "effect/Predicate"
|
|
14
15
|
import * as Schema from "effect/Schema"
|
|
15
16
|
import * as AssistantEvent from "./AssistantEvent.ts"
|
|
16
|
-
import
|
|
17
|
+
import * as Block from "./Block.ts"
|
|
17
18
|
import type * as Timeline from "./Timeline.ts"
|
|
18
19
|
import { CurrentChat } from "./CurrentChat.ts"
|
|
19
20
|
import * as InputId from "./InputId.ts"
|
|
@@ -40,6 +41,15 @@ export const make = <const Blocks extends ReadonlyArray<Block.Any>>(blocks: Bloc
|
|
|
40
41
|
byKind: new Map(blocks.map((block) => [block.kind, block]))
|
|
41
42
|
})
|
|
42
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Returns the names of the tools whose results the kit's blocks show (`Block.fromToolResult`).
|
|
46
|
+
*
|
|
47
|
+
* @category utils
|
|
48
|
+
* @since 0.5.0
|
|
49
|
+
*/
|
|
50
|
+
export const toolNames = (kit: BlockKit | undefined): ReadonlySet<string> =>
|
|
51
|
+
new Set((kit?.blocks ?? []).flatMap((block) => (Block.isToolResult(block) ? [block.tool.name] : [])))
|
|
52
|
+
|
|
43
53
|
type KindOf<B> = B extends { readonly kind: infer K extends string } ? K : never
|
|
44
54
|
|
|
45
55
|
type BlockOf<Kit extends BlockKit, K> = Extract<Kit["blocks"][number], { readonly kind: K }>
|
|
@@ -48,6 +58,13 @@ type PropsOf<B> = B extends { readonly props: Schema.Top } ? B["props"]["Type"]
|
|
|
48
58
|
|
|
49
59
|
type AnswerOf<B> = B extends { readonly answer: infer A } ? (A extends Schema.Top ? A["Type"] : never) : never
|
|
50
60
|
|
|
61
|
+
type ResultArgs<B> = B extends { readonly tool: infer T }
|
|
62
|
+
? {
|
|
63
|
+
/** The tool's result the block shows, decoded by its success schema. */
|
|
64
|
+
readonly result: Tool.Success<T>
|
|
65
|
+
}
|
|
66
|
+
: unknown
|
|
67
|
+
|
|
51
68
|
/**
|
|
52
69
|
* Represents the state of a message's block.
|
|
53
70
|
*
|
|
@@ -164,12 +181,21 @@ export const closeStale = Effect.fn(function* (options: { readonly messages: num
|
|
|
164
181
|
})
|
|
165
182
|
|
|
166
183
|
/**
|
|
167
|
-
* Represents what a renderer receives for one block.
|
|
184
|
+
* Represents what a renderer receives for one block. A block that shows a tool's result also gets
|
|
185
|
+
* the `result`.
|
|
168
186
|
*
|
|
169
187
|
* @category models
|
|
170
188
|
* @since 0.1.0
|
|
171
189
|
*/
|
|
172
|
-
export
|
|
190
|
+
export type RenderArgs<B> = BlockArgs<B> & ResultArgs<B>
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Represents what every block's renderer receives.
|
|
194
|
+
*
|
|
195
|
+
* @category models
|
|
196
|
+
* @since 0.5.0
|
|
197
|
+
*/
|
|
198
|
+
export interface BlockArgs<B> {
|
|
173
199
|
/**
|
|
174
200
|
* The id of the message the block is on. A block whose button calls your API can key the work
|
|
175
201
|
* on it, with the chat's id, so the work runs once per block.
|
|
@@ -256,7 +282,7 @@ export function toClient<Kit extends BlockKit, Out>(
|
|
|
256
282
|
// arguments.
|
|
257
283
|
export function toClient<Out>(
|
|
258
284
|
kit: BlockKit,
|
|
259
|
-
renderers: Readonly<Record<string, (args:
|
|
285
|
+
renderers: Readonly<Record<string, (args: BlockArgs<Block.Any> & { readonly result?: unknown }) => Out>>
|
|
260
286
|
): (message: Timeline.AssistantMessage, options: RenderOptions) => Out | undefined {
|
|
261
287
|
return (message, options) => {
|
|
262
288
|
if (message.block === undefined) return undefined
|
|
@@ -271,6 +297,12 @@ export function toClient<Out>(
|
|
|
271
297
|
|
|
272
298
|
if (Option.isNone(props)) return undefined
|
|
273
299
|
|
|
300
|
+
const result = Block.isToolResult(block)
|
|
301
|
+
? Schema.decodeUnknownOption(Schema.toCodecJson(block.result))(message.block.result)
|
|
302
|
+
: Option.some(undefined)
|
|
303
|
+
|
|
304
|
+
if (Option.isNone(result)) return undefined
|
|
305
|
+
|
|
274
306
|
const given = message.block.answer
|
|
275
307
|
const answerCodec = block.answer === undefined ? undefined : Schema.toCodecJson(block.answer)
|
|
276
308
|
|
|
@@ -281,6 +313,7 @@ export function toClient<Out>(
|
|
|
281
313
|
messageId: message.id,
|
|
282
314
|
text: message.text,
|
|
283
315
|
props: props.value,
|
|
316
|
+
result: result.value,
|
|
284
317
|
open: message.block.status === "open",
|
|
285
318
|
answer: Option.getOrUndefined(answer),
|
|
286
319
|
respond: (value) =>
|
package/src/ChatLog.ts
CHANGED
|
@@ -33,7 +33,7 @@ export class ChatLog extends Context.Service<
|
|
|
33
33
|
chatId: ChatId,
|
|
34
34
|
events: ReadonlyArray<AssistantEvent.AssistantEvent | AssistantEvent.StoredAppEvent>
|
|
35
35
|
) => Effect.Effect<ReadonlyArray<AssistantEvent.Entry>>
|
|
36
|
-
/** Reads a chat's stored entries in order, either all of them or only those after a `
|
|
36
|
+
/** Reads a chat's stored entries in order, either all of them or only those after a `sequence`. */
|
|
37
37
|
readonly read: (chatId: ChatId, options?: ReadOptions) => Effect.Effect<ReadonlyArray<AssistantEvent.Entry>>
|
|
38
38
|
/**
|
|
39
39
|
* Returns the chats whose last entry ends neither a turn nor the chat. After a restart, these
|
|
@@ -50,14 +50,14 @@ export class ChatLog extends Context.Service<
|
|
|
50
50
|
* @since 0.1.0
|
|
51
51
|
*/
|
|
52
52
|
export interface ReadOptions {
|
|
53
|
-
/** Reads only the entries after this `
|
|
53
|
+
/** Reads only the entries after this `sequence`. */
|
|
54
54
|
readonly after?: number | undefined
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
/**
|
|
58
58
|
* Represents the storage that a `ChatLog` is built on.
|
|
59
59
|
*
|
|
60
|
-
* A storage reads a chat's entries, finds a chat's last `
|
|
60
|
+
* A storage reads a chat's entries, finds a chat's last `sequence`, writes new entries, and finds every
|
|
61
61
|
* chat's last entry. Entries are never removed, so a deleted chat keeps its log, which ends in
|
|
62
62
|
* `ChatDeleted`.
|
|
63
63
|
*
|
|
@@ -66,7 +66,7 @@ export interface ReadOptions {
|
|
|
66
66
|
*/
|
|
67
67
|
export interface Storage {
|
|
68
68
|
readonly read: (chatId: ChatId, options?: ReadOptions) => Effect.Effect<ReadonlyArray<AssistantEvent.Entry>>
|
|
69
|
-
readonly
|
|
69
|
+
readonly lastSequence: (chatId: ChatId) => Effect.Effect<number>
|
|
70
70
|
readonly write: (chatId: ChatId, entries: ReadonlyArray<AssistantEvent.Entry>) => Effect.Effect<void>
|
|
71
71
|
/** Returns the last entry of every chat. */
|
|
72
72
|
readonly lastEntries: Effect.Effect<ReadonlyArray<AssistantEvent.Entry>>
|
|
@@ -74,7 +74,7 @@ export interface Storage {
|
|
|
74
74
|
|
|
75
75
|
/**
|
|
76
76
|
* Creates a `ChatLog` from a {@link Storage}. `append` numbers new entries after the chat's last
|
|
77
|
-
* `
|
|
77
|
+
* `sequence` and stamps them with the current time.
|
|
78
78
|
*
|
|
79
79
|
* @category constructors
|
|
80
80
|
* @since 0.1.0
|
|
@@ -88,9 +88,12 @@ export const make = (storage: Storage): ChatLog["Service"] =>
|
|
|
88
88
|
) {
|
|
89
89
|
if (events.length === 0) return []
|
|
90
90
|
|
|
91
|
-
const
|
|
91
|
+
const sequence = yield* storage.lastSequence(chatId)
|
|
92
92
|
const at = yield* Clock.currentTimeMillis
|
|
93
|
-
|
|
93
|
+
|
|
94
|
+
const entries = events.map((event, n) =>
|
|
95
|
+
AssistantEvent.Entry.make({ chatId, sequence: sequence + n + 1, at, event })
|
|
96
|
+
)
|
|
94
97
|
|
|
95
98
|
yield* storage.write(chatId, entries)
|
|
96
99
|
|
|
@@ -121,12 +124,12 @@ export const layerMemory: Layer.Layer<ChatLog> = Layer.effect(
|
|
|
121
124
|
read: Effect.fnUntraced(function* (chatId: ChatId, options?: ReadOptions) {
|
|
122
125
|
const all = yield* Ref.get(chats)
|
|
123
126
|
|
|
124
|
-
return (all.get(chatId) ?? []).filter((entry) => entry.
|
|
127
|
+
return (all.get(chatId) ?? []).filter((entry) => entry.sequence > (options?.after ?? 0))
|
|
125
128
|
}),
|
|
126
|
-
|
|
129
|
+
lastSequence: Effect.fnUntraced(function* (chatId: ChatId) {
|
|
127
130
|
const all = yield* Ref.get(chats)
|
|
128
131
|
|
|
129
|
-
return all.get(chatId)?.at(-1)?.
|
|
132
|
+
return all.get(chatId)?.at(-1)?.sequence ?? 0
|
|
130
133
|
}),
|
|
131
134
|
write: (chatId, entries) =>
|
|
132
135
|
Ref.update(chats, (all) => new Map(all).set(chatId, [...(all.get(chatId) ?? []), ...entries])),
|
|
@@ -144,17 +147,19 @@ const EventJson = Schema.fromJsonString(Schema.Union([AssistantEvent.AssistantEv
|
|
|
144
147
|
// Postgres returns BIGINT and DOUBLE PRECISION columns as strings; other databases as numbers.
|
|
145
148
|
const SqlNumber = Schema.Union([Schema.Finite, Schema.FiniteFromString])
|
|
146
149
|
|
|
147
|
-
const Row = Schema.Struct({ chat_id: ChatId,
|
|
150
|
+
const Row = Schema.Struct({ chat_id: ChatId, sequence: SqlNumber, at: SqlNumber, event: EventJson })
|
|
148
151
|
|
|
149
|
-
const
|
|
152
|
+
const decodeLastSequence = Schema.decodeUnknownEffect(
|
|
153
|
+
Schema.Array(Schema.Struct({ sequence: Schema.NullOr(SqlNumber) }))
|
|
154
|
+
)
|
|
150
155
|
|
|
151
156
|
/**
|
|
152
157
|
* Layer that stores chats in a SQL table named `chat_log`.
|
|
153
158
|
*
|
|
154
159
|
* **Details**
|
|
155
160
|
*
|
|
156
|
-
* The table must already exist. It has the columns `chat_id`, `
|
|
157
|
-
* primary key `(chat_id,
|
|
161
|
+
* The table must already exist. It has the columns `chat_id`, `sequence`, `at`, and `event`, with the
|
|
162
|
+
* primary key `(chat_id, sequence)`. The layer requires a `SqlClient`.
|
|
158
163
|
*
|
|
159
164
|
* **Example** (Storing chats in Postgres)
|
|
160
165
|
*
|
|
@@ -174,10 +179,10 @@ const decodeLastSeq = Schema.decodeUnknownEffect(Schema.Array(Schema.Struct({ se
|
|
|
174
179
|
* ```sql
|
|
175
180
|
* CREATE TABLE chat_log (
|
|
176
181
|
* chat_id TEXT NOT NULL,
|
|
177
|
-
*
|
|
182
|
+
* sequence INTEGER NOT NULL,
|
|
178
183
|
* at DOUBLE PRECISION NOT NULL,
|
|
179
184
|
* event TEXT NOT NULL,
|
|
180
|
-
* PRIMARY KEY (chat_id,
|
|
185
|
+
* PRIMARY KEY (chat_id, sequence)
|
|
181
186
|
* );
|
|
182
187
|
* ```
|
|
183
188
|
*
|
|
@@ -195,7 +200,7 @@ export const layerSql: Layer.Layer<ChatLog, never, SqlClient.SqlClient> = Layer.
|
|
|
195
200
|
return decoded.map((row) =>
|
|
196
201
|
AssistantEvent.Entry.make({
|
|
197
202
|
chatId: row.chat_id,
|
|
198
|
-
|
|
203
|
+
sequence: row.sequence,
|
|
199
204
|
at: row.at,
|
|
200
205
|
event: row.event
|
|
201
206
|
})
|
|
@@ -207,19 +212,19 @@ export const layerSql: Layer.Layer<ChatLog, never, SqlClient.SqlClient> = Layer.
|
|
|
207
212
|
return make({
|
|
208
213
|
read: Effect.fnUntraced(function* (chatId: ChatId, options?: ReadOptions) {
|
|
209
214
|
const rows = yield* sql`
|
|
210
|
-
SELECT chat_id,
|
|
215
|
+
SELECT chat_id, sequence, at, event FROM chat_log WHERE chat_id = ${chatId} AND sequence > ${options?.after ?? 0} ORDER BY sequence
|
|
211
216
|
`
|
|
212
217
|
|
|
213
218
|
return yield* toEntries(rows)
|
|
214
219
|
}, Effect.orDie),
|
|
215
|
-
|
|
220
|
+
lastSequence: Effect.fnUntraced(function* (chatId: ChatId) {
|
|
216
221
|
const result = yield* sql`
|
|
217
|
-
SELECT MAX(
|
|
222
|
+
SELECT MAX(sequence) AS sequence FROM chat_log WHERE chat_id = ${chatId}
|
|
218
223
|
`
|
|
219
224
|
|
|
220
|
-
const rows = yield*
|
|
225
|
+
const rows = yield* decodeLastSequence(result)
|
|
221
226
|
|
|
222
|
-
return rows[0]?.
|
|
227
|
+
return rows[0]?.sequence ?? 0
|
|
223
228
|
}, Effect.orDie),
|
|
224
229
|
write: Effect.fnUntraced(function* (chatId: ChatId, entries: ReadonlyArray<AssistantEvent.Entry>) {
|
|
225
230
|
const rows = yield* Effect.forEach(
|
|
@@ -227,7 +232,7 @@ export const layerSql: Layer.Layer<ChatLog, never, SqlClient.SqlClient> = Layer.
|
|
|
227
232
|
Effect.fnUntraced(function* (entry: AssistantEvent.Entry) {
|
|
228
233
|
const event = yield* encodeEvent(entry.event)
|
|
229
234
|
|
|
230
|
-
return { chat_id: chatId,
|
|
235
|
+
return { chat_id: chatId, sequence: entry.sequence, at: entry.at, event }
|
|
231
236
|
})
|
|
232
237
|
)
|
|
233
238
|
|
|
@@ -235,9 +240,9 @@ export const layerSql: Layer.Layer<ChatLog, never, SqlClient.SqlClient> = Layer.
|
|
|
235
240
|
}, Effect.orDie),
|
|
236
241
|
lastEntries: Effect.gen(function* () {
|
|
237
242
|
const rows = yield* sql`
|
|
238
|
-
SELECT e.chat_id, e.
|
|
239
|
-
JOIN (SELECT chat_id, MAX(
|
|
240
|
-
ON last.chat_id = e.chat_id AND last.
|
|
243
|
+
SELECT e.chat_id, e.sequence, e.at, e.event FROM chat_log e
|
|
244
|
+
JOIN (SELECT chat_id, MAX(sequence) AS sequence FROM chat_log GROUP BY chat_id) last
|
|
245
|
+
ON last.chat_id = e.chat_id AND last.sequence = e.sequence
|
|
241
246
|
`
|
|
242
247
|
|
|
243
248
|
return yield* toEntries(rows)
|