effect-assistant 0.2.0 → 0.4.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 +77 -37
- 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 +104 -11
- package/dist/AssistantEvent.d.ts.map +1 -1
- package/dist/AssistantEvent.js +88 -13
- package/dist/AssistantEvent.js.map +1 -1
- package/dist/AssistantLoop.d.ts +4 -2
- package/dist/AssistantLoop.d.ts.map +1 -1
- package/dist/AssistantLoop.js +4 -2
- package/dist/AssistantLoop.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 +7 -3
- package/dist/Compaction.d.ts.map +1 -1
- package/dist/Compaction.js +19 -8
- 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 +86 -11
- package/dist/Skill.d.ts.map +1 -1
- package/dist/Skill.js +93 -18
- package/dist/Skill.js.map +1 -1
- package/dist/Specialist.d.ts +7 -7
- package/dist/Specialist.d.ts.map +1 -1
- package/dist/Specialist.js.map +1 -1
- package/dist/Timeline.d.ts +516 -0
- package/dist/Timeline.d.ts.map +1 -0
- package/dist/Timeline.js +631 -0
- package/dist/Timeline.js.map +1 -0
- package/dist/ToolApproval.d.ts +146 -0
- package/dist/ToolApproval.d.ts.map +1 -0
- package/dist/ToolApproval.js +201 -0
- package/dist/ToolApproval.js.map +1 -0
- package/dist/ToolDiscovery.d.ts +6 -4
- package/dist/ToolDiscovery.d.ts.map +1 -1
- package/dist/ToolDiscovery.js +5 -4
- package/dist/ToolDiscovery.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +4 -4
- package/dist/internal/chat.js +9 -7
- 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 +53 -22
- 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 +10 -5
- package/dist/internal/loop.d.ts.map +1 -1
- package/dist/internal/loop.js +55 -22
- package/dist/internal/loop.js.map +1 -1
- package/dist/internal/specialists.js +10 -16
- package/dist/internal/specialists.js.map +1 -1
- package/dist/internal/toolkit.d.ts +5 -7
- package/dist/internal/toolkit.d.ts.map +1 -1
- package/dist/internal/toolkit.js +28 -18
- package/dist/internal/toolkit.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 +35 -42
- package/dist/internal/turns.js.map +1 -1
- package/package.json +1 -1
- package/src/Assistant.ts +80 -40
- package/src/AssistantActivity.ts +11 -18
- package/src/AssistantEngine.ts +110 -43
- package/src/AssistantEvent.ts +106 -14
- package/src/AssistantLoop.ts +4 -2
- package/src/BlockKit.ts +6 -6
- package/src/Compaction.ts +25 -4
- package/src/CurrentChat.ts +2 -1
- package/src/Skill.ts +133 -23
- package/src/Specialist.ts +9 -9
- package/src/Timeline.ts +947 -0
- package/src/ToolApproval.ts +249 -0
- package/src/ToolDiscovery.ts +8 -3
- package/src/index.ts +10 -10
- package/src/internal/chat.ts +15 -5
- package/src/internal/events.ts +43 -43
- package/src/internal/history.ts +94 -32
- package/src/internal/input.ts +3 -0
- package/src/internal/loop.ts +120 -34
- package/src/internal/specialists.ts +7 -12
- package/src/internal/toolkit.ts +47 -30
- package/src/internal/turns.ts +52 -58
- 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/dist/internal/skills.js +0 -28
- package/dist/internal/skills.js.map +0 -1
- package/src/AppEvent.ts +0 -134
- package/src/ChatView.ts +0 -587
- package/src/internal/skills.ts +0 -37
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
|
/**
|
|
@@ -138,13 +140,30 @@ export class SpecialistFinished extends Schema.TaggedClass<SpecialistFinished>()
|
|
|
138
140
|
error: Schema.optional(Schema.String)
|
|
139
141
|
}) {}
|
|
140
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Represents the user's decision on a tool call that waits for their approval: `"approved"`, so
|
|
145
|
+
* the call runs, or `"rejected"`, with an optional short reason the model reads.
|
|
146
|
+
*
|
|
147
|
+
* Its id is fixed per call (see `ToolApproval.approve`), so a decision is stored once. A decision on
|
|
148
|
+
* a call that isn't waiting any more (decided, or expired) isn't stored.
|
|
149
|
+
*
|
|
150
|
+
* @category schemas
|
|
151
|
+
* @since 0.4.0
|
|
152
|
+
*/
|
|
153
|
+
export class ApprovalDecision extends Schema.TaggedClass<ApprovalDecision>()("ApprovalDecision", {
|
|
154
|
+
id: InputId,
|
|
155
|
+
callId: Schema.String,
|
|
156
|
+
decision: Schema.Literals(["approved", "rejected"]),
|
|
157
|
+
reason: Schema.optional(Schema.String.check(Schema.isMaxLength(500)))
|
|
158
|
+
}) {}
|
|
159
|
+
|
|
141
160
|
/**
|
|
142
161
|
* Schema for every input `Assistant.submit` accepts.
|
|
143
162
|
*
|
|
144
163
|
* @category schemas
|
|
145
164
|
* @since 0.1.0
|
|
146
165
|
*/
|
|
147
|
-
export const Input = Schema.Union([UserMessage, BlockAnswer, SpecialistFinished])
|
|
166
|
+
export const Input = Schema.Union([UserMessage, BlockAnswer, SpecialistFinished, ApprovalDecision])
|
|
148
167
|
|
|
149
168
|
/**
|
|
150
169
|
* Represents an input to an assistant.
|
|
@@ -155,7 +174,8 @@ export const Input = Schema.Union([UserMessage, BlockAnswer, SpecialistFinished]
|
|
|
155
174
|
export type Input = typeof Input.Type
|
|
156
175
|
|
|
157
176
|
/**
|
|
158
|
-
* Schema for the inputs from the chat's user, which are messages and
|
|
177
|
+
* Schema for the inputs from the chat's user, which are messages, block answers, and decisions on
|
|
178
|
+
* tool calls that wait for their approval.
|
|
159
179
|
*
|
|
160
180
|
* A turn owes each one a reply. It is also what a chat client usually may submit, since the app's
|
|
161
181
|
* events come from the server.
|
|
@@ -163,7 +183,7 @@ export type Input = typeof Input.Type
|
|
|
163
183
|
* @category schemas
|
|
164
184
|
* @since 0.1.0
|
|
165
185
|
*/
|
|
166
|
-
export const UserInput = Schema.Union([UserMessage, BlockAnswer])
|
|
186
|
+
export const UserInput = Schema.Union([UserMessage, BlockAnswer, ApprovalDecision])
|
|
167
187
|
|
|
168
188
|
/**
|
|
169
189
|
* Represents an input from the chat's user.
|
|
@@ -398,6 +418,36 @@ export class BlocksClosed extends Schema.TaggedClass<BlocksClosed>()("BlocksClos
|
|
|
398
418
|
messageIds: Schema.Array(Schema.String)
|
|
399
419
|
}) {}
|
|
400
420
|
|
|
421
|
+
/**
|
|
422
|
+
* Represents a tool call that waits for the user's approval instead of running: its tool needs
|
|
423
|
+
* approval (Effect's `needsApproval`).
|
|
424
|
+
*
|
|
425
|
+
* **Details**
|
|
426
|
+
*
|
|
427
|
+
* It holds the call's id, the tool, and its params, in JSON, as the model wrote them. The model
|
|
428
|
+
* reads it as the call's result, saying it waits for the user; the user's
|
|
429
|
+
* {@link ApprovalDecision} decides it, and an approved call runs then, with the same id.
|
|
430
|
+
*
|
|
431
|
+
* @category schemas
|
|
432
|
+
* @since 0.4.0
|
|
433
|
+
*/
|
|
434
|
+
export class ApprovalRequested extends Schema.TaggedClass<ApprovalRequested>()("ApprovalRequested", {
|
|
435
|
+
callId: Schema.String,
|
|
436
|
+
tool: Schema.String,
|
|
437
|
+
params: Schema.Json
|
|
438
|
+
}) {}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Represents tool calls that waited for the user's approval and won't run: the user moved on, like
|
|
442
|
+
* the ones `ToolApproval.closeStale` expires. The model reads that they weren't approved.
|
|
443
|
+
*
|
|
444
|
+
* @category schemas
|
|
445
|
+
* @since 0.4.0
|
|
446
|
+
*/
|
|
447
|
+
export class ApprovalsExpired extends Schema.TaggedClass<ApprovalsExpired>()("ApprovalsExpired", {
|
|
448
|
+
callIds: Schema.Array(Schema.String)
|
|
449
|
+
}) {}
|
|
450
|
+
|
|
401
451
|
/**
|
|
402
452
|
* Schema for every event the library defines.
|
|
403
453
|
*
|
|
@@ -417,6 +467,9 @@ export const AssistantEvent = Schema.Union([
|
|
|
417
467
|
SpecialistStarted,
|
|
418
468
|
SpecialistFinished,
|
|
419
469
|
BlocksClosed,
|
|
470
|
+
ApprovalRequested,
|
|
471
|
+
ApprovalDecision,
|
|
472
|
+
ApprovalsExpired,
|
|
420
473
|
ChatDeleted
|
|
421
474
|
])
|
|
422
475
|
|
|
@@ -444,19 +497,43 @@ const libraryTags: ReadonlySet<string> = new Set(
|
|
|
444
497
|
*/
|
|
445
498
|
export const AppTag = Schema.String.pipe(Schema.brand("~effect-assistant/AppTag"))
|
|
446
499
|
|
|
500
|
+
/**
|
|
501
|
+
* Represents the schema of one of the application's own events, as `Assistant.make`'s `events`
|
|
502
|
+
* takes it.
|
|
503
|
+
*
|
|
504
|
+
* **Details**
|
|
505
|
+
*
|
|
506
|
+
* It is a tagged struct or class (`Schema.TaggedStruct`, `Schema.TaggedClass`) whose `_tag` is a
|
|
507
|
+
* string literal other than the library's, and that needs no services. The log stores the event in
|
|
508
|
+
* the schema's JSON form.
|
|
509
|
+
*
|
|
510
|
+
* @category models
|
|
511
|
+
* @since 0.3.0
|
|
512
|
+
*/
|
|
513
|
+
export type AppEventSchema = Schema.Codec<{ readonly _tag: string }, { readonly _tag: string }> & {
|
|
514
|
+
readonly fields: { readonly _tag: { readonly ast: SchemaAST.AST } }
|
|
515
|
+
}
|
|
516
|
+
|
|
447
517
|
/**
|
|
448
518
|
* Schema for one of the application's own events as it is stored, with its `_tag` and its fields
|
|
449
519
|
* in JSON.
|
|
450
520
|
*
|
|
451
|
-
*
|
|
452
|
-
*
|
|
521
|
+
* **Details**
|
|
522
|
+
*
|
|
523
|
+
* An assistant made with `events: [Summary]` decodes it to a `Summary` in `CurrentChat`'s
|
|
524
|
+
* `entries` and in its `subscribe`.
|
|
525
|
+
*
|
|
526
|
+
* An event that came in through the assistant's `submit`, from outside a turn, is an input, and the
|
|
527
|
+
* log marks it with a `"~effect-assistant/input": true` key (see {@link isAppInput}); one a turn
|
|
528
|
+
* appended has none. Decoding drops the key, so the app's schemas never see it.
|
|
453
529
|
*
|
|
454
530
|
* @category schemas
|
|
455
531
|
* @since 0.1.0
|
|
456
532
|
*/
|
|
457
|
-
export const StoredAppEvent = Schema.StructWithRest(
|
|
458
|
-
Schema.
|
|
459
|
-
|
|
533
|
+
export const StoredAppEvent = Schema.StructWithRest(
|
|
534
|
+
Schema.Struct({ _tag: AppTag, [inputKey]: Schema.optionalKey(Schema.Literal(true)) }),
|
|
535
|
+
[Schema.Record(Schema.String, Schema.Json)]
|
|
536
|
+
).check(Schema.makeFilter((event) => !libraryTags.has(event._tag) || `${event._tag} is one of the library's events`))
|
|
460
537
|
|
|
461
538
|
/**
|
|
462
539
|
* Represents one of the application's own events, as it is stored.
|
|
@@ -474,6 +551,18 @@ export type StoredAppEvent = typeof StoredAppEvent.Type
|
|
|
474
551
|
*/
|
|
475
552
|
export const isLibraryEvent = (event: { readonly _tag: string }): event is AssistantEvent => libraryTags.has(event._tag)
|
|
476
553
|
|
|
554
|
+
/**
|
|
555
|
+
* Returns whether one of the application's events, as the log stores it, is an input: it came in
|
|
556
|
+
* through the assistant's `submit`, from outside a turn, so storing it woke a turn. An event a turn
|
|
557
|
+
* appended (`CurrentChat`'s `append`) isn't, and neither is a decoded one, which no longer has the
|
|
558
|
+
* mark.
|
|
559
|
+
*
|
|
560
|
+
* @category guards
|
|
561
|
+
* @since 0.3.0
|
|
562
|
+
*/
|
|
563
|
+
export const isAppInput = (event: { readonly _tag: string }): boolean =>
|
|
564
|
+
!isLibraryEvent(event) && Predicate.hasProperty(event, inputKey) && event[inputKey] === true
|
|
565
|
+
|
|
477
566
|
/**
|
|
478
567
|
* Represents a chat's entry with any event in it, either the library's or the application's own
|
|
479
568
|
* (decoded or not).
|
|
@@ -516,10 +605,10 @@ export type Entry = typeof Entry.Type
|
|
|
516
605
|
|
|
517
606
|
/**
|
|
518
607
|
* Returns whether an event is one of the library's inputs, which are a user message, a block
|
|
519
|
-
* answer,
|
|
608
|
+
* answer, a finished specialist run, or a decision on a tool call waiting for approval.
|
|
520
609
|
*
|
|
521
|
-
* The application's events that
|
|
522
|
-
*
|
|
610
|
+
* The application's events that came in through the assistant's `submit` are inputs too (see
|
|
611
|
+
* {@link isAppInput}).
|
|
523
612
|
*
|
|
524
613
|
* @category guards
|
|
525
614
|
* @since 0.1.0
|
|
@@ -527,7 +616,8 @@ export type Entry = typeof Entry.Type
|
|
|
527
616
|
export const isInput = (event: { readonly _tag: string }): event is Input =>
|
|
528
617
|
Predicate.isTagged(event, "UserMessage") ||
|
|
529
618
|
Predicate.isTagged(event, "BlockAnswer") ||
|
|
530
|
-
Predicate.isTagged(event, "SpecialistFinished")
|
|
619
|
+
Predicate.isTagged(event, "SpecialistFinished") ||
|
|
620
|
+
Predicate.isTagged(event, "ApprovalDecision")
|
|
531
621
|
|
|
532
622
|
/**
|
|
533
623
|
* Returns whether an input starts a turn, which every input does except a run the assistant
|
|
@@ -546,7 +636,9 @@ export const isWaking = (input: Input): boolean =>
|
|
|
546
636
|
* @since 0.1.0
|
|
547
637
|
*/
|
|
548
638
|
export const isUserInput = (input: Input): input is UserInput =>
|
|
549
|
-
Predicate.isTagged(input, "UserMessage") ||
|
|
639
|
+
Predicate.isTagged(input, "UserMessage") ||
|
|
640
|
+
Predicate.isTagged(input, "BlockAnswer") ||
|
|
641
|
+
Predicate.isTagged(input, "ApprovalDecision")
|
|
550
642
|
|
|
551
643
|
/**
|
|
552
644
|
* Returns whether an event ends a turn, and so says how far the assistant has read.
|
package/src/AssistantLoop.ts
CHANGED
|
@@ -161,8 +161,10 @@ const toolsOf = <T extends Record<string, Tool.Any>>(toolkit: Toolkit.Toolkit<T>
|
|
|
161
161
|
* the step's result and decides what comes next, with `AssistantLoop.next(state)` or
|
|
162
162
|
* `AssistantLoop.stop()`.
|
|
163
163
|
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
164
|
+
* In a turn's loop, a call to a tool with Effect's `needsApproval` doesn't run: the step emits an
|
|
165
|
+
* `ApprovalRequested` for it, and ends the turn, which waits for the user (see `ToolApproval`). A
|
|
166
|
+
* call the user approved runs at the start of the next step, before the model is called. A
|
|
167
|
+
* specialist's run can't wait for the user: such a call fails, and the run reads why.
|
|
166
168
|
*
|
|
167
169
|
* @category constructors
|
|
168
170
|
* @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
|
@@ -18,6 +18,8 @@ import * as AssistantActivity from "./AssistantActivity.ts"
|
|
|
18
18
|
import * as AssistantEvent from "./AssistantEvent.ts"
|
|
19
19
|
import * as BlockKit from "./BlockKit.ts"
|
|
20
20
|
import { userText } from "./internal/history.ts"
|
|
21
|
+
import { toJsonText } from "./internal/schema.ts"
|
|
22
|
+
import * as ToolApproval from "./ToolApproval.ts"
|
|
21
23
|
|
|
22
24
|
/**
|
|
23
25
|
* Represents the fields a summary event must have: its text and the last `seq` it covers.
|
|
@@ -54,7 +56,7 @@ export const inputTokens = (entries: ReadonlyArray<AssistantEvent.AnyEntry>): nu
|
|
|
54
56
|
|
|
55
57
|
/**
|
|
56
58
|
* Returns the items that were open at `throughSeq` and are still open: blocks the user hasn't
|
|
57
|
-
* answered,
|
|
59
|
+
* answered, specialists that are working, and tool calls waiting for the user's approval. The result is text for
|
|
58
60
|
* the model, or `None` when nothing is open.
|
|
59
61
|
*
|
|
60
62
|
* @category utils
|
|
@@ -100,9 +102,24 @@ export const openItems = (
|
|
|
100
102
|
: []
|
|
101
103
|
)
|
|
102
104
|
|
|
105
|
+
const approvals = [...ToolApproval.fromEntries(entries).values()].flatMap((approval) =>
|
|
106
|
+
approval.status === "pending" &&
|
|
107
|
+
opened(
|
|
108
|
+
seqOf(
|
|
109
|
+
(event) =>
|
|
110
|
+
AssistantEvent.isLibraryEvent(event) &&
|
|
111
|
+
Predicate.isTagged(event, "ApprovalRequested") &&
|
|
112
|
+
event.callId === approval.request.callId
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
? [`- your call to ${approval.request.tool} (${approval.request.callId}): ${toJsonText(approval.request.params)}`]
|
|
116
|
+
: []
|
|
117
|
+
)
|
|
118
|
+
|
|
103
119
|
const sections = [
|
|
104
120
|
blocks.length > 0 ? `Blocks the user hasn't answered:\n${blocks.join("\n")}` : "",
|
|
105
|
-
specialists.length > 0 ? `Specialists still working:\n${specialists.join("\n")}` : ""
|
|
121
|
+
specialists.length > 0 ? `Specialists still working:\n${specialists.join("\n")}` : "",
|
|
122
|
+
approvals.length > 0 ? `Calls waiting for the user's approval:\n${approvals.join("\n")}` : ""
|
|
106
123
|
].filter((section) => section.length > 0)
|
|
107
124
|
|
|
108
125
|
return sections.length === 0 ? Option.none() : Option.some(sections.join("\n\n"))
|
|
@@ -138,10 +155,14 @@ export const attachedFiles = (
|
|
|
138
155
|
* ({@link openItems}), followed by the entries after the summary. When the chat has no summary,
|
|
139
156
|
* the view returns every entry.
|
|
140
157
|
*
|
|
158
|
+
* The app's other events after the summary stay entries, which the model doesn't read: map those
|
|
159
|
+
* it should read to messages after this view, as with any history view (see
|
|
160
|
+
* `Assistant.HistoryView`). A summary the handler appends wakes no turn.
|
|
161
|
+
*
|
|
141
162
|
* **Example** (Compacting a long chat after each turn)
|
|
142
163
|
*
|
|
143
164
|
* ```ts
|
|
144
|
-
* import {
|
|
165
|
+
* import { Assistant, type AssistantEvent, Compaction } from "effect-assistant"
|
|
145
166
|
* import * as LanguageModel from "effect/ai/LanguageModel"
|
|
146
167
|
* import * as Effect from "effect/Effect"
|
|
147
168
|
* import * as Schema from "effect/Schema"
|
|
@@ -154,7 +175,7 @@ export const attachedFiles = (
|
|
|
154
175
|
*
|
|
155
176
|
* declare const transcript: (entries: ReadonlyArray<AssistantEvent.AnyEntry>) => string
|
|
156
177
|
*
|
|
157
|
-
* export const MyAssistant = Assistant.make("my-assistant", { events: [
|
|
178
|
+
* export const MyAssistant = Assistant.make("my-assistant", { events: [Summary] })
|
|
158
179
|
*
|
|
159
180
|
* const MyAssistantLive = MyAssistant.toLayer((turn) => {
|
|
160
181
|
* 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
|
}
|
package/src/Skill.ts
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Defines skills, which are playbooks with step-by-step instructions for a kind of task.
|
|
3
3
|
*
|
|
4
|
-
* A
|
|
5
|
-
*
|
|
6
|
-
* task matches.
|
|
4
|
+
* A list of skills becomes a toolkit with one tool, `load_skill` ({@link toolkit}). Its
|
|
5
|
+
* description is the catalog, each skill's name and when to use it, and the model calls it to read
|
|
6
|
+
* a skill's instructions when a task matches. Merge it into a loop's toolkit like any other.
|
|
7
7
|
*
|
|
8
8
|
* @since 0.1.0
|
|
9
9
|
*/
|
|
10
|
-
import
|
|
10
|
+
import * as Tool from "effect/ai/Tool"
|
|
11
|
+
import * as AiToolkit from "effect/ai/Toolkit"
|
|
12
|
+
import * as Effect from "effect/Effect"
|
|
13
|
+
import type * as Layer from "effect/Layer"
|
|
14
|
+
import * as Schema from "effect/Schema"
|
|
15
|
+
import * as Instructions from "./Instructions.ts"
|
|
16
|
+
import * as ToolFailure from "./ToolFailure.ts"
|
|
11
17
|
|
|
12
18
|
/**
|
|
13
19
|
* Represents a skill.
|
|
@@ -32,7 +38,7 @@ export interface Skill<R = never> {
|
|
|
32
38
|
*
|
|
33
39
|
* Write the description as a trigger ("Use when…"), since it's all the model sees until it loads
|
|
34
40
|
* the skill. The instructions are text, or an Effect that is resolved each time the skill is
|
|
35
|
-
* loaded.
|
|
41
|
+
* loaded. Offer skills to a loop with {@link toolkit}.
|
|
36
42
|
*
|
|
37
43
|
* **Example** (Defining a playbook for ordering campaigns)
|
|
38
44
|
*
|
|
@@ -45,10 +51,6 @@ export interface Skill<R = never> {
|
|
|
45
51
|
* 2. You need who it should reach and a budget. Ask for what's missing, one question at a time.
|
|
46
52
|
* 3. When you have both, call start_campaign.`
|
|
47
53
|
* })
|
|
48
|
-
*
|
|
49
|
-
* const MyAssistantLive = MyAssistant.toLayer((turn) =>
|
|
50
|
-
* turn.loop({ instructions, skills: [orderingCampaigns] })
|
|
51
|
-
* )
|
|
52
54
|
* ```
|
|
53
55
|
*
|
|
54
56
|
* @category constructors
|
|
@@ -72,20 +74,128 @@ export const make = <R = never>(
|
|
|
72
74
|
*/
|
|
73
75
|
export type Services<S> = S extends Skill<infer R> ? R : never
|
|
74
76
|
|
|
77
|
+
const loadSkill = (description: string) =>
|
|
78
|
+
Tool.make("load_skill", {
|
|
79
|
+
description,
|
|
80
|
+
parameters: Schema.Struct({ name: Schema.String }),
|
|
81
|
+
success: Schema.String,
|
|
82
|
+
failure: ToolFailure.ToolFailure
|
|
83
|
+
})
|
|
84
|
+
|
|
75
85
|
/**
|
|
76
|
-
*
|
|
77
|
-
* skills.
|
|
86
|
+
* Represents the `load_skill` tool, which reads a skill's instructions by name.
|
|
78
87
|
*
|
|
79
|
-
* @category
|
|
80
|
-
* @since 0.
|
|
88
|
+
* @category models
|
|
89
|
+
* @since 0.4.0
|
|
90
|
+
*/
|
|
91
|
+
export type LoadSkill = ReturnType<typeof loadSkill>
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Represents a list of skills as a toolkit: the `load_skill` tool, and the layer with its handler.
|
|
95
|
+
*
|
|
96
|
+
* `R` is the services the skills' instructions need, which the layer requires.
|
|
97
|
+
*
|
|
98
|
+
* @category models
|
|
99
|
+
* @since 0.4.0
|
|
100
|
+
*/
|
|
101
|
+
export interface Toolkit<R = never> extends AiToolkit.Toolkit<{ readonly load_skill: LoadSkill }> {
|
|
102
|
+
/** The handler of `load_skill`, which resolves the instructions of these skills. */
|
|
103
|
+
readonly layer: Layer.Layer<Tool.Handler<"load_skill">, never, R>
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const catalog = (skills: ReadonlyArray<Skill<unknown>>): string =>
|
|
107
|
+
[
|
|
108
|
+
"Load a skill, a playbook for a kind of task, by name, and follow it for the rest of the task. When one matches what you're doing, load it in the same step as your first lookups, not in a step of its own.",
|
|
109
|
+
"",
|
|
110
|
+
"Skills:",
|
|
111
|
+
...skills.map((skill) => `- ${skill.name}: ${skill.description}`)
|
|
112
|
+
].join("\n")
|
|
113
|
+
|
|
114
|
+
function toolkitOf<const Skills extends ReadonlyArray<Skill<unknown>>>(
|
|
115
|
+
skills: Skills
|
|
116
|
+
): Toolkit<Services<Skills[number]>>
|
|
117
|
+
// The overload states what the layer requires: the services of these skills' instructions.
|
|
118
|
+
function toolkitOf(skills: ReadonlyArray<Skill<unknown>>): Toolkit<unknown> {
|
|
119
|
+
const byName = new Map(skills.map((skill) => [skill.name, skill]))
|
|
120
|
+
const tools = AiToolkit.make(loadSkill(catalog(skills)))
|
|
121
|
+
|
|
122
|
+
const layer = tools.toLayer(
|
|
123
|
+
Effect.gen(function* () {
|
|
124
|
+
const context = yield* Effect.context<unknown>()
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
load_skill: Effect.fnUntraced(function* ({ name }) {
|
|
128
|
+
const skill = byName.get(name)
|
|
129
|
+
|
|
130
|
+
if (skill === undefined) {
|
|
131
|
+
return yield* ToolFailure.fail(
|
|
132
|
+
`No skill named ${name}. The skills are: ${skills.map((known) => known.name).join(", ")}.`
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const resolved = yield* Instructions.resolve(skill.instructions).pipe(Effect.provideContext(context))
|
|
137
|
+
|
|
138
|
+
return resolved.text
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
})
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
return Object.assign(tools, { layer })
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Creates a toolkit from a list of skills: the `load_skill` tool, whose description lists each
|
|
149
|
+
* skill's name and when to use it, and its handler's `layer`.
|
|
150
|
+
*
|
|
151
|
+
* **Details**
|
|
152
|
+
*
|
|
153
|
+
* Merge the toolkit into a loop's own with `Toolkit.merge`, and provide its `layer`, which requires
|
|
154
|
+
* the services the skills' instructions need. The model loads a skill by name; a name that isn't
|
|
155
|
+
* in the list is a failure the model reads, with the names there are.
|
|
156
|
+
*
|
|
157
|
+
* The handler resolves only the skills of its own list. To offer each user the skills they may
|
|
158
|
+
* use, build the toolkit in the turn from that user's list, and provide its layer to the loop's
|
|
159
|
+
* stream (`Stream.provide(skills.layer)`): building one does no work.
|
|
160
|
+
*
|
|
161
|
+
* **Example** (Giving an assistant its playbooks)
|
|
162
|
+
*
|
|
163
|
+
* ```ts
|
|
164
|
+
* import { Skill } from "effect-assistant"
|
|
165
|
+
* import * as Layer from "effect/Layer"
|
|
166
|
+
* import * as Toolkit from "effect/ai/Toolkit"
|
|
167
|
+
*
|
|
168
|
+
* const AssistantSkills = Skill.toolkit([orderingCampaigns, sharingAssets])
|
|
169
|
+
*
|
|
170
|
+
* const MyAssistantLive = MyAssistant.toLayer((turn) =>
|
|
171
|
+
* turn.loop({ instructions, toolkit: Toolkit.merge(MyTools, AssistantSkills) })
|
|
172
|
+
* ).pipe(Layer.provide([MyToolsLive, AssistantSkills.layer]))
|
|
173
|
+
* ```
|
|
174
|
+
*
|
|
175
|
+
* **Example** (Offering each user the skills their role allows)
|
|
176
|
+
*
|
|
177
|
+
* ```ts
|
|
178
|
+
* import { Skill } from "effect-assistant"
|
|
179
|
+
* import * as Effect from "effect/Effect"
|
|
180
|
+
* import * as Stream from "effect/Stream"
|
|
181
|
+
* import * as Toolkit from "effect/ai/Toolkit"
|
|
182
|
+
*
|
|
183
|
+
* const MyAssistantLive = MyAssistant.toLayer((turn) =>
|
|
184
|
+
* Stream.unwrap(
|
|
185
|
+
* Effect.map(roleOf(turn.chatId), (role) => {
|
|
186
|
+
* const skills = Skill.toolkit(allSkills.filter((skill) => allowed(role, skill)))
|
|
187
|
+
*
|
|
188
|
+
* return turn
|
|
189
|
+
* .loop({ instructions, toolkit: Toolkit.merge(MyTools, skills) })
|
|
190
|
+
* .pipe(Stream.provide(skills.layer))
|
|
191
|
+
* })
|
|
192
|
+
* )
|
|
193
|
+
* )
|
|
194
|
+
* ```
|
|
195
|
+
*
|
|
196
|
+
* @category constructors
|
|
197
|
+
* @since 0.4.0
|
|
81
198
|
*/
|
|
82
|
-
export const
|
|
83
|
-
skills
|
|
84
|
-
|
|
85
|
-
: [
|
|
86
|
-
prompt,
|
|
87
|
-
"",
|
|
88
|
-
"## Skills",
|
|
89
|
-
"Playbooks for common tasks. When one matches what you're doing, load it with load_skill in the same step as your first lookups, and follow it.",
|
|
90
|
-
...skills.map((skill) => `- ${skill.name}: ${skill.description}`)
|
|
91
|
-
].join("\n")
|
|
199
|
+
export const toolkit: <const Skills extends ReadonlyArray<Skill<unknown>>>(
|
|
200
|
+
skills: Skills
|
|
201
|
+
) => Toolkit<Services<Skills[number]>> = toolkitOf
|
package/src/Specialist.ts
CHANGED
|
@@ -23,7 +23,6 @@ import type { ChatId } from "./ChatId.ts"
|
|
|
23
23
|
import type { CurrentChat } from "./CurrentChat.ts"
|
|
24
24
|
import type * as Instructions from "./Instructions.ts"
|
|
25
25
|
import type { RunId } from "./RunId.ts"
|
|
26
|
-
import type { Skill } from "./Skill.ts"
|
|
27
26
|
|
|
28
27
|
const TypeId = "~effect-assistant/Specialist"
|
|
29
28
|
|
|
@@ -123,12 +122,13 @@ export type RunServices = CurrentChat | Runner | LanguageModel.LanguageModel
|
|
|
123
122
|
* @category models
|
|
124
123
|
* @since 0.1.0
|
|
125
124
|
*/
|
|
126
|
-
export interface LoopOptions<T extends Record<string, Tool.Any> = Record<never, never
|
|
125
|
+
export interface LoopOptions<T extends Record<string, Tool.Any> = Record<never, never>> {
|
|
127
126
|
readonly instructions: string | Instructions.Resolved
|
|
128
|
-
/**
|
|
127
|
+
/**
|
|
128
|
+
* The loop's tools, as a native toolkit whose handlers come from the layer. Merge in a
|
|
129
|
+
* `Skill.toolkit(...)` for playbooks.
|
|
130
|
+
*/
|
|
129
131
|
readonly toolkit?: Toolkit.Toolkit<T> | undefined
|
|
130
|
-
/** Playbooks, listed as a catalog in the prompt and loaded with `load_skill` when one matches. */
|
|
131
|
-
readonly skills?: ReadonlyArray<Skill<RS>> | undefined
|
|
132
132
|
/** The model calls allowed per brief. Defaults to 8. */
|
|
133
133
|
readonly maxSteps?: number | undefined
|
|
134
134
|
}
|
|
@@ -149,13 +149,13 @@ export interface Run {
|
|
|
149
149
|
* (follow-ups and steering). Read it before every model call, as the loop does.
|
|
150
150
|
*/
|
|
151
151
|
readonly history: Effect.Effect<ReadonlyArray<Prompt.Message>>
|
|
152
|
-
/** Runs the agent loop over the run's history, with the given instructions
|
|
153
|
-
readonly loop: <T extends Record<string, Tool.Any> = Record<never, never
|
|
154
|
-
options: LoopOptions<T
|
|
152
|
+
/** Runs the agent loop over the run's history, with the given instructions and tools. */
|
|
153
|
+
readonly loop: <T extends Record<string, Tool.Any> = Record<never, never>>(
|
|
154
|
+
options: LoopOptions<T>
|
|
155
155
|
) => Stream.Stream<
|
|
156
156
|
AssistantLoop.Event,
|
|
157
157
|
AiError.AiError,
|
|
158
|
-
LanguageModel.LanguageModel | AssistantLoop.ToolkitServices<T>
|
|
158
|
+
LanguageModel.LanguageModel | AssistantLoop.ToolkitServices<T>
|
|
159
159
|
>
|
|
160
160
|
}
|
|
161
161
|
|