effect-assistant 0.1.0 → 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/README.md +1 -1
- package/dist/AssistantEvent.d.ts +3 -66
- package/dist/AssistantEvent.d.ts.map +1 -1
- package/dist/AssistantEvent.js +1 -51
- package/dist/AssistantEvent.js.map +1 -1
- package/dist/AssistantLoop.d.ts +7 -5
- package/dist/AssistantLoop.d.ts.map +1 -1
- package/dist/AssistantLoop.js +6 -3
- package/dist/AssistantLoop.js.map +1 -1
- package/dist/Block.js.map +1 -1
- package/dist/BlockKit.d.ts +11 -15
- package/dist/BlockKit.d.ts.map +1 -1
- package/dist/BlockKit.js +3 -6
- package/dist/BlockKit.js.map +1 -1
- package/dist/ChatView.d.ts +7 -58
- package/dist/ChatView.d.ts.map +1 -1
- package/dist/ChatView.js +15 -68
- package/dist/ChatView.js.map +1 -1
- package/dist/Compaction.d.ts +2 -2
- package/dist/Compaction.d.ts.map +1 -1
- package/dist/Compaction.js +6 -13
- package/dist/Compaction.js.map +1 -1
- package/dist/ToolDiscovery.d.ts +4 -5
- package/dist/ToolDiscovery.d.ts.map +1 -1
- package/dist/ToolDiscovery.js +5 -4
- package/dist/ToolDiscovery.js.map +1 -1
- package/dist/ToolFailure.d.ts +1 -1
- package/dist/ToolFailure.js +1 -1
- package/dist/ToolFailure.js.map +1 -1
- package/dist/index.d.ts +1 -3
- package/dist/index.js +3 -5
- package/dist/internal/chat.js +6 -31
- package/dist/internal/chat.js.map +1 -1
- package/dist/internal/history.js +2 -40
- package/dist/internal/history.js.map +1 -1
- package/dist/internal/loop.d.ts +2 -2
- package/dist/internal/loop.d.ts.map +1 -1
- package/dist/internal/loop.js +20 -55
- package/dist/internal/loop.js.map +1 -1
- package/dist/internal/specialists.js +2 -3
- package/dist/internal/specialists.js.map +1 -1
- package/dist/internal/toolkit.d.ts +6 -20
- package/dist/internal/toolkit.d.ts.map +1 -1
- package/dist/internal/toolkit.js +10 -31
- package/dist/internal/toolkit.js.map +1 -1
- package/dist/internal/tools.js +1 -20
- package/dist/internal/tools.js.map +1 -1
- package/dist/internal/turns.d.ts.map +1 -1
- package/dist/internal/turns.js +2 -9
- package/dist/internal/turns.js.map +1 -1
- package/package.json +3 -2
- package/src/AssistantEvent.ts +0 -66
- package/src/AssistantLoop.ts +7 -5
- package/src/Block.ts +1 -1
- package/src/BlockKit.ts +12 -25
- package/src/ChatView.ts +10 -81
- package/src/Compaction.ts +2 -19
- package/src/ToolDiscovery.ts +5 -13
- package/src/ToolFailure.ts +1 -1
- package/src/index.ts +0 -10
- package/src/internal/chat.ts +4 -53
- package/src/internal/history.ts +2 -74
- package/src/internal/loop.ts +31 -101
- package/src/internal/specialists.ts +3 -7
- package/src/internal/toolkit.ts +17 -60
- package/src/internal/tools.ts +2 -53
- package/src/internal/turns.ts +7 -26
- package/dist/Decision.d.ts +0 -145
- package/dist/Decision.d.ts.map +0 -1
- package/dist/Decision.js +0 -178
- package/dist/Decision.js.map +0 -1
- package/dist/DecisionId.d.ts +0 -37
- package/dist/DecisionId.d.ts.map +0 -1
- package/dist/DecisionId.js +0 -44
- package/dist/DecisionId.js.map +0 -1
- package/dist/internal/decisions.js +0 -66
- package/dist/internal/decisions.js.map +0 -1
- package/src/Decision.ts +0 -241
- package/src/DecisionId.ts +0 -46
- package/src/internal/decisions.ts +0 -131
package/src/internal/loop.ts
CHANGED
|
@@ -14,20 +14,14 @@ import * as Predicate from "effect/Predicate"
|
|
|
14
14
|
import * as Schema from "effect/Schema"
|
|
15
15
|
import * as Stream from "effect/Stream"
|
|
16
16
|
import * as AssistantEvent from "../AssistantEvent.ts"
|
|
17
|
-
import * as DecisionId from "../DecisionId.ts"
|
|
18
17
|
import type * as Instructions from "../Instructions.ts"
|
|
19
|
-
import * as Decisions from "./decisions.ts"
|
|
20
18
|
import * as History from "./history.ts"
|
|
21
19
|
import * as Telemetry from "./telemetry.ts"
|
|
22
20
|
import * as Toolkits from "./toolkit.ts"
|
|
23
21
|
import { isTurnEnding } from "./tools.ts"
|
|
24
22
|
|
|
25
23
|
/** @internal */
|
|
26
|
-
export type Event =
|
|
27
|
-
| AssistantEvent.ModelOutput
|
|
28
|
-
| AssistantEvent.ToolStarted
|
|
29
|
-
| AssistantEvent.ToolResult
|
|
30
|
-
| AssistantEvent.DecisionRequested
|
|
24
|
+
export type Event = AssistantEvent.ModelOutput | AssistantEvent.ToolStarted | AssistantEvent.ToolResult
|
|
31
25
|
|
|
32
26
|
// The assistant's own tools with the turn's services, so an app's own loop in a turn can message the user. A
|
|
33
27
|
// specialist's run has none.
|
|
@@ -79,10 +73,7 @@ export interface StepResult {
|
|
|
79
73
|
|
|
80
74
|
// The loop runs the calls itself, after storing each as started, so the model call runs none.
|
|
81
75
|
const requestToolkit = (definitions: ReadonlyArray<Tool.Any>): Toolkit.WithHandler<Record<string, Tool.Any>> => ({
|
|
82
|
-
|
|
83
|
-
tools: Object.fromEntries(
|
|
84
|
-
definitions.map((tool) => [tool.name, tool.needsApproval === undefined ? tool : tool.setNeedsApproval(false)])
|
|
85
|
-
),
|
|
76
|
+
tools: Object.fromEntries(definitions.map((tool) => [tool.name, tool])),
|
|
86
77
|
handle: () => Effect.succeed(Stream.empty)
|
|
87
78
|
})
|
|
88
79
|
|
|
@@ -120,73 +111,28 @@ const runCall = Effect.fnUntraced(function* <R>(tools: Toolkits.Tools<R>, call:
|
|
|
120
111
|
return AssistantEvent.ToolResult.make({ callId: call.id, tool: call.name, ...result })
|
|
121
112
|
})
|
|
122
113
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
readonly approved: boolean
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// Only a turn parks: a specialist's call fails instead.
|
|
129
|
-
const park = Effect.fnUntraced(
|
|
130
|
-
function* (call: AssistantEvent.ToolCallPart) {
|
|
131
|
-
if (Option.isNone(yield* Decisions.TurnDecisions)) {
|
|
132
|
-
return AssistantEvent.ToolResult.make({
|
|
133
|
-
callId: call.id,
|
|
134
|
-
tool: call.name,
|
|
135
|
-
...Toolkits.failed(
|
|
136
|
-
`${call.name} needs the user's decision, and only the assistant can ask them. Don't run it: say in your answer what needs deciding, with the details.`,
|
|
137
|
-
"NeedsDecision"
|
|
138
|
-
)
|
|
139
|
-
})
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
return AssistantEvent.DecisionRequested.make({
|
|
143
|
-
id: DecisionId.fromCallId(call.id),
|
|
144
|
-
origin: AssistantEvent.CallOrigin.make({ callId: call.id, tool: call.name, params: call.params })
|
|
145
|
-
})
|
|
146
|
-
},
|
|
147
|
-
(effect, call) => effect.pipe(Telemetry.toolSpan(call))
|
|
148
|
-
)
|
|
149
|
-
|
|
150
|
-
// Every `ToolStarted` is stored before any call runs. `then` is told whether the model has to read the results: one
|
|
151
|
-
// failed, or waits for the user's decision.
|
|
114
|
+
// Every `ToolStarted` is stored before any call runs. `then` is told whether one of the calls failed, so the model
|
|
115
|
+
// has to read the results.
|
|
152
116
|
const runCalls = <R, S, E, R2>(
|
|
153
117
|
tools: Toolkits.Tools<R>,
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
then: (reread: boolean) => Stream.Stream<Step<never, S>, E, R2>
|
|
118
|
+
calls: ReadonlyArray<AssistantEvent.ToolCallPart>,
|
|
119
|
+
then: (failed: boolean) => Stream.Stream<Step<never, S>, E, R2>
|
|
157
120
|
): Stream.Stream<Step<Event, S>, E, R | R2> =>
|
|
158
|
-
Stream.
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
const results = Effect.forEach(pending, ({ call }, i) => (parks[i] ? park(call) : runCall(tools, call)), {
|
|
169
|
-
concurrency: "unbounded"
|
|
170
|
-
})
|
|
171
|
-
|
|
172
|
-
const reread = (done: ReadonlyArray<Event>) =>
|
|
173
|
-
done.some((event) => Predicate.isTagged(event, "DecisionRequested") || isFailure(event))
|
|
174
|
-
|
|
175
|
-
return Stream.fromIterable(started).pipe(
|
|
176
|
-
Stream.concat(
|
|
177
|
-
Stream.unwrap(
|
|
178
|
-
Effect.gen(function* () {
|
|
179
|
-
const done = yield* results
|
|
180
|
-
|
|
181
|
-
return Stream.fromIterable(done.map(value)).pipe(Stream.concat(then(reread(done))))
|
|
182
|
-
})
|
|
183
|
-
)
|
|
184
|
-
)
|
|
121
|
+
Stream.fromIterable(
|
|
122
|
+
calls.map((call) => value(AssistantEvent.ToolStarted.make({ callId: call.id, tool: call.name })))
|
|
123
|
+
).pipe(
|
|
124
|
+
Stream.concat(
|
|
125
|
+
Stream.unwrap(
|
|
126
|
+
Effect.gen(function* () {
|
|
127
|
+
const done = yield* Effect.forEach(calls, (call) => runCall(tools, call), { concurrency: "unbounded" })
|
|
128
|
+
|
|
129
|
+
return Stream.fromIterable(done.map(value)).pipe(Stream.concat(then(done.some(isFailure))))
|
|
130
|
+
})
|
|
185
131
|
)
|
|
186
|
-
|
|
132
|
+
)
|
|
187
133
|
)
|
|
188
134
|
|
|
189
|
-
const isFailure = (
|
|
135
|
+
const isFailure = (result: AssistantEvent.ToolResult) => result.outcome === "failure"
|
|
190
136
|
|
|
191
137
|
/** @internal */
|
|
192
138
|
export type Step<A, S> = Data.TaggedEnum<{
|
|
@@ -246,6 +192,9 @@ export const step = <R, S, E2 = never, R2 = never>(
|
|
|
246
192
|
const instructions = Predicate.isString(options.instructions) ? { text: options.instructions } : options.instructions
|
|
247
193
|
|
|
248
194
|
const after = (result: StepResult) => Stream.suspend(() => then(result))
|
|
195
|
+
const needingApproval = Toolkits.needingApproval(toolkit.definitions)
|
|
196
|
+
|
|
197
|
+
if (needingApproval.length > 0) return Stream.die(Toolkits.needsApprovalMessage(needingApproval))
|
|
249
198
|
|
|
250
199
|
return Stream.unwrap(
|
|
251
200
|
Effect.gen(function* () {
|
|
@@ -253,23 +202,9 @@ export const step = <R, S, E2 = never, R2 = never>(
|
|
|
253
202
|
// Calls a crash cut off, with no result: they run again, with the same ids, before the model goes on.
|
|
254
203
|
const cutOff = History.unansweredCalls(history)
|
|
255
204
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
const approved = Option.isNone(decisions)
|
|
260
|
-
? []
|
|
261
|
-
: (yield* decisions.value.approved).filter((call) =>
|
|
262
|
-
toolkit.definitions.some((tool) => tool.name === call.name)
|
|
263
|
-
)
|
|
264
|
-
|
|
265
|
-
if (cutOff.length > 0 || approved.length > 0) {
|
|
266
|
-
const pending = [
|
|
267
|
-
...cutOff.map((call) => ({ call, approved: false })),
|
|
268
|
-
...approved.map((call) => ({ call, approved: true }))
|
|
269
|
-
]
|
|
270
|
-
|
|
271
|
-
return runCalls(toolkit, pending, history, () =>
|
|
272
|
-
after({ output: undefined, done: false, historyLength: history.length + pending.length })
|
|
205
|
+
if (cutOff.length > 0) {
|
|
206
|
+
return runCalls(toolkit, cutOff, () =>
|
|
207
|
+
after({ output: undefined, done: false, historyLength: history.length + cutOff.length })
|
|
273
208
|
)
|
|
274
209
|
}
|
|
275
210
|
|
|
@@ -295,20 +230,15 @@ export const step = <R, S, E2 = never, R2 = never>(
|
|
|
295
230
|
const calls = toolCalls(output.parts)
|
|
296
231
|
const historyLength = history.length + Prompt.fromResponseParts(output.parts).content.length + calls.length
|
|
297
232
|
|
|
298
|
-
// A failed call doesn't end the turn: a message that failed never reached the user, so the model reads why.
|
|
299
|
-
// does a parked one: the model asks the user for the decision it waits for.
|
|
233
|
+
// A failed call doesn't end the turn: a message that failed never reached the user, so the model reads why.
|
|
300
234
|
return Stream.make(value(output)).pipe(
|
|
301
235
|
Stream.concat(
|
|
302
|
-
runCalls(
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
output,
|
|
309
|
-
done: calls.length === 0 || (!reread && calls.some(isTurnEnding)),
|
|
310
|
-
historyLength
|
|
311
|
-
})
|
|
236
|
+
runCalls(toolkit, calls, (failed) =>
|
|
237
|
+
after({
|
|
238
|
+
output,
|
|
239
|
+
done: calls.length === 0 || (!failed && calls.some(isTurnEnding)),
|
|
240
|
+
historyLength
|
|
241
|
+
})
|
|
312
242
|
)
|
|
313
243
|
)
|
|
314
244
|
)
|
|
@@ -26,7 +26,6 @@ import * as Skill from "../Skill.ts"
|
|
|
26
26
|
import * as Skills from "./skills.ts"
|
|
27
27
|
import * as Specialist from "../Specialist.ts"
|
|
28
28
|
import * as ToolFailure from "../ToolFailure.ts"
|
|
29
|
-
import * as Decisions from "./decisions.ts"
|
|
30
29
|
import { toolMessage, userText } from "./history.ts"
|
|
31
30
|
import { mapText } from "./instructions.ts"
|
|
32
31
|
import * as Telemetry from "./telemetry.ts"
|
|
@@ -96,10 +95,9 @@ export const runBrief = Effect.fnUntraced(
|
|
|
96
95
|
}
|
|
97
96
|
}
|
|
98
97
|
|
|
99
|
-
// A run lives in memory, so there's nothing to run again after a crash: `ToolStarted` isn't kept.
|
|
100
|
-
// call (it has no user to ask), so there's no `DecisionRequested` either.
|
|
98
|
+
// A run lives in memory, so there's nothing to run again after a crash: `ToolStarted` isn't kept.
|
|
101
99
|
const record = Effect.fnUntraced(function* (event: AssistantLoop.Event) {
|
|
102
|
-
if (Predicate.isTagged(event, "ToolStarted")
|
|
100
|
+
if (Predicate.isTagged(event, "ToolStarted")) return
|
|
103
101
|
|
|
104
102
|
if (Predicate.isTagged(event, "ToolResult")) return yield* add([toolMessage(event)])
|
|
105
103
|
|
|
@@ -272,9 +270,7 @@ export const makeRunner = Effect.fnUntraced(function* (options: {
|
|
|
272
270
|
chatId: chat.chatId
|
|
273
271
|
}).pipe(
|
|
274
272
|
Effect.provideContext(Context.merge(options.model, services)),
|
|
275
|
-
// Spawned from a turn, a run would inherit the turn's
|
|
276
|
-
Effect.provideService(Decisions.TurnDecisions, Option.none()),
|
|
277
|
-
// Nor its own tools: a run can't message the user.
|
|
273
|
+
// Spawned from a turn, a run would inherit the turn's own tools: a run can't message the user.
|
|
278
274
|
Effect.provideService(Loop.TurnTools, Option.none()),
|
|
279
275
|
Effect.matchCause({
|
|
280
276
|
onSuccess: (answer): Outcome => ({ status: "answered", report: answer }),
|
package/src/internal/toolkit.ts
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import * as AiError from "effect/ai/AiError"
|
|
2
|
-
import type * as Prompt from "effect/ai/Prompt"
|
|
3
2
|
import type * as Tool from "effect/ai/Tool"
|
|
4
3
|
import type * as Toolkit from "effect/ai/Toolkit"
|
|
5
4
|
import type * as Context from "effect/Context"
|
|
6
5
|
import * as Effect from "effect/Effect"
|
|
7
6
|
import * as Option from "effect/Option"
|
|
8
|
-
import * as Predicate from "effect/Predicate"
|
|
9
7
|
import * as Schema from "effect/Schema"
|
|
10
8
|
import * as Stream from "effect/Stream"
|
|
11
9
|
import type * as AssistantEvent from "../AssistantEvent.ts"
|
|
@@ -25,17 +23,13 @@ export interface CallResult {
|
|
|
25
23
|
export interface Tools<out R = never> {
|
|
26
24
|
readonly definitions: ReadonlyArray<Tool.Any>
|
|
27
25
|
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
26
|
}
|
|
33
27
|
|
|
34
28
|
/** @internal */
|
|
35
|
-
export type ToolServices<T extends Record<string, Tool.Any>> =
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
29
|
+
export type ToolServices<T extends Record<string, Tool.Any>> = Exclude<
|
|
30
|
+
Tool.HandlerServices<T[keyof T]>,
|
|
31
|
+
AssistantToolCall
|
|
32
|
+
>
|
|
39
33
|
|
|
40
34
|
const encodeFailure = Schema.encodeSync(ToolFailure.ToolFailure)
|
|
41
35
|
|
|
@@ -66,36 +60,15 @@ const hasTool = <T extends Record<string, Tool.Any>>(
|
|
|
66
60
|
name: PropertyKey
|
|
67
61
|
): name is keyof T => Object.hasOwn(toolkit.tools, name)
|
|
68
62
|
|
|
69
|
-
//
|
|
70
|
-
|
|
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.
|
|
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.
|
|
76
65
|
/** @internal */
|
|
77
|
-
export const
|
|
78
|
-
|
|
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
|
|
66
|
+
export const needingApproval = (definitions: ReadonlyArray<Tool.Any>): ReadonlyArray<string> =>
|
|
67
|
+
definitions.flatMap((tool) => (tool.needsApproval === undefined || tool.needsApproval === false ? [] : [tool.name]))
|
|
85
68
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
})
|
|
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.`
|
|
99
72
|
|
|
100
73
|
// The toolkit decodes the params with the tool's schema before its handler sees them, and a call whose params don't
|
|
101
74
|
// fit fails with a validation error the model reads: the model's JSON is what `handle` takes.
|
|
@@ -144,8 +117,7 @@ export const fromWithHandler = <T extends Record<string, Tool.Any>>(
|
|
|
144
117
|
toolkit: Toolkit.WithHandler<T>
|
|
145
118
|
): Tools<ToolServices<T>> => ({
|
|
146
119
|
definitions: Object.values(toolkit.tools),
|
|
147
|
-
run: (call) => runNative(toolkit, call)
|
|
148
|
-
needsDecision: (call, history) => needsDecision(toolkit, call, history)
|
|
120
|
+
run: (call) => runNative(toolkit, call)
|
|
149
121
|
})
|
|
150
122
|
|
|
151
123
|
/** @internal */
|
|
@@ -155,8 +127,7 @@ export const fromToolkit = <T extends Record<string, Tool.Any>>(
|
|
|
155
127
|
definitions: Object.values(toolkit.tools),
|
|
156
128
|
run: Effect.fnUntraced(function* (call) {
|
|
157
129
|
return yield* runNative(yield* toolkit, call)
|
|
158
|
-
})
|
|
159
|
-
needsDecision: (call, history) => needsDecision(toolkit, call, history)
|
|
130
|
+
})
|
|
160
131
|
})
|
|
161
132
|
|
|
162
133
|
// The handlers use services of the loop they run in, so they're made for each call, with its context.
|
|
@@ -171,8 +142,7 @@ export const withContext = <T extends Record<string, Tool.Any>, R>(
|
|
|
171
142
|
const resolved = yield* Effect.provideContext(toolkit, yield* toolkit.toHandlers(handlers(context)))
|
|
172
143
|
|
|
173
144
|
return yield* runNative(resolved, call)
|
|
174
|
-
})
|
|
175
|
-
needsDecision: (call, history) => needsDecision(toolkit, call, history)
|
|
145
|
+
})
|
|
176
146
|
})
|
|
177
147
|
|
|
178
148
|
/** @internal */
|
|
@@ -192,26 +162,15 @@ export const merge = <A, B>(self: Tools<A>, that: Tools<B>): Tools<A | B> => {
|
|
|
192
162
|
|
|
193
163
|
return {
|
|
194
164
|
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)
|
|
165
|
+
run: (call) => (inSelf(call) ? self.run(call) : that.run(call))
|
|
198
166
|
}
|
|
199
167
|
}
|
|
200
168
|
|
|
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
169
|
// A turn's own services, for a loop the app writes.
|
|
210
170
|
/** @internal */
|
|
211
171
|
export const provide = <R>(tools: Tools<R>, context: Context.Context<R>): Tools => ({
|
|
212
172
|
definitions: tools.definitions,
|
|
213
|
-
run: (call) => Effect.provideContext(tools.run(call), context)
|
|
214
|
-
needsDecision: (call, history) => Effect.provideContext(needsDecisionOf(tools, call, history), context)
|
|
173
|
+
run: (call) => Effect.provideContext(tools.run(call), context)
|
|
215
174
|
})
|
|
216
175
|
|
|
217
176
|
/** @internal */
|
|
@@ -225,7 +184,5 @@ export const duplicateNames = (tools: Tools<unknown>): ReadonlyArray<string> =>
|
|
|
225
184
|
/** @internal */
|
|
226
185
|
export const runOnly = <R>(tools: Tools<R>, names: ReadonlyArray<string>, message: string): Tools<R> => ({
|
|
227
186
|
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)
|
|
187
|
+
run: (call) => (names.includes(call.name) ? tools.run(call) : Effect.succeed(failed(message, "NotNow")))
|
|
231
188
|
})
|
package/src/internal/tools.ts
CHANGED
|
@@ -6,9 +6,8 @@ import * as Predicate from "effect/Predicate"
|
|
|
6
6
|
import * as Schema from "effect/Schema"
|
|
7
7
|
import * as AssistantEvent from "../AssistantEvent.ts"
|
|
8
8
|
import { AssistantToolCall } from "../AssistantToolCall.ts"
|
|
9
|
-
import * as BlockKit from "../BlockKit.ts"
|
|
9
|
+
import type * as BlockKit from "../BlockKit.ts"
|
|
10
10
|
import { CurrentChat } from "../CurrentChat.ts"
|
|
11
|
-
import * as Decision from "../Decision.ts"
|
|
12
11
|
import * as ToolFailure from "../ToolFailure.ts"
|
|
13
12
|
import * as Toolkits from "./toolkit.ts"
|
|
14
13
|
import * as Uuid from "./uuid.ts"
|
|
@@ -77,45 +76,6 @@ const plainTools = Effect.gen(function* () {
|
|
|
77
76
|
return yield* Effect.provideContext(PlainTools, yield* plainHandlers)
|
|
78
77
|
})
|
|
79
78
|
|
|
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
79
|
const blockTools = Effect.fnUntraced(function* (kit: BlockKit.BlockKit) {
|
|
120
80
|
const catalog = kit.blocks.map((block) => `- ${block.kind}: ${block.description}`).join("\n")
|
|
121
81
|
const kinds = kit.blocks.map((block) => block.kind)
|
|
@@ -137,7 +97,7 @@ ${catalog}`,
|
|
|
137
97
|
),
|
|
138
98
|
closes: Schema.optional(Schema.Array(Schema.String)).annotate({
|
|
139
99
|
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.
|
|
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."
|
|
141
101
|
})
|
|
142
102
|
}),
|
|
143
103
|
success: Schema.String,
|
|
@@ -160,8 +120,6 @@ ${catalog}`,
|
|
|
160
120
|
const id = yield* messageId
|
|
161
121
|
const closed = closes === undefined || closes.length === 0 ? {} : { closes }
|
|
162
122
|
|
|
163
|
-
if (closes !== undefined && closes.length > 0) yield* checkCloses(closes)
|
|
164
|
-
|
|
165
123
|
if (block === undefined) {
|
|
166
124
|
yield* append(AssistantEvent.AssistantMessage.make({ id, text, ...closed }))
|
|
167
125
|
|
|
@@ -174,15 +132,6 @@ ${catalog}`,
|
|
|
174
132
|
return yield* ToolFailure.fail(`Those props don't fit a ${block.kind} block.`)
|
|
175
133
|
}
|
|
176
134
|
|
|
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
135
|
yield* append(
|
|
187
136
|
AssistantEvent.AssistantMessage.make({
|
|
188
137
|
id,
|
package/src/internal/turns.ts
CHANGED
|
@@ -18,12 +18,10 @@ import type * as AssistantLoop from "../AssistantLoop.ts"
|
|
|
18
18
|
import * as Loop from "./loop.ts"
|
|
19
19
|
import type * as BlockKit from "../BlockKit.ts"
|
|
20
20
|
import type { CurrentChat } from "../CurrentChat.ts"
|
|
21
|
-
import * as Decision from "../Decision.ts"
|
|
22
21
|
import * as Instructions from "../Instructions.ts"
|
|
23
22
|
import * as Skill from "../Skill.ts"
|
|
24
23
|
import * as Skills from "./skills.ts"
|
|
25
24
|
import type * as Specialist from "../Specialist.ts"
|
|
26
|
-
import * as Decisions from "./decisions.ts"
|
|
27
25
|
import * as AppEvents from "./events.ts"
|
|
28
26
|
import * as History from "./history.ts"
|
|
29
27
|
import { mapText } from "./instructions.ts"
|
|
@@ -87,12 +85,6 @@ export const register = Effect.fnUntraced(function* (
|
|
|
87
85
|
) {
|
|
88
86
|
const model = yield* Effect.context<LanguageModel.LanguageModel>()
|
|
89
87
|
|
|
90
|
-
const decisionBlock = definition.blocks?.byKind.get(Decision.kind)
|
|
91
|
-
|
|
92
|
-
if (decisionBlock !== undefined && decisionBlock !== Decision.block) {
|
|
93
|
-
return yield* Effect.die(`The ${Decision.kind} block kind is the library's: use Decision.block.`)
|
|
94
|
-
}
|
|
95
|
-
|
|
96
88
|
const specialists = yield* Effect.forEach(
|
|
97
89
|
definition.specialists,
|
|
98
90
|
Effect.fnUntraced(function* (specialist) {
|
|
@@ -172,10 +164,7 @@ const closeOpenCalls = Effect.fnUntraced(function* (chat: Writer, startSeq: numb
|
|
|
172
164
|
entries.flatMap(({ event }) => (Predicate.isTagged(event, "ToolStarted") ? [event.callId] : []))
|
|
173
165
|
)
|
|
174
166
|
|
|
175
|
-
|
|
176
|
-
const all = yield* chat.entries
|
|
177
|
-
const approved = Decisions.approvedCalls(all).filter((call) => started.has(call.id))
|
|
178
|
-
const open = [...History.unansweredCalls(History.fromEntries(entries, () => undefined)), ...approved]
|
|
167
|
+
const open = History.unansweredCalls(History.fromEntries(entries, () => undefined))
|
|
179
168
|
|
|
180
169
|
if (open.length === 0) return
|
|
181
170
|
|
|
@@ -335,23 +324,15 @@ const makeTurnApi = (
|
|
|
335
324
|
const { system, toolkit, view } = prepare(options)
|
|
336
325
|
const twice = Toolkits.duplicateNames(toolkit)
|
|
337
326
|
|
|
338
|
-
const deciding = toolkit.definitions.filter(
|
|
339
|
-
(tool) => tool.needsApproval !== undefined && tool.needsApproval !== false
|
|
340
|
-
)
|
|
341
|
-
|
|
342
327
|
const loop =
|
|
343
328
|
twice.length > 0
|
|
344
329
|
? Stream.die(`Tool names used twice, like one named after a built-in: ${twice.join(", ")}`)
|
|
345
|
-
:
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
)
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
toolkit,
|
|
352
|
-
history: history(view),
|
|
353
|
-
maxSteps: options.maxSteps ?? 16
|
|
354
|
-
})
|
|
330
|
+
: Loop.run({
|
|
331
|
+
instructions: system,
|
|
332
|
+
toolkit,
|
|
333
|
+
history: history(view),
|
|
334
|
+
maxSteps: options.maxSteps ?? 16
|
|
335
|
+
})
|
|
355
336
|
|
|
356
337
|
return Stream.concat(loop, replyPass({ instructions: system, toolkit, view, nudge: options.replyNudge }))
|
|
357
338
|
}
|
package/dist/Decision.d.ts
DELETED
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
import { AnyEntry, AssistantEvent_d_exports, DecisionOutcome, DecisionRequested, ToolResult } from "./AssistantEvent.js";
|
|
2
|
-
import { Block, Block_d_exports } from "./Block.js";
|
|
3
|
-
import { InputId } from "./InputId.js";
|
|
4
|
-
import * as Schema from "effect/Schema";
|
|
5
|
-
declare namespace Decision_d_exports {
|
|
6
|
-
export { Answer, Props, State, answerAll, apply, block, fromEntries, kind };
|
|
7
|
-
}
|
|
8
|
-
/**
|
|
9
|
-
* The kind of the decision block.
|
|
10
|
-
*
|
|
11
|
-
* @category utils
|
|
12
|
-
* @since 0.1.0
|
|
13
|
-
*/
|
|
14
|
-
export declare const kind = "decision";
|
|
15
|
-
/**
|
|
16
|
-
* Schema for the decision block's props, which list the decisions it asks for and, optionally,
|
|
17
|
-
* its buttons' labels.
|
|
18
|
-
*
|
|
19
|
-
* @category schemas
|
|
20
|
-
* @since 0.1.0
|
|
21
|
-
*/
|
|
22
|
-
export declare const Props: Schema.Struct<{
|
|
23
|
-
readonly decisionIds: Schema.$Array<Schema.String>;
|
|
24
|
-
readonly approveLabel: Schema.optional<Schema.String>;
|
|
25
|
-
readonly declineLabel: Schema.optional<Schema.String>;
|
|
26
|
-
}>;
|
|
27
|
-
/**
|
|
28
|
-
* Represents the decision block's props.
|
|
29
|
-
*
|
|
30
|
-
* @category models
|
|
31
|
-
* @since 0.1.0
|
|
32
|
-
*/
|
|
33
|
-
export type Props = typeof Props.Type;
|
|
34
|
-
/**
|
|
35
|
-
* Schema for the decision block's answer, which holds an outcome for every decision the block
|
|
36
|
-
* asks for, by id.
|
|
37
|
-
*
|
|
38
|
-
* @category schemas
|
|
39
|
-
* @since 0.1.0
|
|
40
|
-
*/
|
|
41
|
-
export declare const Answer: Schema.Struct<{
|
|
42
|
-
readonly decisions: Schema.$Record<Schema.String, Schema.Literals<readonly ["approved", "declined"]>>;
|
|
43
|
-
}>;
|
|
44
|
-
/**
|
|
45
|
-
* Represents the decision block's answer.
|
|
46
|
-
*
|
|
47
|
-
* @category models
|
|
48
|
-
* @since 0.1.0
|
|
49
|
-
*/
|
|
50
|
-
export type Answer = typeof Answer.Type;
|
|
51
|
-
/**
|
|
52
|
-
* Block with approve and decline buttons for the decisions waiting for the user.
|
|
53
|
-
*
|
|
54
|
-
* **Details**
|
|
55
|
-
*
|
|
56
|
-
* Put it in the assistant's `BlockKit`. An assistant with a tool that needs a decision must have
|
|
57
|
-
* it. The client renders it like any other block, with the calls from {@link fromEntries} (their
|
|
58
|
-
* tool and params).
|
|
59
|
-
*
|
|
60
|
-
* **Example** (Asking the user before placing an order)
|
|
61
|
-
*
|
|
62
|
-
* ```ts
|
|
63
|
-
* import { Assistant, BlockKit, Decision } from "effect-assistant"
|
|
64
|
-
* import * as Tool from "effect/ai/Tool"
|
|
65
|
-
* import * as Toolkit from "effect/ai/Toolkit"
|
|
66
|
-
* import * as Effect from "effect/Effect"
|
|
67
|
-
* import * as Layer from "effect/Layer"
|
|
68
|
-
* import * as Schema from "effect/Schema"
|
|
69
|
-
*
|
|
70
|
-
* export const Shop = Assistant.make("shop", { blocks: BlockKit.make([Decision.block]) })
|
|
71
|
-
*
|
|
72
|
-
* const OrderTools = Toolkit.make(
|
|
73
|
-
* Tool.make("place_order", {
|
|
74
|
-
* description: "Places an order.",
|
|
75
|
-
* parameters: Schema.Struct({ sku: Schema.String, quantity: Schema.Int }),
|
|
76
|
-
* success: Schema.Struct({ reference: Schema.String }),
|
|
77
|
-
* needsApproval: true
|
|
78
|
-
* })
|
|
79
|
-
* )
|
|
80
|
-
*
|
|
81
|
-
* const OrderToolsLive = OrderTools.toLayer({
|
|
82
|
-
* place_order: () => Effect.succeed({ reference: "R-1042" })
|
|
83
|
-
* })
|
|
84
|
-
*
|
|
85
|
-
* // The call waits until the user answers the decision block.
|
|
86
|
-
* const ShopLive = Shop.toLayer((turn) =>
|
|
87
|
-
* turn.loop({ instructions: "You place orders for the team.", toolkit: OrderTools })
|
|
88
|
-
* ).pipe(Layer.provide(OrderToolsLive))
|
|
89
|
-
* ```
|
|
90
|
-
*
|
|
91
|
-
* @category constructors
|
|
92
|
-
* @since 0.1.0
|
|
93
|
-
*/
|
|
94
|
-
export declare const block: Block<"decision", Schema.Struct<{
|
|
95
|
-
readonly decisionIds: Schema.$Array<Schema.String>;
|
|
96
|
-
readonly approveLabel: Schema.optional<Schema.String>;
|
|
97
|
-
readonly declineLabel: Schema.optional<Schema.String>;
|
|
98
|
-
}>, Schema.Struct<{
|
|
99
|
-
readonly decisions: Schema.$Record<Schema.String, Schema.Literals<readonly ["approved", "declined"]>>;
|
|
100
|
-
}>>;
|
|
101
|
-
/**
|
|
102
|
-
* Creates an answer that gives every decision the block asks for the same outcome.
|
|
103
|
-
*
|
|
104
|
-
* This is what an "Approve" or "Decline" button sends.
|
|
105
|
-
*
|
|
106
|
-
* @category constructors
|
|
107
|
-
* @since 0.1.0
|
|
108
|
-
*/
|
|
109
|
-
export declare const answerAll: (props: Props, outcome: DecisionOutcome) => Answer;
|
|
110
|
-
/**
|
|
111
|
-
* Represents the state of a decision.
|
|
112
|
-
*
|
|
113
|
-
* @category models
|
|
114
|
-
* @since 0.1.0
|
|
115
|
-
*/
|
|
116
|
-
export interface State {
|
|
117
|
-
readonly request: DecisionRequested;
|
|
118
|
-
/** How the user decided, or `undefined` while the decision waits. */
|
|
119
|
-
readonly outcome: DecisionOutcome | undefined;
|
|
120
|
-
/** The id of the answer that decided it. */
|
|
121
|
-
readonly answer: InputId | undefined;
|
|
122
|
-
/** Whether its `DecisionResolved` is stored. A workflow's decision has been passed on by then. */
|
|
123
|
-
readonly resolved: boolean;
|
|
124
|
-
/** The approved call's result, once the call has run. */
|
|
125
|
-
readonly result: ToolResult | undefined;
|
|
126
|
-
}
|
|
127
|
-
/**
|
|
128
|
-
* Returns the decision states after one more event.
|
|
129
|
-
*
|
|
130
|
-
* @category utils
|
|
131
|
-
* @since 0.1.0
|
|
132
|
-
*/
|
|
133
|
-
export declare const apply: (states: ReadonlyMap<string, State>, event: {
|
|
134
|
-
readonly _tag: string;
|
|
135
|
-
}) => ReadonlyMap<string, State>;
|
|
136
|
-
/**
|
|
137
|
-
* Returns every decision in the log and where it stands, by id.
|
|
138
|
-
*
|
|
139
|
-
* @category utils
|
|
140
|
-
* @since 0.1.0
|
|
141
|
-
*/
|
|
142
|
-
export declare const fromEntries: (entries: ReadonlyArray<AnyEntry>) => ReadonlyMap<string, State>;
|
|
143
|
-
//#endregion
|
|
144
|
-
export { Decision_d_exports };
|
|
145
|
-
//# sourceMappingURL=Decision.d.ts.map
|
package/dist/Decision.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"Decision.d.ts","names":[],"sources":["../src/Decision.ts"],"mappings":";;;;;;;;;;;;;qBAyBa;;;;;;;;qBASA,OAAK,OAAA;;;;;;;;;;;YAqBN,eAAe,MAAM;;;;;;;;qBASpB,QAAM,OAAA;;;;;;;;;YAUP,gBAAgB,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBA6CtB,OAAK,kBAAA,OAAA;;;;;;;;;;;;;;;qBAeL,YAAS,OAAW,OAAK,SAAW,oBAAiC;;;;;;;iBAUjE;WACN,SAAS;;WAET,SAAS;;WAET,QAAQ;;WAER;;WAEA,QAAQ;;;;;;;;qBAWN,QAAK,QACR,oBAAoB,QAAM;WAChB;MACjB,oBAAoB;;;;;;;qBAmEV,cAAW,SAAa,cAAc,cAA2B,oBAAoB"}
|