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.
Files changed (80) hide show
  1. package/README.md +1 -1
  2. package/dist/AssistantEvent.d.ts +3 -66
  3. package/dist/AssistantEvent.d.ts.map +1 -1
  4. package/dist/AssistantEvent.js +1 -51
  5. package/dist/AssistantEvent.js.map +1 -1
  6. package/dist/AssistantLoop.d.ts +7 -5
  7. package/dist/AssistantLoop.d.ts.map +1 -1
  8. package/dist/AssistantLoop.js +6 -3
  9. package/dist/AssistantLoop.js.map +1 -1
  10. package/dist/Block.js.map +1 -1
  11. package/dist/BlockKit.d.ts +11 -15
  12. package/dist/BlockKit.d.ts.map +1 -1
  13. package/dist/BlockKit.js +3 -6
  14. package/dist/BlockKit.js.map +1 -1
  15. package/dist/ChatView.d.ts +7 -58
  16. package/dist/ChatView.d.ts.map +1 -1
  17. package/dist/ChatView.js +15 -68
  18. package/dist/ChatView.js.map +1 -1
  19. package/dist/Compaction.d.ts +2 -2
  20. package/dist/Compaction.d.ts.map +1 -1
  21. package/dist/Compaction.js +6 -13
  22. package/dist/Compaction.js.map +1 -1
  23. package/dist/ToolDiscovery.d.ts +4 -5
  24. package/dist/ToolDiscovery.d.ts.map +1 -1
  25. package/dist/ToolDiscovery.js +5 -4
  26. package/dist/ToolDiscovery.js.map +1 -1
  27. package/dist/ToolFailure.d.ts +1 -1
  28. package/dist/ToolFailure.js +1 -1
  29. package/dist/ToolFailure.js.map +1 -1
  30. package/dist/index.d.ts +1 -3
  31. package/dist/index.js +3 -5
  32. package/dist/internal/chat.js +6 -31
  33. package/dist/internal/chat.js.map +1 -1
  34. package/dist/internal/history.js +2 -40
  35. package/dist/internal/history.js.map +1 -1
  36. package/dist/internal/loop.d.ts +2 -2
  37. package/dist/internal/loop.d.ts.map +1 -1
  38. package/dist/internal/loop.js +20 -55
  39. package/dist/internal/loop.js.map +1 -1
  40. package/dist/internal/specialists.js +2 -3
  41. package/dist/internal/specialists.js.map +1 -1
  42. package/dist/internal/toolkit.d.ts +6 -20
  43. package/dist/internal/toolkit.d.ts.map +1 -1
  44. package/dist/internal/toolkit.js +10 -31
  45. package/dist/internal/toolkit.js.map +1 -1
  46. package/dist/internal/tools.js +1 -20
  47. package/dist/internal/tools.js.map +1 -1
  48. package/dist/internal/turns.d.ts.map +1 -1
  49. package/dist/internal/turns.js +2 -9
  50. package/dist/internal/turns.js.map +1 -1
  51. package/package.json +3 -2
  52. package/src/AssistantEvent.ts +0 -66
  53. package/src/AssistantLoop.ts +7 -5
  54. package/src/Block.ts +1 -1
  55. package/src/BlockKit.ts +12 -25
  56. package/src/ChatView.ts +10 -81
  57. package/src/Compaction.ts +2 -19
  58. package/src/ToolDiscovery.ts +5 -13
  59. package/src/ToolFailure.ts +1 -1
  60. package/src/index.ts +0 -10
  61. package/src/internal/chat.ts +4 -53
  62. package/src/internal/history.ts +2 -74
  63. package/src/internal/loop.ts +31 -101
  64. package/src/internal/specialists.ts +3 -7
  65. package/src/internal/toolkit.ts +17 -60
  66. package/src/internal/tools.ts +2 -53
  67. package/src/internal/turns.ts +7 -26
  68. package/dist/Decision.d.ts +0 -145
  69. package/dist/Decision.d.ts.map +0 -1
  70. package/dist/Decision.js +0 -178
  71. package/dist/Decision.js.map +0 -1
  72. package/dist/DecisionId.d.ts +0 -37
  73. package/dist/DecisionId.d.ts.map +0 -1
  74. package/dist/DecisionId.js +0 -44
  75. package/dist/DecisionId.js.map +0 -1
  76. package/dist/internal/decisions.js +0 -66
  77. package/dist/internal/decisions.js.map +0 -1
  78. package/src/Decision.ts +0 -241
  79. package/src/DecisionId.ts +0 -46
  80. package/src/internal/decisions.ts +0 -131
@@ -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
- // The loop decides which calls wait for the user's decision; the model call would ask for approvals of its own.
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
- interface Pending {
124
- readonly call: AssistantEvent.ToolCallPart
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
- pending: ReadonlyArray<Pending>,
155
- history: ReadonlyArray<Prompt.Message>,
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.unwrap(
159
- Effect.gen(function* () {
160
- const parks = yield* Effect.forEach(pending, ({ call, approved }) =>
161
- approved ? Effect.succeed(false) : Toolkits.needsDecisionOf(tools, call, history)
162
- )
163
-
164
- const started = pending.flatMap(({ call }, i) =>
165
- parks[i] ? [] : [value(AssistantEvent.ToolStarted.make({ callId: call.id, tool: call.name }))]
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 = (event: Event) => Predicate.isTagged(event, "ToolResult") && event.outcome === "failure"
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
- // Calls the user approved: they run as the model made them, before the model goes on. Not in a loop without them.
257
- const decisions = yield* Decisions.TurnDecisions
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. Nor
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
- toolkit,
304
- calls.map((call) => ({ call, approved: false })),
305
- history,
306
- (reread) =>
307
- after({
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. A run parks no
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") || Predicate.isTagged(event, "DecisionRequested")) return
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 decisions: it can't park a call or run an approved one.
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 }),
@@ -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
- | 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"]
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
- // 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.
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 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
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
- 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
- })
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
  })
@@ -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. A decision block never closes this way: only the user's answer in it decides."
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,
@@ -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
- // Approved calls this turn started that have no result are cut off like any other; one not started yet waits.
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
- : deciding.length > 0 && definition.blocks?.byKind.get(Decision.kind) === undefined
346
- ? Stream.die(
347
- `${deciding.map((tool) => tool.name).join(", ")} can wait for the user's decision: add Decision.block to the assistant's blocks.`
348
- )
349
- : Loop.run({
350
- instructions: system,
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
  }
@@ -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
@@ -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"}