effect-assistant 0.0.0-stage → 0.1.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 +614 -0
- package/dist/AssistantEvent.d.ts.map +1 -0
- package/dist/AssistantEvent.js +523 -0
- package/dist/AssistantEvent.js.map +1 -0
- package/dist/AssistantLoop.d.ts +171 -0
- package/dist/AssistantLoop.d.ts.map +1 -0
- package/dist/AssistantLoop.js +108 -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 +184 -0
- package/dist/BlockKit.d.ts.map +1 -0
- package/dist/BlockKit.js +151 -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 +459 -0
- package/dist/ChatView.d.ts.map +1 -0
- package/dist/ChatView.js +496 -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 +133 -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/Decision.d.ts +145 -0
- package/dist/Decision.d.ts.map +1 -0
- package/dist/Decision.js +178 -0
- package/dist/Decision.js.map +1 -0
- package/dist/DecisionId.d.ts +37 -0
- package/dist/DecisionId.d.ts.map +1 -0
- package/dist/DecisionId.js +44 -0
- package/dist/DecisionId.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 +108 -0
- package/dist/ToolDiscovery.d.ts.map +1 -0
- package/dist/ToolDiscovery.js +139 -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 +26 -0
- package/dist/index.js +26 -0
- package/dist/internal/chat.js +202 -0
- package/dist/internal/chat.js.map +1 -0
- package/dist/internal/decisions.js +66 -0
- package/dist/internal/decisions.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 +185 -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 +166 -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 +273 -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 +76 -0
- package/dist/internal/toolkit.d.ts.map +1 -0
- package/dist/internal/toolkit.js +129 -0
- package/dist/internal/toolkit.js.map +1 -0
- package/dist/internal/tools.js +132 -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 +221 -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 +51 -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 +635 -0
- package/src/AssistantLoop.ts +207 -0
- package/src/AssistantToolCall.ts +40 -0
- package/src/Block.ts +84 -0
- package/src/BlockKit.ts +311 -0
- package/src/ChatId.ts +36 -0
- package/src/ChatLog.ts +247 -0
- package/src/ChatView.ts +658 -0
- package/src/Chats.ts +122 -0
- package/src/Compaction.ts +224 -0
- package/src/CurrentChat.ts +47 -0
- package/src/Decision.ts +241 -0
- package/src/DecisionId.ts +46 -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 +187 -0
- package/src/ToolFailure.ts +74 -0
- package/src/index.ts +130 -0
- package/src/internal/chat.ts +372 -0
- package/src/internal/decisions.ts +131 -0
- package/src/internal/events.ts +100 -0
- package/src/internal/history.ts +366 -0
- package/src/internal/instructions.ts +9 -0
- package/src/internal/loop.ts +344 -0
- package/src/internal/schema.ts +25 -0
- package/src/internal/skills.ts +37 -0
- package/src/internal/specialists.ts +414 -0
- package/src/internal/telemetry.ts +125 -0
- package/src/internal/toolkit.ts +231 -0
- package/src/internal/tools.ts +210 -0
- package/src/internal/turns.ts +429 -0
- package/src/internal/uuid.ts +50 -0
- package/src/internal/writer.ts +12 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import * as AiError from "effect/ai/AiError"
|
|
2
|
+
import type * as Prompt from "effect/ai/Prompt"
|
|
3
|
+
import type * as Tool from "effect/ai/Tool"
|
|
4
|
+
import type * as Toolkit from "effect/ai/Toolkit"
|
|
5
|
+
import type * as Context from "effect/Context"
|
|
6
|
+
import * as Effect from "effect/Effect"
|
|
7
|
+
import * as Option from "effect/Option"
|
|
8
|
+
import * as Predicate from "effect/Predicate"
|
|
9
|
+
import * as Schema from "effect/Schema"
|
|
10
|
+
import * as Stream from "effect/Stream"
|
|
11
|
+
import type * as AssistantEvent from "../AssistantEvent.ts"
|
|
12
|
+
import { AssistantToolCall } from "../AssistantToolCall.ts"
|
|
13
|
+
import * as ToolFailure from "../ToolFailure.ts"
|
|
14
|
+
import { toJson } from "./schema.ts"
|
|
15
|
+
|
|
16
|
+
/** @internal */
|
|
17
|
+
export interface CallResult {
|
|
18
|
+
readonly outcome: AssistantEvent.ToolOutcome
|
|
19
|
+
readonly result: Schema.Json
|
|
20
|
+
readonly reason?: string | undefined
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Apps give the loop native toolkits; these are what the loop makes of them, merged with its own.
|
|
24
|
+
/** @internal */
|
|
25
|
+
export interface Tools<out R = never> {
|
|
26
|
+
readonly definitions: ReadonlyArray<Tool.Any>
|
|
27
|
+
readonly run: (call: AssistantEvent.ToolCallPart) => Effect.Effect<CallResult, never, R>
|
|
28
|
+
// Unset: no call waits for the user's decision.
|
|
29
|
+
readonly needsDecision?:
|
|
30
|
+
| ((call: AssistantEvent.ToolCallPart, history: ReadonlyArray<Prompt.Message>) => Effect.Effect<boolean, never, R>)
|
|
31
|
+
| undefined
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** @internal */
|
|
35
|
+
export type ToolServices<T extends Record<string, Tool.Any>> =
|
|
36
|
+
| Exclude<Tool.HandlerServices<T[keyof T]>, AssistantToolCall>
|
|
37
|
+
// Already in the handlers' services; stated for the loop, which decodes params to decide on a call.
|
|
38
|
+
| T[keyof T]["parametersSchema"]["DecodingServices"]
|
|
39
|
+
|
|
40
|
+
const encodeFailure = Schema.encodeSync(ToolFailure.ToolFailure)
|
|
41
|
+
|
|
42
|
+
/** @internal */
|
|
43
|
+
export const failed = (message: string, reason: string): CallResult => ({
|
|
44
|
+
outcome: "failure",
|
|
45
|
+
result: toJson(encodeFailure(new ToolFailure.ToolFailure({ message, reason }))),
|
|
46
|
+
reason
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
const decodeReason = Schema.decodeUnknownOption(Schema.Struct({ reason: Schema.String }))
|
|
50
|
+
|
|
51
|
+
const decodeTag = Schema.decodeUnknownOption(Schema.Struct({ _tag: Schema.String }))
|
|
52
|
+
|
|
53
|
+
const failure = (result: Schema.Json): CallResult => ({
|
|
54
|
+
outcome: "failure",
|
|
55
|
+
result,
|
|
56
|
+
reason: Option.getOrUndefined(
|
|
57
|
+
Option.orElse(
|
|
58
|
+
Option.map(decodeReason(result), ({ reason }) => reason),
|
|
59
|
+
() => Option.map(decodeTag(result), ({ _tag }) => _tag)
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
const hasTool = <T extends Record<string, Tool.Any>>(
|
|
65
|
+
toolkit: { readonly tools: T },
|
|
66
|
+
name: PropertyKey
|
|
67
|
+
): name is keyof T => Object.hasOwn(toolkit.tools, name)
|
|
68
|
+
|
|
69
|
+
// With noUncheckedIndexedAccess, an index doesn't narrow on `hasTool`.
|
|
70
|
+
const toolNamed = (toolkit: { readonly tools: Record<string, Tool.Any> }, name: string): Tool.Any | undefined =>
|
|
71
|
+
Object.hasOwn(toolkit.tools, name) ? toolkit.tools[name] : undefined
|
|
72
|
+
|
|
73
|
+
const decodeParams = (tool: Tool.Any, params: Schema.Json) => Schema.decodeUnknownEffect(tool.parametersSchema)(params)
|
|
74
|
+
|
|
75
|
+
// Params that don't fit the tool don't park: the call runs, and fails with why.
|
|
76
|
+
/** @internal */
|
|
77
|
+
export const needsDecision = Effect.fnUntraced(function* <T extends Record<string, Tool.Any>>(
|
|
78
|
+
toolkit: { readonly tools: T },
|
|
79
|
+
call: AssistantEvent.ToolCallPart,
|
|
80
|
+
history: ReadonlyArray<Prompt.Message>
|
|
81
|
+
): Effect.fn.Return<boolean, never, ToolServices<T>> {
|
|
82
|
+
const tool = toolNamed(toolkit, call.name)
|
|
83
|
+
|
|
84
|
+
if (tool === undefined) return false
|
|
85
|
+
|
|
86
|
+
const needs = tool.needsApproval
|
|
87
|
+
|
|
88
|
+
if (needs === undefined || Predicate.isBoolean(needs)) return needs === true
|
|
89
|
+
|
|
90
|
+
return yield* Effect.gen(function* () {
|
|
91
|
+
const decided = needs(yield* decodeParams(tool, call.params), {
|
|
92
|
+
toolCallId: call.id,
|
|
93
|
+
messages: history
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
return Effect.isEffect(decided) ? yield* decided : decided
|
|
97
|
+
}).pipe(Effect.orElseSucceed(() => false))
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
// The toolkit decodes the params with the tool's schema before its handler sees them, and a call whose params don't
|
|
101
|
+
// fit fails with a validation error the model reads: the model's JSON is what `handle` takes.
|
|
102
|
+
const isParams = <T>(params: unknown): params is Tool.ParametersEncoded<T> => params !== undefined
|
|
103
|
+
|
|
104
|
+
// Every failure is the call's result, for the model to read. With `failureMode: "return"` the toolkit encodes the
|
|
105
|
+
// tool's own failure with its schema first.
|
|
106
|
+
/** @internal */
|
|
107
|
+
export const runNative = Effect.fnUntraced(function* <T extends Record<string, Tool.Any>>(
|
|
108
|
+
toolkit: Toolkit.WithHandler<T>,
|
|
109
|
+
call: AssistantEvent.ToolCallPart
|
|
110
|
+
): Effect.fn.Return<CallResult, never, ToolServices<T>> {
|
|
111
|
+
const name: PropertyKey = call.name
|
|
112
|
+
|
|
113
|
+
if (!hasTool(toolkit, name)) return failed(`No tool named ${call.name}.`, "ToolNotFoundError")
|
|
114
|
+
|
|
115
|
+
const params: unknown = call.params
|
|
116
|
+
|
|
117
|
+
if (!isParams<T[keyof T]>(params)) {
|
|
118
|
+
return failed(`Invalid parameters for ${call.name}.`, "ToolParameterValidationError")
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return yield* Effect.gen(function* () {
|
|
122
|
+
const results = yield* toolkit.handle<keyof T>(name, params, call.id)
|
|
123
|
+
const last = yield* Stream.runLast(Stream.filter(results, (result) => !result.preliminary))
|
|
124
|
+
|
|
125
|
+
return Option.match(last, {
|
|
126
|
+
onNone: () => failed(`${call.name} returned no result.`, "NoResult"),
|
|
127
|
+
onSome: (result): CallResult =>
|
|
128
|
+
result.isFailure
|
|
129
|
+
? failure(toJson(result.encodedResult))
|
|
130
|
+
: { outcome: "success", result: toJson(result.encodedResult) }
|
|
131
|
+
})
|
|
132
|
+
}).pipe(
|
|
133
|
+
Effect.catch((error) =>
|
|
134
|
+
Effect.succeed(
|
|
135
|
+
AiError.isAiError(error) ? failed(error.reason.message, error.reason._tag) : failure(toJson(error))
|
|
136
|
+
)
|
|
137
|
+
),
|
|
138
|
+
Effect.provideService(AssistantToolCall, { callId: call.id, tool: call.name })
|
|
139
|
+
)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
/** @internal */
|
|
143
|
+
export const fromWithHandler = <T extends Record<string, Tool.Any>>(
|
|
144
|
+
toolkit: Toolkit.WithHandler<T>
|
|
145
|
+
): Tools<ToolServices<T>> => ({
|
|
146
|
+
definitions: Object.values(toolkit.tools),
|
|
147
|
+
run: (call) => runNative(toolkit, call),
|
|
148
|
+
needsDecision: (call, history) => needsDecision(toolkit, call, history)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
/** @internal */
|
|
152
|
+
export const fromToolkit = <T extends Record<string, Tool.Any>>(
|
|
153
|
+
toolkit: Toolkit.Toolkit<T>
|
|
154
|
+
): Tools<ToolServices<T> | Tool.HandlersFor<T>> => ({
|
|
155
|
+
definitions: Object.values(toolkit.tools),
|
|
156
|
+
run: Effect.fnUntraced(function* (call) {
|
|
157
|
+
return yield* runNative(yield* toolkit, call)
|
|
158
|
+
}),
|
|
159
|
+
needsDecision: (call, history) => needsDecision(toolkit, call, history)
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
// The handlers use services of the loop they run in, so they're made for each call, with its context.
|
|
163
|
+
/** @internal */
|
|
164
|
+
export const withContext = <T extends Record<string, Tool.Any>, R>(
|
|
165
|
+
toolkit: Toolkit.Toolkit<T>,
|
|
166
|
+
handlers: (context: Context.Context<R>) => Toolkit.HandlersFrom<T>
|
|
167
|
+
): Tools<R | ToolServices<T>> => ({
|
|
168
|
+
definitions: Object.values(toolkit.tools),
|
|
169
|
+
run: Effect.fnUntraced(function* (call) {
|
|
170
|
+
const context = yield* Effect.context<R>()
|
|
171
|
+
const resolved = yield* Effect.provideContext(toolkit, yield* toolkit.toHandlers(handlers(context)))
|
|
172
|
+
|
|
173
|
+
return yield* runNative(resolved, call)
|
|
174
|
+
}),
|
|
175
|
+
needsDecision: (call, history) => needsDecision(toolkit, call, history)
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
/** @internal */
|
|
179
|
+
export const fromToolkitOrEmpty = <T extends Record<string, Tool.Any>>(
|
|
180
|
+
toolkit: Toolkit.Toolkit<T> | undefined
|
|
181
|
+
): Tools<ToolServices<T> | Tool.HandlersFor<T>> => (toolkit === undefined ? empty : fromToolkit(toolkit))
|
|
182
|
+
|
|
183
|
+
/** @internal */
|
|
184
|
+
export const empty: Tools = {
|
|
185
|
+
definitions: [],
|
|
186
|
+
run: (call) => Effect.succeed(failed(`No tool named ${call.name}.`, "ToolNotFoundError"))
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** @internal */
|
|
190
|
+
export const merge = <A, B>(self: Tools<A>, that: Tools<B>): Tools<A | B> => {
|
|
191
|
+
const inSelf = (call: AssistantEvent.ToolCallPart) => self.definitions.some((tool) => tool.name === call.name)
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
definitions: [...self.definitions, ...that.definitions],
|
|
195
|
+
run: (call) => (inSelf(call) ? self.run(call) : that.run(call)),
|
|
196
|
+
needsDecision: (call, history) =>
|
|
197
|
+
inSelf(call) ? needsDecisionOf(self, call, history) : needsDecisionOf(that, call, history)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** @internal */
|
|
202
|
+
export const needsDecisionOf = <R>(
|
|
203
|
+
tools: Tools<R>,
|
|
204
|
+
call: AssistantEvent.ToolCallPart,
|
|
205
|
+
history: ReadonlyArray<Prompt.Message>
|
|
206
|
+
): Effect.Effect<boolean, never, R> =>
|
|
207
|
+
tools.needsDecision === undefined ? Effect.succeed(false) : tools.needsDecision(call, history)
|
|
208
|
+
|
|
209
|
+
// A turn's own services, for a loop the app writes.
|
|
210
|
+
/** @internal */
|
|
211
|
+
export const provide = <R>(tools: Tools<R>, context: Context.Context<R>): Tools => ({
|
|
212
|
+
definitions: tools.definitions,
|
|
213
|
+
run: (call) => Effect.provideContext(tools.run(call), context),
|
|
214
|
+
needsDecision: (call, history) => Effect.provideContext(needsDecisionOf(tools, call, history), context)
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
/** @internal */
|
|
218
|
+
export const duplicateNames = (tools: Tools<unknown>): ReadonlyArray<string> => {
|
|
219
|
+
const names = tools.definitions.map((tool) => tool.name)
|
|
220
|
+
|
|
221
|
+
return names.filter((name, i) => names.indexOf(name) !== i)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// The same tools are offered, so the prompt doesn't change.
|
|
225
|
+
/** @internal */
|
|
226
|
+
export const runOnly = <R>(tools: Tools<R>, names: ReadonlyArray<string>, message: string): Tools<R> => ({
|
|
227
|
+
definitions: tools.definitions,
|
|
228
|
+
run: (call) => (names.includes(call.name) ? tools.run(call) : Effect.succeed(failed(message, "NotNow"))),
|
|
229
|
+
needsDecision: (call, history) =>
|
|
230
|
+
names.includes(call.name) ? needsDecisionOf(tools, call, history) : Effect.succeed(false)
|
|
231
|
+
})
|
|
@@ -0,0 +1,210 @@
|
|
|
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 * as BlockKit from "../BlockKit.ts"
|
|
10
|
+
import { CurrentChat } from "../CurrentChat.ts"
|
|
11
|
+
import * as Decision from "../Decision.ts"
|
|
12
|
+
import * as ToolFailure from "../ToolFailure.ts"
|
|
13
|
+
import * as Toolkits from "./toolkit.ts"
|
|
14
|
+
import * as Uuid from "./uuid.ts"
|
|
15
|
+
|
|
16
|
+
/** @internal */
|
|
17
|
+
export const waitToolName = "wait"
|
|
18
|
+
|
|
19
|
+
/** @internal */
|
|
20
|
+
export const sendUserMessageName = "send_user_message"
|
|
21
|
+
|
|
22
|
+
const Wait = Tool.make(waitToolName, {
|
|
23
|
+
description:
|
|
24
|
+
"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).",
|
|
25
|
+
success: Schema.String
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
const description =
|
|
29
|
+
"Send the user a chat message. It's the only way they see anything you write: text outside it is private to you."
|
|
30
|
+
|
|
31
|
+
const WaitFlag = Schema.Boolean.annotate({
|
|
32
|
+
description:
|
|
33
|
+
"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."
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const endsWithMessage = Schema.decodeUnknownOption(Schema.Struct({ wait: Schema.Literal(true) }))
|
|
37
|
+
|
|
38
|
+
/** @internal */
|
|
39
|
+
export const isTurnEnding = (call: AssistantEvent.ToolCallPart): boolean =>
|
|
40
|
+
call.name === waitToolName || (call.name === sendUserMessageName && Option.isSome(endsWithMessage(call.params)))
|
|
41
|
+
|
|
42
|
+
// From the call: a call cut off by a crash runs again with the same id, and finds its message already sent.
|
|
43
|
+
const messageId = AssistantToolCall.useSync((call) => Uuid.fromKey(`message:${call.callId}`))
|
|
44
|
+
|
|
45
|
+
const append = Effect.fnUntraced(function* (message: AssistantEvent.AssistantMessage) {
|
|
46
|
+
const chat = yield* CurrentChat
|
|
47
|
+
const entries = yield* chat.entries
|
|
48
|
+
|
|
49
|
+
const sent = entries.some(
|
|
50
|
+
({ event }) =>
|
|
51
|
+
AssistantEvent.isLibraryEvent(event) && Predicate.isTagged(event, "AssistantMessage") && event.id === message.id
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
if (!sent) yield* chat.append(message)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
const PlainMessage = Tool.make(sendUserMessageName, {
|
|
58
|
+
description,
|
|
59
|
+
parameters: Schema.Struct({ text: Schema.String, wait: WaitFlag }),
|
|
60
|
+
success: Schema.String,
|
|
61
|
+
failure: ToolFailure.ToolFailure,
|
|
62
|
+
dependencies: [CurrentChat, AssistantToolCall]
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
const PlainTools = Toolkit.make(PlainMessage, Wait)
|
|
66
|
+
|
|
67
|
+
const plainHandlers = PlainTools.toHandlers({
|
|
68
|
+
wait: () => Effect.succeed("waiting"),
|
|
69
|
+
send_user_message: Effect.fnUntraced(function* ({ text }) {
|
|
70
|
+
yield* append(AssistantEvent.AssistantMessage.make({ id: yield* messageId, text }))
|
|
71
|
+
|
|
72
|
+
return "sent"
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
const plainTools = Effect.gen(function* () {
|
|
77
|
+
return yield* Effect.provideContext(PlainTools, yield* plainHandlers)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
const decodeDecisionProps = Schema.decodeUnknownOption(Decision.Props)
|
|
81
|
+
|
|
82
|
+
const checkDecisions = Effect.fnUntraced(function* (ids: ReadonlyArray<string>) {
|
|
83
|
+
const states = Decision.fromEntries(yield* CurrentChat.use((chat) => chat.entries))
|
|
84
|
+
const notWaiting = ids.filter((id) => states.get(id)?.outcome !== undefined || !states.has(id))
|
|
85
|
+
|
|
86
|
+
if (notWaiting.length > 0) {
|
|
87
|
+
return yield* ToolFailure.fail(
|
|
88
|
+
`Not waiting for the user's decision: ${notWaiting.join(", ")}. Use the ids of decisions that wait for one.`,
|
|
89
|
+
{ reason: "NotWaitingForDecision" }
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
// A decision block closes when the user decides on it, not because the model says so: the order it asks about would
|
|
95
|
+
// otherwise never be approved.
|
|
96
|
+
const checkCloses = Effect.fnUntraced(function* (ids: ReadonlyArray<string>) {
|
|
97
|
+
const entries = yield* CurrentChat.use((chat) => chat.entries)
|
|
98
|
+
const states = BlockKit.fromEntries(entries)
|
|
99
|
+
|
|
100
|
+
const waiting = ids.filter((id) =>
|
|
101
|
+
entries.some(
|
|
102
|
+
({ event }) =>
|
|
103
|
+
AssistantEvent.isLibraryEvent(event) &&
|
|
104
|
+
Predicate.isTagged(event, "AssistantMessage") &&
|
|
105
|
+
event.id === id &&
|
|
106
|
+
event.block?.kind === Decision.kind &&
|
|
107
|
+
states.get(id)?.open === true
|
|
108
|
+
)
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
if (waiting.length > 0) {
|
|
112
|
+
return yield* ToolFailure.fail(
|
|
113
|
+
`${waiting.join(", ")} asks for a decision that still waits for the user: only their answer in the block closes it. Send the message again without ${waiting.length === 1 ? "that id" : "those ids"} in closes.`,
|
|
114
|
+
{ reason: "DecisionWaiting" }
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
const blockTools = Effect.fnUntraced(function* (kit: BlockKit.BlockKit) {
|
|
120
|
+
const catalog = kit.blocks.map((block) => `- ${block.kind}: ${block.description}`).join("\n")
|
|
121
|
+
const kinds = kit.blocks.map((block) => block.kind)
|
|
122
|
+
|
|
123
|
+
const BlockMessage = Tool.make(sendUserMessageName, {
|
|
124
|
+
description: `${description}
|
|
125
|
+
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.
|
|
126
|
+
Blocks:
|
|
127
|
+
${catalog}`,
|
|
128
|
+
// Props are checked against their kind's schema by the handler; the model sees every kind's in the parameters.
|
|
129
|
+
parameters: Schema.Struct({
|
|
130
|
+
text: Schema.String,
|
|
131
|
+
wait: WaitFlag,
|
|
132
|
+
block: Schema.optional(
|
|
133
|
+
Schema.Struct({
|
|
134
|
+
kind: Schema.Literals(kinds),
|
|
135
|
+
props: Schema.Union(kit.blocks.map((block) => block.props))
|
|
136
|
+
})
|
|
137
|
+
),
|
|
138
|
+
closes: Schema.optional(Schema.Array(Schema.String)).annotate({
|
|
139
|
+
description:
|
|
140
|
+
"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. A decision block never closes this way: only the user's answer in it decides."
|
|
141
|
+
})
|
|
142
|
+
}),
|
|
143
|
+
success: Schema.String,
|
|
144
|
+
failure: ToolFailure.ToolFailure,
|
|
145
|
+
dependencies: [CurrentChat, AssistantToolCall]
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
const BlockTools = Toolkit.make(BlockMessage, Wait)
|
|
149
|
+
|
|
150
|
+
const handlers = yield* BlockTools.toHandlers({
|
|
151
|
+
wait: () => Effect.succeed("waiting"),
|
|
152
|
+
send_user_message: Effect.fnUntraced(function* ({ text, block, closes }) {
|
|
153
|
+
if (text.trim() === "") {
|
|
154
|
+
return yield* ToolFailure.fail(
|
|
155
|
+
"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).",
|
|
156
|
+
{ reason: "EmptyMessage" }
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const id = yield* messageId
|
|
161
|
+
const closed = closes === undefined || closes.length === 0 ? {} : { closes }
|
|
162
|
+
|
|
163
|
+
if (closes !== undefined && closes.length > 0) yield* checkCloses(closes)
|
|
164
|
+
|
|
165
|
+
if (block === undefined) {
|
|
166
|
+
yield* append(AssistantEvent.AssistantMessage.make({ id, text, ...closed }))
|
|
167
|
+
|
|
168
|
+
return "sent"
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const props = kit.byKind.get(block.kind)?.props
|
|
172
|
+
|
|
173
|
+
if (props === undefined || Option.isNone(Schema.decodeUnknownOption(props)(block.props))) {
|
|
174
|
+
return yield* ToolFailure.fail(`Those props don't fit a ${block.kind} block.`)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (block.kind === Decision.kind) {
|
|
178
|
+
yield* checkDecisions(
|
|
179
|
+
Option.match(decodeDecisionProps(block.props), {
|
|
180
|
+
onNone: () => [],
|
|
181
|
+
onSome: (decoded) => decoded.decisionIds
|
|
182
|
+
})
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
yield* append(
|
|
187
|
+
AssistantEvent.AssistantMessage.make({
|
|
188
|
+
id,
|
|
189
|
+
text,
|
|
190
|
+
block: AssistantEvent.MessageBlock.make(block),
|
|
191
|
+
...closed
|
|
192
|
+
})
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
return `sent as message ${id}`
|
|
196
|
+
})
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
return yield* Effect.provideContext(BlockTools, handlers)
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
/** @internal */
|
|
203
|
+
export const makeMessaging: (kit: BlockKit.BlockKit | undefined) => Effect.Effect<Toolkits.Tools<CurrentChat>> =
|
|
204
|
+
Effect.fnUntraced(function* (kit) {
|
|
205
|
+
if (kit === undefined || kit.blocks.length === 0) {
|
|
206
|
+
return Toolkits.fromWithHandler(yield* plainTools)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return Toolkits.fromWithHandler(yield* blockTools(kit))
|
|
210
|
+
})
|