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/Assistant.ts
CHANGED
|
@@ -24,7 +24,6 @@ import * as Predicate from "effect/Predicate"
|
|
|
24
24
|
import * as Schema from "effect/Schema"
|
|
25
25
|
import type * as Scope from "effect/Scope"
|
|
26
26
|
import * as Stream from "effect/Stream"
|
|
27
|
-
import type * as AppEvent from "./AppEvent.ts"
|
|
28
27
|
import { AssistantEngine } from "./AssistantEngine.ts"
|
|
29
28
|
import * as AssistantEvent from "./AssistantEvent.ts"
|
|
30
29
|
import type * as AssistantLoop from "./AssistantLoop.ts"
|
|
@@ -66,7 +65,8 @@ export interface LoopOptions<T extends Record<string, Tool.Any> = Record<never,
|
|
|
66
65
|
readonly toolkit?: Toolkit.Toolkit<T> | undefined
|
|
67
66
|
/**
|
|
68
67
|
* Playbooks the model can follow. The prompt lists them as a catalog, and the model loads one
|
|
69
|
-
* with `load_skill` when a task matches it.
|
|
68
|
+
* with `load_skill` when a task matches it. A skill made with `tools` is left out when the turn's
|
|
69
|
+
* tools lack one of them.
|
|
70
70
|
*/
|
|
71
71
|
readonly skills?: ReadonlyArray<Skill<RS>> | undefined
|
|
72
72
|
/** The maximum number of model calls per turn. Defaults to 16. */
|
|
@@ -75,7 +75,8 @@ export interface LoopOptions<T extends Record<string, Tool.Any> = Record<never,
|
|
|
75
75
|
readonly replyNudge?: string | undefined
|
|
76
76
|
/**
|
|
77
77
|
* Selects which of the chat's entries the model reads, for example only those since the last
|
|
78
|
-
* summary. Defaults to all
|
|
78
|
+
* summary, and maps the app's own events it should read into prompt messages. Defaults to all
|
|
79
|
+
* entries, without the app's events.
|
|
79
80
|
*/
|
|
80
81
|
readonly history?: HistoryView<Events> | undefined
|
|
81
82
|
}
|
|
@@ -86,9 +87,43 @@ export interface LoopOptions<T extends Record<string, Tool.Any> = Record<never,
|
|
|
86
87
|
*
|
|
87
88
|
* **Details**
|
|
88
89
|
*
|
|
89
|
-
* The function receives the chat's entries
|
|
90
|
-
* `Prompt.Message`s. The model reads the returned entries the same way
|
|
91
|
-
* and reads each prompt message unchanged.
|
|
90
|
+
* The function receives the chat's entries, the app's own events among them, and returns, in
|
|
91
|
+
* order, a mix of entries and `Prompt.Message`s. The model reads the returned entries the same way
|
|
92
|
+
* it reads the full history, and reads each prompt message unchanged, where the view put it. The
|
|
93
|
+
* model reads none of the app's events by itself: map those it should read to a message, and an
|
|
94
|
+
* app event's entry left in the result is dropped.
|
|
95
|
+
*
|
|
96
|
+
* A message stands right after the entry before it: one that came after the model's last read, or
|
|
97
|
+
* between a tool call and its result, moves after them, as an input does.
|
|
98
|
+
*
|
|
99
|
+
* **Example** (Telling the model about an order the user placed)
|
|
100
|
+
*
|
|
101
|
+
* ```ts
|
|
102
|
+
* import { Assistant, InputId } from "effect-assistant"
|
|
103
|
+
* import * as Prompt from "effect/ai/Prompt"
|
|
104
|
+
* import * as Schema from "effect/Schema"
|
|
105
|
+
*
|
|
106
|
+
* class OrderPlaced extends Schema.TaggedClass<OrderPlaced>()("OrderPlaced", {
|
|
107
|
+
* id: InputId.InputId,
|
|
108
|
+
* reference: Schema.String
|
|
109
|
+
* }) {}
|
|
110
|
+
*
|
|
111
|
+
* const MyAssistant = Assistant.make("my-assistant", { events: [OrderPlaced] })
|
|
112
|
+
*
|
|
113
|
+
* const withOrders: Assistant.HistoryView<OrderPlaced> = (entries) =>
|
|
114
|
+
* entries.map((entry) =>
|
|
115
|
+
* entry.event instanceof OrderPlaced
|
|
116
|
+
* ? Prompt.userMessage({
|
|
117
|
+
* content: [Prompt.textPart({ text: `The user placed order ${entry.event.reference}.` })]
|
|
118
|
+
* })
|
|
119
|
+
* : entry
|
|
120
|
+
* )
|
|
121
|
+
*
|
|
122
|
+
* // An `OrderPlaced` that the API submits wakes a turn, which reads it as this note.
|
|
123
|
+
* const MyAssistantLive = MyAssistant.toLayer((turn) =>
|
|
124
|
+
* turn.loop({ instructions: "Be brief.", history: withOrders })
|
|
125
|
+
* )
|
|
126
|
+
* ```
|
|
92
127
|
*
|
|
93
128
|
* @category models
|
|
94
129
|
* @since 0.1.0
|
|
@@ -249,8 +284,8 @@ export interface Chat<Events = never> {
|
|
|
249
284
|
readonly entries: Effect.Effect<ReadonlyArray<Entry<Events>>>
|
|
250
285
|
/**
|
|
251
286
|
* Appends events after the chat's last entry and sends them to the chat's subscribers. The
|
|
252
|
-
* events can be the library's or the assistant's own, such as a summary.
|
|
253
|
-
* turn
|
|
287
|
+
* events can be the library's or the assistant's own, such as a summary. None of them wakes a
|
|
288
|
+
* turn: only what comes in through the assistant's `submit` does.
|
|
254
289
|
*/
|
|
255
290
|
append(...events: ReadonlyArray<AssistantEvent.AssistantEvent | Events>): Effect.Effect<void>
|
|
256
291
|
}
|
|
@@ -266,8 +301,11 @@ export interface Chat<Events = never> {
|
|
|
266
301
|
export interface Service<Events = never> {
|
|
267
302
|
/**
|
|
268
303
|
* Stores an input or one of the assistant's own events, then returns. Examples are a user's
|
|
269
|
-
* message or block answer, a workflow's progress, or the
|
|
270
|
-
*
|
|
304
|
+
* message or block answer, a workflow's progress, or the order the user placed. Each one is an
|
|
305
|
+
* input: storing it wakes a turn, which the assistant runs in the background. The handler decides
|
|
306
|
+
* what the turn does with it, and an app event the model should read needs a history view that
|
|
307
|
+
* maps it (see {@link HistoryView}). To store an app event without waking a turn, append it from
|
|
308
|
+
* within the chat instead (`yield* MyAssistant.Chat`).
|
|
271
309
|
*
|
|
272
310
|
* Fails with `GetChatError` when the app's `Chats` service reports that the chat can't be used,
|
|
273
311
|
* because the app doesn't know the chat or has deleted it.
|
|
@@ -279,9 +317,10 @@ export interface Service<Events = never> {
|
|
|
279
317
|
* A reader that already has the earlier entries passes `after` and receives only the entries
|
|
280
318
|
* after that `seq`.
|
|
281
319
|
*
|
|
282
|
-
* For a chat client, pipe the whole log into `
|
|
283
|
-
* streams the chat as its user sees it. Its `after` goes there, not here:
|
|
284
|
-
* depends on earlier entries.
|
|
320
|
+
* For a chat client, pipe the whole log into `Timeline.changes(MyAssistant, { after })`, which
|
|
321
|
+
* streams the chat's timeline, as its user sees it. Its `after` goes there, not here: a message
|
|
322
|
+
* on the timeline depends on earlier entries. The timeline shows only the app events named in its
|
|
323
|
+
* `show`, none by default (see `Timeline.changes`).
|
|
285
324
|
*/
|
|
286
325
|
readonly subscribe: (
|
|
287
326
|
chatId: ChatId,
|
|
@@ -323,8 +362,8 @@ export interface Assistant<
|
|
|
323
362
|
readonly blocks: BlockKit.BlockKit | undefined
|
|
324
363
|
/** The specialists the assistant can hand work to. */
|
|
325
364
|
readonly specialists: ReadonlyArray<Specs>
|
|
326
|
-
/** The app's own events the assistant
|
|
327
|
-
readonly events: ReadonlyArray<
|
|
365
|
+
/** The schemas of the app's own events the assistant stores. */
|
|
366
|
+
readonly events: ReadonlyArray<AssistantEvent.AppEventSchema>
|
|
328
367
|
/**
|
|
329
368
|
* Creates a layer that registers the assistant's handler with the engine and provides the
|
|
330
369
|
* assistant's service.
|
|
@@ -387,9 +426,9 @@ export interface Assistant<
|
|
|
387
426
|
*
|
|
388
427
|
* export const MyAssistant = Assistant.make("my-assistant")
|
|
389
428
|
*
|
|
390
|
-
* // Requires `
|
|
429
|
+
* // Requires `Sharding`. The cluster's runners run the chats, with `AssistantEngine.layerCluster`.
|
|
391
430
|
* const MyAssistantClient = MyAssistant.layerClient.pipe(
|
|
392
|
-
* Layer.provide(AssistantEngine.
|
|
431
|
+
* Layer.provide(AssistantEngine.layerClusterClient)
|
|
393
432
|
* )
|
|
394
433
|
* ```
|
|
395
434
|
*/
|
|
@@ -419,28 +458,35 @@ export type Any = Assistant<any, any, any>
|
|
|
419
458
|
*/
|
|
420
459
|
export const isAssistant = (u: unknown): u is Any => Predicate.hasProperty(u, TypeId)
|
|
421
460
|
|
|
422
|
-
function entrySchema<E>(events: ReadonlyArray<
|
|
461
|
+
function entrySchema<E>(events: ReadonlyArray<AssistantEvent.AppEventSchema>): Schema.Codec<Entry<E>, Schema.Json>
|
|
423
462
|
// The overload states the decoded type: the library's events, or one of these schemas' types.
|
|
424
|
-
function entrySchema(events: ReadonlyArray<
|
|
463
|
+
function entrySchema(events: ReadonlyArray<AssistantEvent.AppEventSchema>): Schema.Top {
|
|
425
464
|
return Schema.toCodecJson(
|
|
426
465
|
Schema.Struct({
|
|
427
466
|
chatId: ChatId,
|
|
428
467
|
seq: Schema.Int,
|
|
429
468
|
at: Schema.Finite,
|
|
430
|
-
event: Schema.Union([AssistantEvent.AssistantEvent, ...events
|
|
469
|
+
event: Schema.Union([AssistantEvent.AssistantEvent, ...events])
|
|
431
470
|
})
|
|
432
471
|
)
|
|
433
472
|
}
|
|
434
473
|
|
|
435
474
|
/**
|
|
436
|
-
* Creates an assistant definition from its name, its UI blocks, its specialists, and the
|
|
437
|
-
* events it stores
|
|
475
|
+
* Creates an assistant definition from its name, its UI blocks, its specialists, and the schemas of
|
|
476
|
+
* the app's own events it stores.
|
|
438
477
|
*
|
|
439
478
|
* **Details**
|
|
440
479
|
*
|
|
441
480
|
* The definition holds no code that runs turns. Provide a handler with its `toLayer`, then yield
|
|
442
481
|
* the definition to get the assistant's {@link Service}.
|
|
443
482
|
*
|
|
483
|
+
* The definition only says which of the app's events a chat stores. What happens with one is up
|
|
484
|
+
* to the places that use it. One that comes in through the service's `submit`, from outside a
|
|
485
|
+
* turn, is an input and wakes a turn, like a user's message; one a turn appends to its chat never
|
|
486
|
+
* does. The handler decides what a turn does, a history view says how the model reads an event
|
|
487
|
+
* ({@link HistoryView}), and the chat's timeline says which its user sees
|
|
488
|
+
* (`Timeline.changes`).
|
|
489
|
+
*
|
|
444
490
|
* **Example** (Defining an assistant and submitting a message)
|
|
445
491
|
*
|
|
446
492
|
* ```ts
|
|
@@ -466,7 +512,7 @@ function entrySchema(events: ReadonlyArray<AppEvent.Any>): Schema.Top {
|
|
|
466
512
|
export const make = <
|
|
467
513
|
const Name extends string,
|
|
468
514
|
const Specs extends ReadonlyArray<Specialist.Any> = readonly [],
|
|
469
|
-
const Events extends ReadonlyArray<
|
|
515
|
+
const Events extends ReadonlyArray<AssistantEvent.AppEventSchema> = readonly []
|
|
470
516
|
>(
|
|
471
517
|
name: Name,
|
|
472
518
|
options?: {
|
|
@@ -478,14 +524,14 @@ export const make = <
|
|
|
478
524
|
*/
|
|
479
525
|
readonly specialists?: Specs | undefined
|
|
480
526
|
/**
|
|
481
|
-
* The app's own events, each
|
|
482
|
-
* `
|
|
483
|
-
* decoded.
|
|
484
|
-
*
|
|
527
|
+
* The schemas of the app's own events, each a tagged struct or class. `submit` and the chat's
|
|
528
|
+
* `append` store them, in their JSON form, and the chat's `entries` and `subscribe` read them
|
|
529
|
+
* back, decoded. Each needs a tag of its own that isn't one of the library's: `make` throws
|
|
530
|
+
* otherwise.
|
|
485
531
|
*/
|
|
486
532
|
readonly events?: Events | undefined
|
|
487
533
|
}
|
|
488
|
-
): Assistant<Name, Specs[number],
|
|
534
|
+
): Assistant<Name, Specs[number], Events[number]["Type"]> => {
|
|
489
535
|
const events = options?.events ?? []
|
|
490
536
|
|
|
491
537
|
const definition: Turns.Definition = {
|
|
@@ -495,7 +541,7 @@ export const make = <
|
|
|
495
541
|
app: AppEvents.make(events)
|
|
496
542
|
}
|
|
497
543
|
|
|
498
|
-
type OwnEvents =
|
|
544
|
+
type OwnEvents = Events[number]["Type"]
|
|
499
545
|
|
|
500
546
|
const Key = Context.Service<Identifier<Name>, Service<OwnEvents>>(`effect-assistant/Assistant/${name}`)
|
|
501
547
|
|
package/src/AssistantActivity.ts
CHANGED
|
@@ -71,32 +71,25 @@ const withoutRun = (activity: State, runId: RunId): State => {
|
|
|
71
71
|
return { ...activity, specialists }
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
/**
|
|
75
|
-
* Represents the options for folding a chat's entries into its activity.
|
|
76
|
-
*
|
|
77
|
-
* @category models
|
|
78
|
-
* @since 0.1.0
|
|
79
|
-
*/
|
|
80
|
-
export interface Options {
|
|
81
|
-
/**
|
|
82
|
-
* The tags of the app's events that wake a turn (see `AppEvent`). These events count as inputs
|
|
83
|
-
* too. Defaults to none.
|
|
84
|
-
*/
|
|
85
|
-
readonly wakes?: ReadonlySet<string> | undefined
|
|
86
|
-
}
|
|
87
|
-
|
|
88
74
|
/**
|
|
89
75
|
* Returns the activity after one more entry. Fold a chat's entries in order, or use
|
|
90
76
|
* {@link fromEntries}.
|
|
91
77
|
*
|
|
78
|
+
* **Details**
|
|
79
|
+
*
|
|
80
|
+
* One of the app's events counts as an input, and a turn as about to start, when it came in
|
|
81
|
+
* through the assistant's `submit`. The log marks it so (`AssistantEvent.isAppInput`), as
|
|
82
|
+
* `ChatLog.read` returns it; `subscribe` streams it decoded, without the mark, so there the turn
|
|
83
|
+
* shows from its `TurnStarted`.
|
|
84
|
+
*
|
|
92
85
|
* @category combinators
|
|
93
86
|
* @since 0.1.0
|
|
94
87
|
*/
|
|
95
|
-
export const apply = (activity: State, entry: AssistantEvent.AnyEntry
|
|
88
|
+
export const apply = (activity: State, entry: AssistantEvent.AnyEntry): State => {
|
|
96
89
|
const event = entry.event
|
|
97
90
|
|
|
98
91
|
if (!AssistantEvent.isLibraryEvent(event)) {
|
|
99
|
-
return
|
|
92
|
+
return AssistantEvent.isAppInput(event) ? { ...activity, queued: true, lastInputSeq: entry.seq } : activity
|
|
100
93
|
}
|
|
101
94
|
|
|
102
95
|
if (AssistantEvent.isInput(event)) {
|
|
@@ -136,8 +129,8 @@ export const apply = (activity: State, entry: AssistantEvent.AnyEntry, options?:
|
|
|
136
129
|
* @category constructors
|
|
137
130
|
* @since 0.1.0
|
|
138
131
|
*/
|
|
139
|
-
export const fromEntries = (entries: ReadonlyArray<AssistantEvent.AnyEntry
|
|
140
|
-
entries.reduce((activity, entry) => apply(activity, entry
|
|
132
|
+
export const fromEntries = (entries: ReadonlyArray<AssistantEvent.AnyEntry>): State =>
|
|
133
|
+
entries.reduce((activity, entry) => apply(activity, entry), empty)
|
|
141
134
|
|
|
142
135
|
/**
|
|
143
136
|
* Returns whether anything is working: a turn that is running or about to start, or a specialist
|
package/src/AssistantEngine.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* This module provides the `AssistantEngine` service, which runs assistants' chats, and its
|
|
2
|
+
* This module provides the `AssistantEngine` service, which runs assistants' chats, and its
|
|
3
3
|
* implementations.
|
|
4
4
|
*
|
|
5
5
|
* An assistant's `toLayer` registers the assistant with the engine, and the assistant's `submit`
|
|
6
6
|
* and `subscribe` go through it. Pick one engine for the app, as with a workflow engine.
|
|
7
7
|
* {@link layerMemory} runs chats in the process, for tests and single-process apps.
|
|
8
|
-
* {@link layerCluster} runs each chat as a cluster entity, so a turn cut off by a crash resumes
|
|
8
|
+
* {@link layerCluster} runs each chat as a cluster entity, so a turn cut off by a crash resumes, and
|
|
9
|
+
* {@link layerClusterClient} reaches those chats from a process that runs none.
|
|
9
10
|
*
|
|
10
11
|
* @since 0.1.0
|
|
11
12
|
*/
|
|
@@ -155,7 +156,7 @@ export const layerMemory: Layer.Layer<AssistantEngine, never, ChatLog> = Layer.e
|
|
|
155
156
|
|
|
156
157
|
// A chat owes a turn when its last turn was cut off, or an input came after.
|
|
157
158
|
const resumeAll = Effect.fnUntraced(function* (registration: Turns.Registration) {
|
|
158
|
-
const { name
|
|
159
|
+
const { name } = registration.definition
|
|
159
160
|
|
|
160
161
|
for (const chatId of yield* log.unfinished) {
|
|
161
162
|
const entries = yield* log.read(chatId)
|
|
@@ -163,7 +164,7 @@ export const layerMemory: Layer.Layer<AssistantEngine, never, ChatLog> = Layer.e
|
|
|
163
164
|
const lastTurn = entries.findLast(({ event }) => Predicate.isTagged(event, "TurnStarted"))?.event
|
|
164
165
|
const ours = Predicate.isTagged(lastTurn, "TurnStarted") && lastTurn.assistant === name
|
|
165
166
|
|
|
166
|
-
if (ours && History.needsTurn(entries
|
|
167
|
+
if (ours && History.needsTurn(entries)) yield* withChat(name, chatId, (chat) => chat.resume)
|
|
167
168
|
}
|
|
168
169
|
})
|
|
169
170
|
|
|
@@ -212,6 +213,59 @@ const Delete = Rpc.make("Delete")
|
|
|
212
213
|
|
|
213
214
|
const chatEntity = (assistant: string) => Entity.make(`Assistant/${assistant}`, [Submit, Subscribe, Delete])
|
|
214
215
|
|
|
216
|
+
type EngineClient = Omit<AssistantEngine["Service"], "register">
|
|
217
|
+
|
|
218
|
+
// Entity clients are made on first use, so a process that registers no assistant can still reach its chats.
|
|
219
|
+
const makeClusterClient = Effect.gen(function* () {
|
|
220
|
+
const context = yield* Effect.context<Sharding.Sharding>()
|
|
221
|
+
const clients = new Map<string, EntityClient>()
|
|
222
|
+
|
|
223
|
+
const clientOf = Effect.fnUntraced(function* (assistant: string) {
|
|
224
|
+
const known = clients.get(assistant)
|
|
225
|
+
|
|
226
|
+
if (known !== undefined) return known
|
|
227
|
+
|
|
228
|
+
const makeClient = yield* chatEntity(assistant).client.pipe(Effect.provide(context))
|
|
229
|
+
|
|
230
|
+
const client: EntityClient = {
|
|
231
|
+
submit: (chatId, input) => makeClient(chatId).Submit({ input }, { discard: true }).pipe(Effect.orDie),
|
|
232
|
+
subscribe: (chatId, after) => makeClient(chatId).Subscribe({ after }).pipe(Stream.orDie),
|
|
233
|
+
delete: (chatId) => makeClient(chatId).Delete().pipe(Effect.orDie)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
clients.set(assistant, client)
|
|
237
|
+
|
|
238
|
+
return client
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
const engine: EngineClient = {
|
|
242
|
+
submit: Effect.fn("AssistantEngine.submit")(function* (
|
|
243
|
+
assistant: string,
|
|
244
|
+
chatId: ChatId,
|
|
245
|
+
input: AssistantEvent.Input | AssistantEvent.StoredAppEvent
|
|
246
|
+
) {
|
|
247
|
+
const client = yield* clientOf(assistant)
|
|
248
|
+
|
|
249
|
+
yield* client.submit(chatId, input)
|
|
250
|
+
}),
|
|
251
|
+
subscribe: (assistant, chatId, after) =>
|
|
252
|
+
Stream.unwrap(
|
|
253
|
+
Effect.gen(function* () {
|
|
254
|
+
const client = yield* clientOf(assistant)
|
|
255
|
+
|
|
256
|
+
return client.subscribe(chatId, after)
|
|
257
|
+
})
|
|
258
|
+
),
|
|
259
|
+
delete: Effect.fn("AssistantEngine.delete")(function* (assistant: string, chatId: ChatId) {
|
|
260
|
+
const client = yield* clientOf(assistant)
|
|
261
|
+
|
|
262
|
+
yield* client.delete(chatId)
|
|
263
|
+
})
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return engine
|
|
267
|
+
})
|
|
268
|
+
|
|
215
269
|
/**
|
|
216
270
|
* Layer that runs chats as cluster entities, one entity per chat, keyed by chat id.
|
|
217
271
|
*
|
|
@@ -249,6 +303,7 @@ const chatEntity = (assistant: string) => Entity.make(`Assistant/${assistant}`,
|
|
|
249
303
|
* ```
|
|
250
304
|
*
|
|
251
305
|
* @see {@link layerMemory} for running chats in one process
|
|
306
|
+
* @see {@link layerClusterClient} for a process that only reaches chats the cluster runs
|
|
252
307
|
*
|
|
253
308
|
* @category layers
|
|
254
309
|
* @since 0.1.0
|
|
@@ -258,7 +313,6 @@ export const layerCluster: Layer.Layer<AssistantEngine, never, ChatLog | Shardin
|
|
|
258
313
|
Effect.gen(function* () {
|
|
259
314
|
const context = yield* Effect.context<ChatLog | Sharding.Sharding>()
|
|
260
315
|
const sharding = yield* Sharding.Sharding
|
|
261
|
-
const clients = new Map<string, EntityClient>()
|
|
262
316
|
|
|
263
317
|
const register = Effect.fnUntraced(function* (registration: Turns.Registration) {
|
|
264
318
|
const ChatEntity = chatEntity(registration.definition.name)
|
|
@@ -295,49 +349,62 @@ export const layerCluster: Layer.Layer<AssistantEngine, never, ChatLog | Shardin
|
|
|
295
349
|
})
|
|
296
350
|
})
|
|
297
351
|
|
|
298
|
-
|
|
299
|
-
const clientOf = Effect.fnUntraced(function* (assistant: string) {
|
|
300
|
-
const known = clients.get(assistant)
|
|
301
|
-
|
|
302
|
-
if (known !== undefined) return known
|
|
303
|
-
|
|
304
|
-
const makeClient = yield* chatEntity(assistant).client.pipe(Effect.provide(context))
|
|
305
|
-
|
|
306
|
-
const client: EntityClient = {
|
|
307
|
-
submit: (chatId, input) => makeClient(chatId).Submit({ input }, { discard: true }).pipe(Effect.orDie),
|
|
308
|
-
subscribe: (chatId, after) => makeClient(chatId).Subscribe({ after }).pipe(Stream.orDie),
|
|
309
|
-
delete: (chatId) => makeClient(chatId).Delete().pipe(Effect.orDie)
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
clients.set(assistant, client)
|
|
313
|
-
|
|
314
|
-
return client
|
|
315
|
-
})
|
|
352
|
+
const client = yield* makeClusterClient
|
|
316
353
|
|
|
317
354
|
return AssistantEngine.of({
|
|
318
355
|
register: (registration) => register(registration).pipe(Effect.provide(context)),
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
) {
|
|
324
|
-
const client = yield* clientOf(assistant)
|
|
325
|
-
|
|
326
|
-
yield* client.submit(chatId, input)
|
|
327
|
-
}),
|
|
328
|
-
subscribe: (assistant, chatId, after) =>
|
|
329
|
-
Stream.unwrap(
|
|
330
|
-
Effect.gen(function* () {
|
|
331
|
-
const client = yield* clientOf(assistant)
|
|
356
|
+
...client
|
|
357
|
+
})
|
|
358
|
+
})
|
|
359
|
+
)
|
|
332
360
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
361
|
+
/**
|
|
362
|
+
* Layer that reaches chats running as cluster entities elsewhere, and runs none itself.
|
|
363
|
+
*
|
|
364
|
+
* **When to use**
|
|
365
|
+
*
|
|
366
|
+
* Use it in a process that only submits to, subscribes to, and deletes chats that the cluster's
|
|
367
|
+
* runners run with {@link layerCluster}, such as a web server that provides an assistant's
|
|
368
|
+
* `layerClient`. It needs no `ChatLog`, since the runners store the chats.
|
|
369
|
+
*
|
|
370
|
+
* **Details**
|
|
371
|
+
*
|
|
372
|
+
* The layer requires `Sharding`. An assistant's `toLayer` can't register with it: it dies, since
|
|
373
|
+
* no chats run in this process.
|
|
374
|
+
*
|
|
375
|
+
* **Example** (A web server in front of the cluster)
|
|
376
|
+
*
|
|
377
|
+
* ```ts
|
|
378
|
+
* import { Assistant, AssistantEngine } from "effect-assistant"
|
|
379
|
+
* import type * as Sharding from "effect/cluster/Sharding"
|
|
380
|
+
* import * as Layer from "effect/Layer"
|
|
381
|
+
*
|
|
382
|
+
* declare const ShardingLive: Layer.Layer<Sharding.Sharding>
|
|
383
|
+
*
|
|
384
|
+
* export const MyAssistant = Assistant.make("my-assistant")
|
|
385
|
+
*
|
|
386
|
+
* const MyAssistantClient = MyAssistant.layerClient.pipe(
|
|
387
|
+
* Layer.provide(AssistantEngine.layerClusterClient),
|
|
388
|
+
* Layer.provide(ShardingLive)
|
|
389
|
+
* )
|
|
390
|
+
* ```
|
|
391
|
+
*
|
|
392
|
+
* @see {@link layerCluster} for the runners that run the chats
|
|
393
|
+
*
|
|
394
|
+
* @category layers
|
|
395
|
+
* @since 0.3.0
|
|
396
|
+
*/
|
|
397
|
+
export const layerClusterClient: Layer.Layer<AssistantEngine, never, Sharding.Sharding> = Layer.effect(
|
|
398
|
+
AssistantEngine,
|
|
399
|
+
Effect.gen(function* () {
|
|
400
|
+
const client = yield* makeClusterClient
|
|
338
401
|
|
|
339
|
-
|
|
340
|
-
|
|
402
|
+
return AssistantEngine.of({
|
|
403
|
+
register: (registration) =>
|
|
404
|
+
Effect.die(
|
|
405
|
+
`The ${registration.definition.name} assistant can't register with AssistantEngine.layerClusterClient, which runs no chats: provide AssistantEngine.layerCluster where its toLayer runs.`
|
|
406
|
+
),
|
|
407
|
+
...client
|
|
341
408
|
})
|
|
342
409
|
})
|
|
343
410
|
)
|
package/src/AssistantEvent.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Defines the events that can happen in a chat.
|
|
3
3
|
*
|
|
4
4
|
* A chat is an append-only log of these events, and the log is the source of truth. A chat client
|
|
5
|
-
* receives the part of the log its user sees (`
|
|
5
|
+
* receives the part of the log its user sees (its `Timeline`), and the model's history is rebuilt
|
|
6
6
|
* from the log before every model call. Besides the library's events ({@link AssistantEvent}), a
|
|
7
7
|
* log holds the application's own events ({@link StoredAppEvent}).
|
|
8
8
|
*
|
|
@@ -11,8 +11,10 @@
|
|
|
11
11
|
import * as Response from "effect/ai/Response"
|
|
12
12
|
import * as Predicate from "effect/Predicate"
|
|
13
13
|
import * as Schema from "effect/Schema"
|
|
14
|
+
import type * as SchemaAST from "effect/SchemaAST"
|
|
14
15
|
import { ChatId } from "./ChatId.ts"
|
|
15
16
|
import { InputId, make as makeInputId } from "./InputId.ts"
|
|
17
|
+
import { inputKey } from "./internal/input.ts"
|
|
16
18
|
import { RunId } from "./RunId.ts"
|
|
17
19
|
|
|
18
20
|
/**
|
|
@@ -444,19 +446,43 @@ const libraryTags: ReadonlySet<string> = new Set(
|
|
|
444
446
|
*/
|
|
445
447
|
export const AppTag = Schema.String.pipe(Schema.brand("~effect-assistant/AppTag"))
|
|
446
448
|
|
|
449
|
+
/**
|
|
450
|
+
* Represents the schema of one of the application's own events, as `Assistant.make`'s `events`
|
|
451
|
+
* takes it.
|
|
452
|
+
*
|
|
453
|
+
* **Details**
|
|
454
|
+
*
|
|
455
|
+
* It is a tagged struct or class (`Schema.TaggedStruct`, `Schema.TaggedClass`) whose `_tag` is a
|
|
456
|
+
* string literal other than the library's, and that needs no services. The log stores the event in
|
|
457
|
+
* the schema's JSON form.
|
|
458
|
+
*
|
|
459
|
+
* @category models
|
|
460
|
+
* @since 0.3.0
|
|
461
|
+
*/
|
|
462
|
+
export type AppEventSchema = Schema.Codec<{ readonly _tag: string }, { readonly _tag: string }> & {
|
|
463
|
+
readonly fields: { readonly _tag: { readonly ast: SchemaAST.AST } }
|
|
464
|
+
}
|
|
465
|
+
|
|
447
466
|
/**
|
|
448
467
|
* Schema for one of the application's own events as it is stored, with its `_tag` and its fields
|
|
449
468
|
* in JSON.
|
|
450
469
|
*
|
|
451
|
-
*
|
|
452
|
-
*
|
|
470
|
+
* **Details**
|
|
471
|
+
*
|
|
472
|
+
* An assistant made with `events: [Summary]` decodes it to a `Summary` in `CurrentChat`'s
|
|
473
|
+
* `entries` and in its `subscribe`.
|
|
474
|
+
*
|
|
475
|
+
* An event that came in through the assistant's `submit`, from outside a turn, is an input, and the
|
|
476
|
+
* log marks it with a `"~effect-assistant/input": true` key (see {@link isAppInput}); one a turn
|
|
477
|
+
* appended has none. Decoding drops the key, so the app's schemas never see it.
|
|
453
478
|
*
|
|
454
479
|
* @category schemas
|
|
455
480
|
* @since 0.1.0
|
|
456
481
|
*/
|
|
457
|
-
export const StoredAppEvent = Schema.StructWithRest(
|
|
458
|
-
Schema.
|
|
459
|
-
|
|
482
|
+
export const StoredAppEvent = Schema.StructWithRest(
|
|
483
|
+
Schema.Struct({ _tag: AppTag, [inputKey]: Schema.optionalKey(Schema.Literal(true)) }),
|
|
484
|
+
[Schema.Record(Schema.String, Schema.Json)]
|
|
485
|
+
).check(Schema.makeFilter((event) => !libraryTags.has(event._tag) || `${event._tag} is one of the library's events`))
|
|
460
486
|
|
|
461
487
|
/**
|
|
462
488
|
* Represents one of the application's own events, as it is stored.
|
|
@@ -474,6 +500,18 @@ export type StoredAppEvent = typeof StoredAppEvent.Type
|
|
|
474
500
|
*/
|
|
475
501
|
export const isLibraryEvent = (event: { readonly _tag: string }): event is AssistantEvent => libraryTags.has(event._tag)
|
|
476
502
|
|
|
503
|
+
/**
|
|
504
|
+
* Returns whether one of the application's events, as the log stores it, is an input: it came in
|
|
505
|
+
* through the assistant's `submit`, from outside a turn, so storing it woke a turn. An event a turn
|
|
506
|
+
* appended (`CurrentChat`'s `append`) isn't, and neither is a decoded one, which no longer has the
|
|
507
|
+
* mark.
|
|
508
|
+
*
|
|
509
|
+
* @category guards
|
|
510
|
+
* @since 0.3.0
|
|
511
|
+
*/
|
|
512
|
+
export const isAppInput = (event: { readonly _tag: string }): boolean =>
|
|
513
|
+
!isLibraryEvent(event) && Predicate.hasProperty(event, inputKey) && event[inputKey] === true
|
|
514
|
+
|
|
477
515
|
/**
|
|
478
516
|
* Represents a chat's entry with any event in it, either the library's or the application's own
|
|
479
517
|
* (decoded or not).
|
|
@@ -518,8 +556,8 @@ export type Entry = typeof Entry.Type
|
|
|
518
556
|
* Returns whether an event is one of the library's inputs, which are a user message, a block
|
|
519
557
|
* answer, or a finished specialist run.
|
|
520
558
|
*
|
|
521
|
-
* The application's events that
|
|
522
|
-
*
|
|
559
|
+
* The application's events that came in through the assistant's `submit` are inputs too (see
|
|
560
|
+
* {@link isAppInput}).
|
|
523
561
|
*
|
|
524
562
|
* @category guards
|
|
525
563
|
* @since 0.1.0
|
package/src/BlockKit.ts
CHANGED
|
@@ -14,7 +14,7 @@ import * as Predicate from "effect/Predicate"
|
|
|
14
14
|
import * as Schema from "effect/Schema"
|
|
15
15
|
import * as AssistantEvent from "./AssistantEvent.ts"
|
|
16
16
|
import type * as Block from "./Block.ts"
|
|
17
|
-
import type * as
|
|
17
|
+
import type * as Timeline from "./Timeline.ts"
|
|
18
18
|
import { CurrentChat } from "./CurrentChat.ts"
|
|
19
19
|
import * as InputId from "./InputId.ts"
|
|
20
20
|
|
|
@@ -210,8 +210,8 @@ export interface RenderOptions {
|
|
|
210
210
|
}
|
|
211
211
|
|
|
212
212
|
/**
|
|
213
|
-
* Creates a client-side renderer for the blocks on a chat
|
|
214
|
-
* (`
|
|
213
|
+
* Creates a client-side renderer for the blocks on the assistant's messages in a chat's timeline
|
|
214
|
+
* (`Timeline.AssistantMessage`).
|
|
215
215
|
*
|
|
216
216
|
* **Details**
|
|
217
217
|
*
|
|
@@ -241,7 +241,7 @@ export interface RenderOptions {
|
|
|
241
241
|
* order_draft: ({ props }) => <button onClick={() => placeOrder(props)}>Place order</button>
|
|
242
242
|
* })
|
|
243
243
|
*
|
|
244
|
-
* // For each AssistantMessage in the chat's
|
|
244
|
+
* // For each AssistantMessage in the chat's timeline:
|
|
245
245
|
* renderBlock(message, { respond: (answer) => submit(answer) })
|
|
246
246
|
* ```
|
|
247
247
|
*
|
|
@@ -251,13 +251,13 @@ export interface RenderOptions {
|
|
|
251
251
|
export function toClient<Kit extends BlockKit, Out>(
|
|
252
252
|
kit: Kit,
|
|
253
253
|
renderers: Renderers<Kit, Out>
|
|
254
|
-
): (message:
|
|
254
|
+
): (message: Timeline.AssistantMessage, options: RenderOptions) => Out | undefined
|
|
255
255
|
// The overload above checks that every kind has a renderer; here each one takes any block's
|
|
256
256
|
// arguments.
|
|
257
257
|
export function toClient<Out>(
|
|
258
258
|
kit: BlockKit,
|
|
259
259
|
renderers: Readonly<Record<string, (args: RenderArgs<Block.Any>) => Out>>
|
|
260
|
-
): (message:
|
|
260
|
+
): (message: Timeline.AssistantMessage, options: RenderOptions) => Out | undefined {
|
|
261
261
|
return (message, options) => {
|
|
262
262
|
if (message.block === undefined) return undefined
|
|
263
263
|
|
package/src/Compaction.ts
CHANGED
|
@@ -138,10 +138,14 @@ export const attachedFiles = (
|
|
|
138
138
|
* ({@link openItems}), followed by the entries after the summary. When the chat has no summary,
|
|
139
139
|
* the view returns every entry.
|
|
140
140
|
*
|
|
141
|
+
* The app's other events after the summary stay entries, which the model doesn't read: map those
|
|
142
|
+
* it should read to messages after this view, as with any history view (see
|
|
143
|
+
* `Assistant.HistoryView`). A summary the handler appends wakes no turn.
|
|
144
|
+
*
|
|
141
145
|
* **Example** (Compacting a long chat after each turn)
|
|
142
146
|
*
|
|
143
147
|
* ```ts
|
|
144
|
-
* import {
|
|
148
|
+
* import { Assistant, type AssistantEvent, Compaction } from "effect-assistant"
|
|
145
149
|
* import * as LanguageModel from "effect/ai/LanguageModel"
|
|
146
150
|
* import * as Effect from "effect/Effect"
|
|
147
151
|
* import * as Schema from "effect/Schema"
|
|
@@ -154,7 +158,7 @@ export const attachedFiles = (
|
|
|
154
158
|
*
|
|
155
159
|
* declare const transcript: (entries: ReadonlyArray<AssistantEvent.AnyEntry>) => string
|
|
156
160
|
*
|
|
157
|
-
* export const MyAssistant = Assistant.make("my-assistant", { events: [
|
|
161
|
+
* export const MyAssistant = Assistant.make("my-assistant", { events: [Summary] })
|
|
158
162
|
*
|
|
159
163
|
* const MyAssistantLive = MyAssistant.toLayer((turn) => {
|
|
160
164
|
* const compact = Effect.gen(function* () {
|
package/src/CurrentChat.ts
CHANGED
|
@@ -40,7 +40,8 @@ export class CurrentChat extends Context.Service<
|
|
|
40
40
|
/**
|
|
41
41
|
* Appends events after the chat's last entry and sends them to the chat's subscribers. The
|
|
42
42
|
* events can be the library's or the assistant's own (registered with its `events` option),
|
|
43
|
-
* such as a summary. An event
|
|
43
|
+
* such as a summary. An app event appended here wakes no turn: only one that comes in through
|
|
44
|
+
* the assistant's `submit` does.
|
|
44
45
|
*/
|
|
45
46
|
readonly append: (...events: ReadonlyArray<{ readonly _tag: string }>) => Effect.Effect<void>
|
|
46
47
|
}
|