effect-assistant 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +22 -2
- package/dist/AppEvent.d.ts +111 -0
- package/dist/AppEvent.d.ts.map +1 -0
- package/dist/AppEvent.js +71 -0
- package/dist/AppEvent.js.map +1 -0
- package/dist/Assistant.d.ts +403 -0
- package/dist/Assistant.d.ts.map +1 -0
- package/dist/Assistant.js +126 -0
- package/dist/Assistant.js.map +1 -0
- package/dist/AssistantActivity.d.ts +85 -0
- package/dist/AssistantActivity.d.ts.map +1 -0
- package/dist/AssistantActivity.js +105 -0
- package/dist/AssistantActivity.js.map +1 -0
- package/dist/AssistantEngine.d.ts +116 -0
- package/dist/AssistantEngine.d.ts.map +1 -0
- package/dist/AssistantEngine.js +255 -0
- package/dist/AssistantEngine.js.map +1 -0
- package/dist/AssistantEvent.d.ts +551 -0
- package/dist/AssistantEvent.d.ts.map +1 -0
- package/dist/AssistantEvent.js +473 -0
- package/dist/AssistantEvent.js.map +1 -0
- package/dist/AssistantLoop.d.ts +173 -0
- package/dist/AssistantLoop.d.ts.map +1 -0
- package/dist/AssistantLoop.js +111 -0
- package/dist/AssistantLoop.js.map +1 -0
- package/dist/AssistantToolCall.d.ts +32 -0
- package/dist/AssistantToolCall.d.ts.map +1 -0
- package/dist/AssistantToolCall.js +37 -0
- package/dist/AssistantToolCall.js.map +1 -0
- package/dist/Block.d.ts +69 -0
- package/dist/Block.d.ts.map +1 -0
- package/dist/Block.js +39 -0
- package/dist/Block.js.map +1 -0
- package/dist/BlockKit.d.ts +180 -0
- package/dist/BlockKit.d.ts.map +1 -0
- package/dist/BlockKit.js +148 -0
- package/dist/BlockKit.js.map +1 -0
- package/dist/ChatId.d.ts +31 -0
- package/dist/ChatId.d.ts.map +1 -0
- package/dist/ChatId.js +36 -0
- package/dist/ChatId.js.map +1 -0
- package/dist/ChatLog.d.ts +109 -0
- package/dist/ChatLog.d.ts.map +1 -0
- package/dist/ChatLog.js +177 -0
- package/dist/ChatLog.js.map +1 -0
- package/dist/ChatView.d.ts +408 -0
- package/dist/ChatView.d.ts.map +1 -0
- package/dist/ChatView.js +443 -0
- package/dist/ChatView.js.map +1 -0
- package/dist/Chats.d.ts +110 -0
- package/dist/Chats.d.ts.map +1 -0
- package/dist/Chats.js +98 -0
- package/dist/Chats.js.map +1 -0
- package/dist/Compaction.d.ts +105 -0
- package/dist/Compaction.d.ts.map +1 -0
- package/dist/Compaction.js +126 -0
- package/dist/Compaction.js.map +1 -0
- package/dist/CurrentChat.d.ts +40 -0
- package/dist/CurrentChat.d.ts.map +1 -0
- package/dist/CurrentChat.js +33 -0
- package/dist/CurrentChat.js.map +1 -0
- package/dist/InputId.d.ts +38 -0
- package/dist/InputId.d.ts.map +1 -0
- package/dist/InputId.js +43 -0
- package/dist/InputId.js.map +1 -0
- package/dist/Instructions.d.ts +56 -0
- package/dist/Instructions.d.ts.map +1 -0
- package/dist/Instructions.js +50 -0
- package/dist/Instructions.js.map +1 -0
- package/dist/Memory.d.ts +190 -0
- package/dist/Memory.d.ts.map +1 -0
- package/dist/Memory.js +216 -0
- package/dist/Memory.js.map +1 -0
- package/dist/RunId.d.ts +41 -0
- package/dist/RunId.d.ts.map +1 -0
- package/dist/RunId.js +46 -0
- package/dist/RunId.js.map +1 -0
- package/dist/Skill.d.ts +72 -0
- package/dist/Skill.d.ts.map +1 -0
- package/dist/Skill.js +58 -0
- package/dist/Skill.js.map +1 -0
- package/dist/Specialist.d.ts +267 -0
- package/dist/Specialist.d.ts.map +1 -0
- package/dist/Specialist.js +119 -0
- package/dist/Specialist.js.map +1 -0
- package/dist/ToolDiscovery.d.ts +107 -0
- package/dist/ToolDiscovery.d.ts.map +1 -0
- package/dist/ToolDiscovery.js +140 -0
- package/dist/ToolDiscovery.js.map +1 -0
- package/dist/ToolFailure.d.ts +66 -0
- package/dist/ToolFailure.d.ts.map +1 -0
- package/dist/ToolFailure.js +79 -0
- package/dist/ToolFailure.js.map +1 -0
- package/dist/_virtual/_rolldown/runtime.js +13 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +24 -0
- package/dist/internal/chat.js +177 -0
- package/dist/internal/chat.js.map +1 -0
- package/dist/internal/events.d.ts +46 -0
- package/dist/internal/events.d.ts.map +1 -0
- package/dist/internal/events.js +65 -0
- package/dist/internal/events.js.map +1 -0
- package/dist/internal/history.js +147 -0
- package/dist/internal/history.js.map +1 -0
- package/dist/internal/instructions.js +11 -0
- package/dist/internal/instructions.js.map +1 -0
- package/dist/internal/loop.d.ts +73 -0
- package/dist/internal/loop.d.ts.map +1 -0
- package/dist/internal/loop.js +131 -0
- package/dist/internal/loop.js.map +1 -0
- package/dist/internal/schema.js +14 -0
- package/dist/internal/schema.js.map +1 -0
- package/dist/internal/skills.js +28 -0
- package/dist/internal/skills.js.map +1 -0
- package/dist/internal/specialists.js +272 -0
- package/dist/internal/specialists.js.map +1 -0
- package/dist/internal/telemetry.js +85 -0
- package/dist/internal/telemetry.js.map +1 -0
- package/dist/internal/toolkit.d.ts +62 -0
- package/dist/internal/toolkit.d.ts.map +1 -0
- package/dist/internal/toolkit.js +108 -0
- package/dist/internal/toolkit.js.map +1 -0
- package/dist/internal/tools.js +113 -0
- package/dist/internal/tools.js.map +1 -0
- package/dist/internal/turns.d.ts +64 -0
- package/dist/internal/turns.d.ts.map +1 -0
- package/dist/internal/turns.js +214 -0
- package/dist/internal/turns.js.map +1 -0
- package/dist/internal/uuid.js +45 -0
- package/dist/internal/uuid.js.map +1 -0
- package/dist/internal/writer.d.ts +12 -0
- package/dist/internal/writer.d.ts.map +1 -0
- package/package.json +52 -3
- package/src/AppEvent.ts +134 -0
- package/src/Assistant.ts +589 -0
- package/src/AssistantActivity.ts +150 -0
- package/src/AssistantEngine.ts +343 -0
- package/src/AssistantEvent.ts +569 -0
- package/src/AssistantLoop.ts +209 -0
- package/src/AssistantToolCall.ts +40 -0
- package/src/Block.ts +84 -0
- package/src/BlockKit.ts +298 -0
- package/src/ChatId.ts +36 -0
- package/src/ChatLog.ts +247 -0
- package/src/ChatView.ts +587 -0
- package/src/Chats.ts +122 -0
- package/src/Compaction.ts +207 -0
- package/src/CurrentChat.ts +47 -0
- package/src/InputId.ts +43 -0
- package/src/Instructions.ts +72 -0
- package/src/Memory.ts +276 -0
- package/src/RunId.ts +46 -0
- package/src/Skill.ts +91 -0
- package/src/Specialist.ts +355 -0
- package/src/ToolDiscovery.ts +179 -0
- package/src/ToolFailure.ts +74 -0
- package/src/index.ts +120 -0
- package/src/internal/chat.ts +323 -0
- package/src/internal/events.ts +100 -0
- package/src/internal/history.ts +294 -0
- package/src/internal/instructions.ts +9 -0
- package/src/internal/loop.ts +274 -0
- package/src/internal/schema.ts +25 -0
- package/src/internal/skills.ts +37 -0
- package/src/internal/specialists.ts +410 -0
- package/src/internal/telemetry.ts +125 -0
- package/src/internal/toolkit.ts +188 -0
- package/src/internal/tools.ts +159 -0
- package/src/internal/turns.ts +410 -0
- package/src/internal/uuid.ts +50 -0
- package/src/internal/writer.ts +12 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import * as AiError from "effect/ai/AiError"
|
|
2
|
+
import type * as Tool from "effect/ai/Tool"
|
|
3
|
+
import type * as Toolkit from "effect/ai/Toolkit"
|
|
4
|
+
import type * as Context from "effect/Context"
|
|
5
|
+
import * as Effect from "effect/Effect"
|
|
6
|
+
import * as Option from "effect/Option"
|
|
7
|
+
import * as Schema from "effect/Schema"
|
|
8
|
+
import * as Stream from "effect/Stream"
|
|
9
|
+
import type * as AssistantEvent from "../AssistantEvent.ts"
|
|
10
|
+
import { AssistantToolCall } from "../AssistantToolCall.ts"
|
|
11
|
+
import * as ToolFailure from "../ToolFailure.ts"
|
|
12
|
+
import { toJson } from "./schema.ts"
|
|
13
|
+
|
|
14
|
+
/** @internal */
|
|
15
|
+
export interface CallResult {
|
|
16
|
+
readonly outcome: AssistantEvent.ToolOutcome
|
|
17
|
+
readonly result: Schema.Json
|
|
18
|
+
readonly reason?: string | undefined
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Apps give the loop native toolkits; these are what the loop makes of them, merged with its own.
|
|
22
|
+
/** @internal */
|
|
23
|
+
export interface Tools<out R = never> {
|
|
24
|
+
readonly definitions: ReadonlyArray<Tool.Any>
|
|
25
|
+
readonly run: (call: AssistantEvent.ToolCallPart) => Effect.Effect<CallResult, never, R>
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** @internal */
|
|
29
|
+
export type ToolServices<T extends Record<string, Tool.Any>> = Exclude<
|
|
30
|
+
Tool.HandlerServices<T[keyof T]>,
|
|
31
|
+
AssistantToolCall
|
|
32
|
+
>
|
|
33
|
+
|
|
34
|
+
const encodeFailure = Schema.encodeSync(ToolFailure.ToolFailure)
|
|
35
|
+
|
|
36
|
+
/** @internal */
|
|
37
|
+
export const failed = (message: string, reason: string): CallResult => ({
|
|
38
|
+
outcome: "failure",
|
|
39
|
+
result: toJson(encodeFailure(new ToolFailure.ToolFailure({ message, reason }))),
|
|
40
|
+
reason
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
const decodeReason = Schema.decodeUnknownOption(Schema.Struct({ reason: Schema.String }))
|
|
44
|
+
|
|
45
|
+
const decodeTag = Schema.decodeUnknownOption(Schema.Struct({ _tag: Schema.String }))
|
|
46
|
+
|
|
47
|
+
const failure = (result: Schema.Json): CallResult => ({
|
|
48
|
+
outcome: "failure",
|
|
49
|
+
result,
|
|
50
|
+
reason: Option.getOrUndefined(
|
|
51
|
+
Option.orElse(
|
|
52
|
+
Option.map(decodeReason(result), ({ reason }) => reason),
|
|
53
|
+
() => Option.map(decodeTag(result), ({ _tag }) => _tag)
|
|
54
|
+
)
|
|
55
|
+
)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
const hasTool = <T extends Record<string, Tool.Any>>(
|
|
59
|
+
toolkit: { readonly tools: T },
|
|
60
|
+
name: PropertyKey
|
|
61
|
+
): name is keyof T => Object.hasOwn(toolkit.tools, name)
|
|
62
|
+
|
|
63
|
+
// Approval would park a call until the user decides, which this library doesn't do: the model calls a tool with
|
|
64
|
+
// final values, and a user's own commitment goes through the app's API from a block.
|
|
65
|
+
/** @internal */
|
|
66
|
+
export const needingApproval = (definitions: ReadonlyArray<Tool.Any>): ReadonlyArray<string> =>
|
|
67
|
+
definitions.flatMap((tool) => (tool.needsApproval === undefined || tool.needsApproval === false ? [] : [tool.name]))
|
|
68
|
+
|
|
69
|
+
/** @internal */
|
|
70
|
+
export const needsApprovalMessage = (names: ReadonlyArray<string>): string =>
|
|
71
|
+
`${names.join(", ")} ${names.length === 1 ? "sets" : "set"} needsApproval, and effect-assistant doesn't run tools that wait for approval. Remove needsApproval: let the model call the tool with final values, or show a block whose button calls your app's API, which does the work and submits an app event that wakes the chat.`
|
|
72
|
+
|
|
73
|
+
// The toolkit decodes the params with the tool's schema before its handler sees them, and a call whose params don't
|
|
74
|
+
// fit fails with a validation error the model reads: the model's JSON is what `handle` takes.
|
|
75
|
+
const isParams = <T>(params: unknown): params is Tool.ParametersEncoded<T> => params !== undefined
|
|
76
|
+
|
|
77
|
+
// Every failure is the call's result, for the model to read. With `failureMode: "return"` the toolkit encodes the
|
|
78
|
+
// tool's own failure with its schema first.
|
|
79
|
+
/** @internal */
|
|
80
|
+
export const runNative = Effect.fnUntraced(function* <T extends Record<string, Tool.Any>>(
|
|
81
|
+
toolkit: Toolkit.WithHandler<T>,
|
|
82
|
+
call: AssistantEvent.ToolCallPart
|
|
83
|
+
): Effect.fn.Return<CallResult, never, ToolServices<T>> {
|
|
84
|
+
const name: PropertyKey = call.name
|
|
85
|
+
|
|
86
|
+
if (!hasTool(toolkit, name)) return failed(`No tool named ${call.name}.`, "ToolNotFoundError")
|
|
87
|
+
|
|
88
|
+
const params: unknown = call.params
|
|
89
|
+
|
|
90
|
+
if (!isParams<T[keyof T]>(params)) {
|
|
91
|
+
return failed(`Invalid parameters for ${call.name}.`, "ToolParameterValidationError")
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return yield* Effect.gen(function* () {
|
|
95
|
+
const results = yield* toolkit.handle<keyof T>(name, params, call.id)
|
|
96
|
+
const last = yield* Stream.runLast(Stream.filter(results, (result) => !result.preliminary))
|
|
97
|
+
|
|
98
|
+
return Option.match(last, {
|
|
99
|
+
onNone: () => failed(`${call.name} returned no result.`, "NoResult"),
|
|
100
|
+
onSome: (result): CallResult =>
|
|
101
|
+
result.isFailure
|
|
102
|
+
? failure(toJson(result.encodedResult))
|
|
103
|
+
: { outcome: "success", result: toJson(result.encodedResult) }
|
|
104
|
+
})
|
|
105
|
+
}).pipe(
|
|
106
|
+
Effect.catch((error) =>
|
|
107
|
+
Effect.succeed(
|
|
108
|
+
AiError.isAiError(error) ? failed(error.reason.message, error.reason._tag) : failure(toJson(error))
|
|
109
|
+
)
|
|
110
|
+
),
|
|
111
|
+
Effect.provideService(AssistantToolCall, { callId: call.id, tool: call.name })
|
|
112
|
+
)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
/** @internal */
|
|
116
|
+
export const fromWithHandler = <T extends Record<string, Tool.Any>>(
|
|
117
|
+
toolkit: Toolkit.WithHandler<T>
|
|
118
|
+
): Tools<ToolServices<T>> => ({
|
|
119
|
+
definitions: Object.values(toolkit.tools),
|
|
120
|
+
run: (call) => runNative(toolkit, call)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
/** @internal */
|
|
124
|
+
export const fromToolkit = <T extends Record<string, Tool.Any>>(
|
|
125
|
+
toolkit: Toolkit.Toolkit<T>
|
|
126
|
+
): Tools<ToolServices<T> | Tool.HandlersFor<T>> => ({
|
|
127
|
+
definitions: Object.values(toolkit.tools),
|
|
128
|
+
run: Effect.fnUntraced(function* (call) {
|
|
129
|
+
return yield* runNative(yield* toolkit, call)
|
|
130
|
+
})
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
// The handlers use services of the loop they run in, so they're made for each call, with its context.
|
|
134
|
+
/** @internal */
|
|
135
|
+
export const withContext = <T extends Record<string, Tool.Any>, R>(
|
|
136
|
+
toolkit: Toolkit.Toolkit<T>,
|
|
137
|
+
handlers: (context: Context.Context<R>) => Toolkit.HandlersFrom<T>
|
|
138
|
+
): Tools<R | ToolServices<T>> => ({
|
|
139
|
+
definitions: Object.values(toolkit.tools),
|
|
140
|
+
run: Effect.fnUntraced(function* (call) {
|
|
141
|
+
const context = yield* Effect.context<R>()
|
|
142
|
+
const resolved = yield* Effect.provideContext(toolkit, yield* toolkit.toHandlers(handlers(context)))
|
|
143
|
+
|
|
144
|
+
return yield* runNative(resolved, call)
|
|
145
|
+
})
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
/** @internal */
|
|
149
|
+
export const fromToolkitOrEmpty = <T extends Record<string, Tool.Any>>(
|
|
150
|
+
toolkit: Toolkit.Toolkit<T> | undefined
|
|
151
|
+
): Tools<ToolServices<T> | Tool.HandlersFor<T>> => (toolkit === undefined ? empty : fromToolkit(toolkit))
|
|
152
|
+
|
|
153
|
+
/** @internal */
|
|
154
|
+
export const empty: Tools = {
|
|
155
|
+
definitions: [],
|
|
156
|
+
run: (call) => Effect.succeed(failed(`No tool named ${call.name}.`, "ToolNotFoundError"))
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** @internal */
|
|
160
|
+
export const merge = <A, B>(self: Tools<A>, that: Tools<B>): Tools<A | B> => {
|
|
161
|
+
const inSelf = (call: AssistantEvent.ToolCallPart) => self.definitions.some((tool) => tool.name === call.name)
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
definitions: [...self.definitions, ...that.definitions],
|
|
165
|
+
run: (call) => (inSelf(call) ? self.run(call) : that.run(call))
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// A turn's own services, for a loop the app writes.
|
|
170
|
+
/** @internal */
|
|
171
|
+
export const provide = <R>(tools: Tools<R>, context: Context.Context<R>): Tools => ({
|
|
172
|
+
definitions: tools.definitions,
|
|
173
|
+
run: (call) => Effect.provideContext(tools.run(call), context)
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
/** @internal */
|
|
177
|
+
export const duplicateNames = (tools: Tools<unknown>): ReadonlyArray<string> => {
|
|
178
|
+
const names = tools.definitions.map((tool) => tool.name)
|
|
179
|
+
|
|
180
|
+
return names.filter((name, i) => names.indexOf(name) !== i)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// The same tools are offered, so the prompt doesn't change.
|
|
184
|
+
/** @internal */
|
|
185
|
+
export const runOnly = <R>(tools: Tools<R>, names: ReadonlyArray<string>, message: string): Tools<R> => ({
|
|
186
|
+
definitions: tools.definitions,
|
|
187
|
+
run: (call) => (names.includes(call.name) ? tools.run(call) : Effect.succeed(failed(message, "NotNow")))
|
|
188
|
+
})
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import * as Tool from "effect/ai/Tool"
|
|
2
|
+
import * as Toolkit from "effect/ai/Toolkit"
|
|
3
|
+
import * as Effect from "effect/Effect"
|
|
4
|
+
import * as Option from "effect/Option"
|
|
5
|
+
import * as Predicate from "effect/Predicate"
|
|
6
|
+
import * as Schema from "effect/Schema"
|
|
7
|
+
import * as AssistantEvent from "../AssistantEvent.ts"
|
|
8
|
+
import { AssistantToolCall } from "../AssistantToolCall.ts"
|
|
9
|
+
import type * as BlockKit from "../BlockKit.ts"
|
|
10
|
+
import { CurrentChat } from "../CurrentChat.ts"
|
|
11
|
+
import * as ToolFailure from "../ToolFailure.ts"
|
|
12
|
+
import * as Toolkits from "./toolkit.ts"
|
|
13
|
+
import * as Uuid from "./uuid.ts"
|
|
14
|
+
|
|
15
|
+
/** @internal */
|
|
16
|
+
export const waitToolName = "wait"
|
|
17
|
+
|
|
18
|
+
/** @internal */
|
|
19
|
+
export const sendUserMessageName = "send_user_message"
|
|
20
|
+
|
|
21
|
+
const Wait = Tool.make(waitToolName, {
|
|
22
|
+
description:
|
|
23
|
+
"End your turn and wait for the next message or update, when you have nothing more to do right now and no message to send (otherwise set wait on send_user_message).",
|
|
24
|
+
success: Schema.String
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
const description =
|
|
28
|
+
"Send the user a chat message. It's the only way they see anything you write: text outside it is private to you."
|
|
29
|
+
|
|
30
|
+
const WaitFlag = Schema.Boolean.annotate({
|
|
31
|
+
description:
|
|
32
|
+
"true when this is your last message for now: your turn ends with it, as with the wait tool. false when you keep working after it."
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const endsWithMessage = Schema.decodeUnknownOption(Schema.Struct({ wait: Schema.Literal(true) }))
|
|
36
|
+
|
|
37
|
+
/** @internal */
|
|
38
|
+
export const isTurnEnding = (call: AssistantEvent.ToolCallPart): boolean =>
|
|
39
|
+
call.name === waitToolName || (call.name === sendUserMessageName && Option.isSome(endsWithMessage(call.params)))
|
|
40
|
+
|
|
41
|
+
// From the call: a call cut off by a crash runs again with the same id, and finds its message already sent.
|
|
42
|
+
const messageId = AssistantToolCall.useSync((call) => Uuid.fromKey(`message:${call.callId}`))
|
|
43
|
+
|
|
44
|
+
const append = Effect.fnUntraced(function* (message: AssistantEvent.AssistantMessage) {
|
|
45
|
+
const chat = yield* CurrentChat
|
|
46
|
+
const entries = yield* chat.entries
|
|
47
|
+
|
|
48
|
+
const sent = entries.some(
|
|
49
|
+
({ event }) =>
|
|
50
|
+
AssistantEvent.isLibraryEvent(event) && Predicate.isTagged(event, "AssistantMessage") && event.id === message.id
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
if (!sent) yield* chat.append(message)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const PlainMessage = Tool.make(sendUserMessageName, {
|
|
57
|
+
description,
|
|
58
|
+
parameters: Schema.Struct({ text: Schema.String, wait: WaitFlag }),
|
|
59
|
+
success: Schema.String,
|
|
60
|
+
failure: ToolFailure.ToolFailure,
|
|
61
|
+
dependencies: [CurrentChat, AssistantToolCall]
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
const PlainTools = Toolkit.make(PlainMessage, Wait)
|
|
65
|
+
|
|
66
|
+
const plainHandlers = PlainTools.toHandlers({
|
|
67
|
+
wait: () => Effect.succeed("waiting"),
|
|
68
|
+
send_user_message: Effect.fnUntraced(function* ({ text }) {
|
|
69
|
+
yield* append(AssistantEvent.AssistantMessage.make({ id: yield* messageId, text }))
|
|
70
|
+
|
|
71
|
+
return "sent"
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
const plainTools = Effect.gen(function* () {
|
|
76
|
+
return yield* Effect.provideContext(PlainTools, yield* plainHandlers)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
const blockTools = Effect.fnUntraced(function* (kit: BlockKit.BlockKit) {
|
|
80
|
+
const catalog = kit.blocks.map((block) => `- ${block.kind}: ${block.description}`).join("\n")
|
|
81
|
+
const kinds = kit.blocks.map((block) => block.kind)
|
|
82
|
+
|
|
83
|
+
const BlockMessage = Tool.make(sendUserMessageName, {
|
|
84
|
+
description: `${description}
|
|
85
|
+
You can add a block to show UI with the message, e.g. a few options to pick from or a yes/no, when that makes answering easier than typing. Plain text is fine too. Using it sends you an <answer>; the user may also just type their answer instead, and then you pass that message's id in closes.
|
|
86
|
+
Blocks:
|
|
87
|
+
${catalog}`,
|
|
88
|
+
// Props are checked against their kind's schema by the handler; the model sees every kind's in the parameters.
|
|
89
|
+
parameters: Schema.Struct({
|
|
90
|
+
text: Schema.String,
|
|
91
|
+
wait: WaitFlag,
|
|
92
|
+
block: Schema.optional(
|
|
93
|
+
Schema.Struct({
|
|
94
|
+
kind: Schema.Literals(kinds),
|
|
95
|
+
props: Schema.Union(kit.blocks.map((block) => block.props))
|
|
96
|
+
})
|
|
97
|
+
),
|
|
98
|
+
closes: Schema.optional(Schema.Array(Schema.String)).annotate({
|
|
99
|
+
description:
|
|
100
|
+
"Ids of your earlier messages whose block the user just answered by typing instead of using it. Those blocks close. Leave it out otherwise: a block stays open when the user talks about something else."
|
|
101
|
+
})
|
|
102
|
+
}),
|
|
103
|
+
success: Schema.String,
|
|
104
|
+
failure: ToolFailure.ToolFailure,
|
|
105
|
+
dependencies: [CurrentChat, AssistantToolCall]
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
const BlockTools = Toolkit.make(BlockMessage, Wait)
|
|
109
|
+
|
|
110
|
+
const handlers = yield* BlockTools.toHandlers({
|
|
111
|
+
wait: () => Effect.succeed("waiting"),
|
|
112
|
+
send_user_message: Effect.fnUntraced(function* ({ text, block, closes }) {
|
|
113
|
+
if (text.trim() === "") {
|
|
114
|
+
return yield* ToolFailure.fail(
|
|
115
|
+
"The message has no text: the user sees only what you send here, and a block alone is an empty bubble. Send it again with the words they should read (a short lead-in for the block).",
|
|
116
|
+
{ reason: "EmptyMessage" }
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const id = yield* messageId
|
|
121
|
+
const closed = closes === undefined || closes.length === 0 ? {} : { closes }
|
|
122
|
+
|
|
123
|
+
if (block === undefined) {
|
|
124
|
+
yield* append(AssistantEvent.AssistantMessage.make({ id, text, ...closed }))
|
|
125
|
+
|
|
126
|
+
return "sent"
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const props = kit.byKind.get(block.kind)?.props
|
|
130
|
+
|
|
131
|
+
if (props === undefined || Option.isNone(Schema.decodeUnknownOption(props)(block.props))) {
|
|
132
|
+
return yield* ToolFailure.fail(`Those props don't fit a ${block.kind} block.`)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
yield* append(
|
|
136
|
+
AssistantEvent.AssistantMessage.make({
|
|
137
|
+
id,
|
|
138
|
+
text,
|
|
139
|
+
block: AssistantEvent.MessageBlock.make(block),
|
|
140
|
+
...closed
|
|
141
|
+
})
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
return `sent as message ${id}`
|
|
145
|
+
})
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
return yield* Effect.provideContext(BlockTools, handlers)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
/** @internal */
|
|
152
|
+
export const makeMessaging: (kit: BlockKit.BlockKit | undefined) => Effect.Effect<Toolkits.Tools<CurrentChat>> =
|
|
153
|
+
Effect.fnUntraced(function* (kit) {
|
|
154
|
+
if (kit === undefined || kit.blocks.length === 0) {
|
|
155
|
+
return Toolkits.fromWithHandler(yield* plainTools)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return Toolkits.fromWithHandler(yield* blockTools(kit))
|
|
159
|
+
})
|