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
package/src/ChatView.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * This module provides the view of a chat as its user sees it. A chat client receives this view
3
3
  * and nothing else.
4
4
  *
5
- * A {@link View} holds the chat's messages with their blocks and decisions, the chat's
5
+ * A {@link View} holds the chat's messages with their blocks, the chat's
6
6
  * {@link Activity}, and whether the last turn failed or the chat was deleted. It never contains
7
7
  * the model's output, tool calls or results, what the model read, the app's own events, or why a
8
8
  * turn failed. The server streams {@link Update}s by piping the assistant's `subscribe` through
@@ -13,40 +13,14 @@
13
13
  */
14
14
  import { dual } from "effect/Function"
15
15
  import * as Match from "effect/Match"
16
- import * as Option from "effect/Option"
17
16
  import * as Schema from "effect/Schema"
18
17
  import * as Stream from "effect/Stream"
19
18
  import * as AssistantActivity from "./AssistantActivity.ts"
20
19
  import * as AssistantEvent from "./AssistantEvent.ts"
21
20
  import * as BlockKit from "./BlockKit.ts"
22
- import * as Decisions from "./Decision.ts"
23
- import { DecisionId } from "./DecisionId.ts"
24
21
  import { InputId } from "./InputId.ts"
25
22
  import { toJson } from "./internal/schema.ts"
26
23
 
27
- /**
28
- * Schema for a decision as the user sees it on a decision block. It holds the call the decision is
29
- * for (its tool and params, which the app can display as it likes) and, once the user has
30
- * decided, the outcome.
31
- *
32
- * @category schemas
33
- * @since 0.1.0
34
- */
35
- export const PendingDecision = Schema.Struct({
36
- id: DecisionId,
37
- tool: Schema.String,
38
- params: Schema.Json,
39
- outcome: Schema.optional(AssistantEvent.DecisionOutcome)
40
- })
41
-
42
- /**
43
- * Represents a decision as the user sees it.
44
- *
45
- * @category models
46
- * @since 0.1.0
47
- */
48
- export type PendingDecision = typeof PendingDecision.Type
49
-
50
24
  /**
51
25
  * Schema for the status of a block. A block is `"open"` while it accepts an answer, `"answered"`
52
26
  * once the user has used it, and `"closed"` when it closed without an answer because the user
@@ -67,8 +41,8 @@ export type BlockStatus = typeof BlockStatus.Type
67
41
 
68
42
  /**
69
43
  * Schema for a block on an assistant's message. It holds the block's kind, its props (JSON, as
70
- * the block's props schema encodes them), its status, the user's answer (JSON, as the block's
71
- * answer schema encodes it), and, for a decision block, its decisions.
44
+ * the block's props schema encodes them), its status, and the user's answer (JSON, as the block's
45
+ * answer schema encodes it).
72
46
  *
73
47
  * @category schemas
74
48
  * @since 0.1.0
@@ -77,8 +51,7 @@ export const Block = Schema.Struct({
77
51
  kind: Schema.String,
78
52
  props: Schema.Json,
79
53
  status: BlockStatus,
80
- answer: Schema.optional(Schema.Json),
81
- decisions: Schema.optional(Schema.Array(PendingDecision))
54
+ answer: Schema.optional(Schema.Json)
82
55
  })
83
56
 
84
57
  /**
@@ -173,7 +146,7 @@ export type SpecialistWorking = typeof SpecialistWorking.Type
173
146
  *
174
147
  * `working` is true while the assistant runs a turn, or when a turn is about to start for
175
148
  * something that arrived. `specialists` lists the specialists that are working. When the chat is
176
- * idle, nothing is working, even if a workflow is waiting on a timer or a decision.
149
+ * idle, nothing is working, even if a workflow is waiting on a timer or on the user.
177
150
  *
178
151
  * @category schemas
179
152
  * @since 0.1.0
@@ -220,8 +193,8 @@ export type View = typeof View.Type
220
193
  /**
221
194
  * Schema for a change to a chat's view.
222
195
  *
223
- * An update holds each message that is new or changed (for example, a block answered or closed,
224
- * or a decision made), in full. It also holds the current activity, whether the last turn failed,
196
+ * An update holds each message that is new or changed (for example, a block answered or
197
+ * closed), in full. It also holds the current activity, whether the last turn failed,
225
198
  * and whether the chat was deleted. `seq` is the position in the chat's log that the update is up
226
199
  * to.
227
200
  *
@@ -349,10 +322,6 @@ interface Folding {
349
322
  readonly view: View
350
323
  }
351
324
 
352
- const decodeProps = Schema.decodeUnknownOption(Decisions.Props)
353
-
354
- const decodeAnswer = Schema.decodeUnknownOption(Decisions.Answer)
355
-
356
325
  const activityOf = (activity: AssistantActivity.State): Activity => ({
357
326
  working: activity.turnSince !== undefined || activity.queued,
358
327
  specialists: [...activity.specialists.values()].map((run) => ({
@@ -370,49 +339,24 @@ const sameActivity = (a: Activity, b: Activity) =>
370
339
  return other !== undefined && run.specialist === other.specialist && run.since === other.since
371
340
  })
372
341
 
373
- const decisionIds = (block: AssistantEvent.MessageBlock): ReadonlyArray<string> =>
374
- block.kind === Decisions.kind
375
- ? Option.match(decodeProps(block.props), {
376
- onNone: () => [],
377
- onSome: (props) => props.decisionIds
378
- })
379
- : []
380
-
381
342
  const folding = (wakes: ReadonlySet<string>): Folding => {
382
343
  let seq = 0
383
344
  let failed = false
384
345
  let deleted = false
385
346
  let activity = AssistantActivity.empty
386
347
  let blocks: ReadonlyMap<string, BlockKit.State> = new Map()
387
- let decisions: ReadonlyMap<string, Decisions.State> = new Map()
388
348
  // The messages in order, and the assistant's as stored: their blocks change as the chat goes on.
389
349
  const messages = new Map<string, Message>()
390
350
  const sent = new Map<string, AssistantEvent.AssistantMessage>()
391
- // Which messages ask for a decision, so a decision's outcome updates the block that asked for it.
392
- const asking = new Map<string, Array<string>>()
393
351
 
394
352
  const blockOf = (message: AssistantEvent.AssistantMessage): Block | undefined => {
395
353
  if (message.block === undefined) return undefined
396
354
 
397
355
  const state = blocks.get(message.id)
398
356
  const status = state === undefined || state.open ? "open" : state.answer === undefined ? "closed" : "answered"
399
- const ids = decisionIds(message.block)
400
-
401
- const shown = ids.flatMap((id) => {
402
- const decision = decisions.get(id)
403
-
404
- if (decision === undefined) return []
405
-
406
- const { tool, params } = decision.request.origin
407
- const shown = { id: decision.request.id, tool, params }
408
-
409
- return [decision.outcome === undefined ? shown : { ...shown, outcome: decision.outcome }]
410
- })
411
-
412
357
  const block: Block = { kind: message.block.kind, props: toJson(message.block.props), status }
413
- const answered = state?.answer === undefined ? block : { ...block, answer: toJson(state.answer.value) }
414
358
 
415
- return message.block.kind === Decisions.kind ? { ...answered, decisions: shown } : answered
359
+ return state?.answer === undefined ? block : { ...block, answer: toJson(state.answer.value) }
416
360
  }
417
361
 
418
362
  const viewOf = (message: AssistantEvent.AssistantMessage, at: number): AssistantMessage => {
@@ -447,27 +391,14 @@ const folding = (wakes: ReadonlySet<string>): Folding => {
447
391
  messages.set(message.id, viewOf(message, at))
448
392
  failed = false
449
393
 
450
- if (message.block !== undefined) {
451
- for (const id of decisionIds(message.block)) asking.set(id, [...(asking.get(id) ?? []), message.id])
452
- }
453
-
454
394
  return [message.id, ...(message.closes ?? [])]
455
395
  },
456
396
  BlockAnswer: (answer) => {
457
397
  failed = false
458
398
 
459
- const decided =
460
- answer.kind === Decisions.kind
461
- ? Option.match(decodeAnswer(answer.value), {
462
- onNone: () => [],
463
- onSome: ({ decisions }) => Object.keys(decisions).flatMap((id) => asking.get(id) ?? [])
464
- })
465
- : []
466
-
467
- return [answer.messageId, ...decided]
399
+ return [answer.messageId]
468
400
  },
469
401
  BlocksClosed: (closed) => closed.messageIds,
470
- DecisionResolved: (resolved) => asking.get(resolved.id) ?? [],
471
402
  TurnFailed: () => {
472
403
  failed = true
473
404
 
@@ -480,7 +411,6 @@ const folding = (wakes: ReadonlySet<string>): Folding => {
480
411
  TurnCompleted: () => [],
481
412
  SpecialistStarted: () => [],
482
413
  SpecialistFinished: () => [],
483
- DecisionRequested: () => [],
484
414
  ChatDeleted: () => {
485
415
  deleted = true
486
416
 
@@ -503,7 +433,6 @@ const folding = (wakes: ReadonlySet<string>): Folding => {
503
433
  }
504
434
 
505
435
  blocks = BlockKit.apply(blocks, event)
506
- decisions = Decisions.apply(decisions, event)
507
436
 
508
437
  const changed = [...new Set(touched(event, entry.at))].flatMap((id) => {
509
438
  const message = sent.get(id)
@@ -588,7 +517,7 @@ const wakesOf = (definition: Definition): ReadonlySet<string> =>
588
517
  *
589
518
  * The entries must stream from the start of the chat's log, as `assistant.subscribe(chatId)` does
590
519
  * without `after`. The fold reads every entry, because a message's view depends on earlier
591
- * entries: a block is answered or closed later, and a decision is made later. `after` only skips
520
+ * entries: a block is answered or closed later. `after` only skips
592
521
  * emitting the updates up to that `seq`, so a client that has the view up to a `seq` receives only
593
522
  * what changed since. Pass `after` here, not to `subscribe`: a subscription that starts after a
594
523
  * `seq` gives the fold an incomplete log, and the updates it produces are wrong.
package/src/Compaction.ts CHANGED
@@ -17,9 +17,7 @@ import type * as Assistant from "./Assistant.ts"
17
17
  import * as AssistantActivity from "./AssistantActivity.ts"
18
18
  import * as AssistantEvent from "./AssistantEvent.ts"
19
19
  import * as BlockKit from "./BlockKit.ts"
20
- import * as Decision from "./Decision.ts"
21
20
  import { userText } from "./internal/history.ts"
22
- import { toJsonText } from "./internal/schema.ts"
23
21
 
24
22
  /**
25
23
  * Represents the fields a summary event must have: its text and the last `seq` it covers.
@@ -55,8 +53,8 @@ export const inputTokens = (entries: ReadonlyArray<AssistantEvent.AnyEntry>): nu
55
53
  }
56
54
 
57
55
  /**
58
- * Returns the items that were open at `throughSeq` and are still open: decisions waiting for the
59
- * user, blocks the user hasn't answered, and specialists that are working. The result is text for
56
+ * Returns the items that were open at `throughSeq` and are still open: blocks the user hasn't
57
+ * answered, and specialists that are working. The result is text for
60
58
  * the model, or `None` when nothing is open.
61
59
  *
62
60
  * @category utils
@@ -71,20 +69,6 @@ export const openItems = (
71
69
 
72
70
  const opened = (seq: number) => seq <= throughSeq
73
71
 
74
- const decisions = [...Decision.fromEntries(entries).values()].flatMap((state) =>
75
- state.outcome === undefined &&
76
- opened(
77
- seqOf(
78
- (event) =>
79
- AssistantEvent.isLibraryEvent(event) &&
80
- Predicate.isTagged(event, "DecisionRequested") &&
81
- event.id === state.request.id
82
- )
83
- )
84
- ? [`- decision ${state.request.id}, for ${state.request.origin.tool}: ${toJsonText(state.request.origin.params)}`]
85
- : []
86
- )
87
-
88
72
  const blocks = [...BlockKit.fromEntries(entries)].flatMap(([messageId, state]) => {
89
73
  const shown = entries.find(
90
74
  ({ event }) =>
@@ -117,7 +101,6 @@ export const openItems = (
117
101
  )
118
102
 
119
103
  const sections = [
120
- decisions.length > 0 ? `Decisions waiting for the user:\n${decisions.join("\n")}` : "",
121
104
  blocks.length > 0 ? `Blocks the user hasn't answered:\n${blocks.join("\n")}` : "",
122
105
  specialists.length > 0 ? `Specialists still working:\n${specialists.join("\n")}` : ""
123
106
  ].filter((section) => section.length > 0)
@@ -94,8 +94,8 @@ const requiring =
94
94
  * the tools' handlers. A catalog tool's services are `execute_tool`'s, so the turn provides them
95
95
  * as it does for any tool.
96
96
  *
97
- * `execute_tool` doesn't run a tool that needs the user's decision; keep such tools in the loop's
98
- * toolkit directly.
97
+ * A catalog tool with Effect's `needsApproval` set fails the layer when it is built, as it would
98
+ * fail a loop: the library doesn't run calls that wait for approval.
99
99
  *
100
100
  * **Example** (Finding rarely used tools when needed)
101
101
  *
@@ -146,6 +146,9 @@ export const layer = <T extends Record<string, Tool.Any>>(catalog: Toolkit.Toolk
146
146
  Effect.gen(function* () {
147
147
  const resolved = yield* catalog
148
148
  const definitions = Object.values(resolved.tools)
149
+ const needingApproval = Toolkits.needingApproval(definitions)
150
+
151
+ if (needingApproval.length > 0) return yield* Effect.die(Toolkits.needsApprovalMessage(needingApproval))
149
152
 
150
153
  return {
151
154
  search_tools: ({ query }) =>
@@ -167,17 +170,6 @@ export const layer = <T extends Record<string, Tool.Any>>(catalog: Toolkit.Toolk
167
170
  providerExecuted: false
168
171
  })
169
172
 
170
- // A call that needs the user's decision parks only as a call the model made itself,
171
- // where it can ask for it.
172
- if (yield* Toolkits.needsDecision(resolved, call, [])) {
173
- return yield* Effect.fail(
174
- Toolkits.failed(
175
- `${name} needs the user's decision, so it can't run through execute_tool.`,
176
- "NeedsDecision"
177
- ).result
178
- )
179
- }
180
-
181
173
  const { outcome, result } = yield* Toolkits.runNative(resolved, call)
182
174
 
183
175
  return outcome === "failure" ? yield* Effect.fail(result) : result
@@ -15,7 +15,7 @@ import * as Schema from "effect/Schema"
15
15
  *
16
16
  * It holds a message that says what went wrong and, optionally, a `reason` the model can tell
17
17
  * apart, such as `"NotFound"` or `"OverBudget"`. The library's own reasons are PascalCase, like
18
- * `"NeedsDecision"`.
18
+ * `"ToolNotFoundError"`.
19
19
  *
20
20
  * @category errors
21
21
  * @since 0.1.0
package/src/index.ts CHANGED
@@ -79,16 +79,6 @@ export * as Compaction from "./Compaction.ts"
79
79
  */
80
80
  export * as CurrentChat from "./CurrentChat.ts"
81
81
 
82
- /**
83
- * @since 0.1.0
84
- */
85
- export * as Decision from "./Decision.ts"
86
-
87
- /**
88
- * @since 0.1.0
89
- */
90
- export * as DecisionId from "./DecisionId.ts"
91
-
92
82
  /**
93
83
  * @since 0.1.0
94
84
  */
@@ -5,7 +5,6 @@ import * as Effect from "effect/Effect"
5
5
  import * as Exit from "effect/Exit"
6
6
  import * as Fiber from "effect/Fiber"
7
7
  import * as FiberSet from "effect/FiberSet"
8
- import * as Option from "effect/Option"
9
8
  import * as Predicate from "effect/Predicate"
10
9
  import * as PubSub from "effect/PubSub"
11
10
  import * as Ref from "effect/Ref"
@@ -17,10 +16,8 @@ import * as AssistantEvent from "../AssistantEvent.ts"
17
16
  import type { ChatId } from "../ChatId.ts"
18
17
  import { ChatLog } from "../ChatLog.ts"
19
18
  import { CurrentChat } from "../CurrentChat.ts"
20
- import type { InputId } from "../InputId.ts"
21
19
  import * as Chats from "../Chats.ts"
22
20
  import * as SpecialistService from "../Specialist.ts"
23
- import * as Decisions from "./decisions.ts"
24
21
  import * as AppEvents from "./events.ts"
25
22
  import * as History from "./history.ts"
26
23
  import * as Specialists from "./specialists.ts"
@@ -113,8 +110,6 @@ export const make = Effect.fnUntraced(function* (options: {
113
110
 
114
111
  const channel = yield* makeChannelPubSub
115
112
  const writes = yield* Semaphore.make(1)
116
- // Inputs are stored one at a time with the decisions they make: an answer checks the decisions the last one made.
117
- const storing = yield* Semaphore.make(1)
118
113
  const turnsLock = yield* Semaphore.make(1)
119
114
  const holding = yield* makeHolding(options.keepAlive)
120
115
  const deleted = yield* Deferred.make<undefined>()
@@ -179,13 +174,6 @@ export const make = Effect.fnUntraced(function* (options: {
179
174
  append: (...events) => Effect.asVoid(Effect.flatMap(Effect.forEach(events, toStored), chat.append))
180
175
  }
181
176
 
182
- // An approved call runs at the turn's next step.
183
- const resolve = Effect.fnUntraced(function* (decided: ReadonlyArray<Decisions.Decided>, answer: InputId) {
184
- yield* chat.append(
185
- decided.map(({ request, outcome }) => AssistantEvent.DecisionResolved.make({ id: request.id, outcome, answer }))
186
- )
187
- })
188
-
189
177
  // A chat the app deleted is deleted here too, which stops what works in it: a turn that checks stops there.
190
178
  const checkAvailable: Effect.Effect<boolean> = chats.get(chatId).pipe(
191
179
  Effect.as(true),
@@ -199,51 +187,24 @@ export const make = Effect.fnUntraced(function* (options: {
199
187
  )
200
188
  )
201
189
 
202
- // Under `storing`: an answer's decisions are resolved before the next input is stored.
203
190
  const store = Effect.fnUntraced(function* (input: AssistantEvent.Input | AssistantEvent.StoredAppEvent) {
204
191
  if (yield* isDeleted) return yield* refused(input, "is deleted")
205
192
 
206
193
  if (!(yield* checkAvailable)) return yield* refused(input, "is unavailable")
207
194
 
208
195
  // Checked under the write lock: a retry that arrives while the first submit is storing is stored once.
209
- const verdict = yield* Semaphore.withPermit(writes)(
196
+ yield* Semaphore.withPermit(writes)(
210
197
  Effect.gen(function* () {
211
- if (yield* isDeleted) {
212
- yield* refused(input, "is deleted")
198
+ if (yield* isDeleted) return yield* refused(input, "is deleted")
213
199
 
214
- return Option.none()
215
- }
216
-
217
- const stored = yield* Ref.get(entries)
218
200
  const id = idOf(input)
219
201
 
220
- if (id !== undefined && stored.some(({ event }) => idOf(event) === id)) return Option.none()
221
-
222
- const verdict = Decisions.verdict(stored, input)
223
-
224
- if (Predicate.isTagged(verdict, "Refuse")) {
225
- yield* Effect.logWarning(`assistant chat refused ${input._tag}: ${verdict.reason}`)
226
-
227
- return Option.none()
228
- }
202
+ if (id !== undefined && (yield* Ref.get(entries)).some(({ event }) => idOf(event) === id)) return
229
203
 
230
204
  yield* write([input])
231
-
232
- return Option.some(verdict)
233
205
  })
234
206
  )
235
-
236
- if (Option.isSome(verdict) && Predicate.isTagged(verdict.value, "Decide")) {
237
- yield* resolve(verdict.value.decided, verdict.value.answer)
238
- }
239
- }, Semaphore.withPermit(storing))
240
-
241
- // A restart between storing an answer and resolving it: resolve it now, before anything reads the chat.
242
- yield* Effect.forEach(
243
- Decisions.unresolved(yield* Ref.get(entries)),
244
- (decided) => resolve([decided], decided.answer),
245
- { discard: true }
246
- )
207
+ })
247
208
 
248
209
  // The handlers' builds run in the chat's working scope: what they set up lives until the chat closes or is deleted.
249
210
  const handler = yield* inWorking(Effect.provideService(registration.open(handle), CurrentChat, handle))
@@ -293,16 +254,6 @@ export const make = Effect.fnUntraced(function* (options: {
293
254
 
294
255
  const services = Context.make(CurrentChat, handle).pipe(
295
256
  Context.add(SpecialistService.Runner, runner),
296
- Context.add(
297
- Decisions.TurnDecisions,
298
- Option.some({
299
- approved: Effect.gen(function* () {
300
- const all = yield* chat.entries
301
-
302
- return Decisions.approvedCalls(all)
303
- })
304
- })
305
- ),
306
257
  Context.merge(registration.model)
307
258
  )
308
259
 
@@ -5,7 +5,6 @@ import * as Option from "effect/Option"
5
5
  import * as Predicate from "effect/Predicate"
6
6
  import * as Schema from "effect/Schema"
7
7
  import * as AssistantEvent from "../AssistantEvent.ts"
8
- import * as Decision from "../Decision.ts"
9
8
  import { toJson, toJsonText } from "./schema.ts"
10
9
  import * as Uuid from "./uuid.ts"
11
10
 
@@ -108,61 +107,9 @@ const finishedText = (
108
107
  const answerText = (answer: AssistantEvent.BlockAnswer) =>
109
108
  `<answer message="${answer.messageId}" kind="${answer.kind}">\nThe user answered with the block on your message ${answer.messageId}.\n${toJsonText(answer.value)}\n</answer>`
110
109
 
111
- /** @internal */
112
- export const parkedResult = (
113
- request: AssistantEvent.DecisionRequested,
114
- origin: AssistantEvent.CallOrigin
115
- ): Prompt.ToolMessage =>
116
- Prompt.toolMessage({
117
- content: [
118
- Prompt.toolResultPart({
119
- id: origin.callId,
120
- name: origin.tool,
121
- isFailure: false,
122
- result: {
123
- status: "waiting_for_decision",
124
- decisionId: request.id,
125
- next: "Not run: it runs only if the user approves it. Send them a message with a decision block for this decisionId that says what they approve (the call's key facts). Their answer runs it, or declines it."
126
- },
127
- providerExecuted: false
128
- })
129
- ]
130
- })
131
-
132
- const whatDecided = (request: AssistantEvent.DecisionRequested, outcome: AssistantEvent.DecisionOutcome) =>
133
- outcome === "approved"
134
- ? `approved: ${request.origin.tool} runs now, and its result follows.`
135
- : `declined: ${request.origin.tool} didn't run, and won't.`
136
-
137
- const decodeDecisionAnswer = Schema.decodeUnknownOption(Decision.Answer)
138
-
139
- const decisionAnswerText = (
140
- answer: AssistantEvent.BlockAnswer,
141
- requests: ReadonlyMap<string, AssistantEvent.DecisionRequested>
142
- ) =>
143
- Option.match(decodeDecisionAnswer(answer.value), {
144
- onNone: () => answerText(answer),
145
- onSome: ({ decisions }) => {
146
- const lines = Object.entries(decisions).map(([id, outcome]) => {
147
- const request = requests.get(id)
148
-
149
- return `- ${id}: ${request === undefined ? outcome : whatDecided(request, outcome)}`
150
- })
151
-
152
- return `<answer message="${answer.messageId}" kind="${answer.kind}">\nThe user decided with the block on your message ${answer.messageId}:\n${lines.join("\n")}\n</answer>`
153
- }
154
- })
155
-
156
110
  const resultText = (result: AssistantEvent.ToolResult) =>
157
111
  `<result tool="${result.tool}" call="${result.callId}" outcome="${result.outcome}">\nThe result of a call from earlier in this chat.\n${toJsonText(result.result)}\n</result>`
158
112
 
159
- const approvedResultText = (
160
- request: AssistantEvent.DecisionRequested,
161
- tool: string,
162
- result: AssistantEvent.ToolResult
163
- ) =>
164
- `<decision id="${request.id}" tool="${tool}" outcome="approved">\nThe user approved it, and ${tool} ran: ${result.outcome}.\n${toJsonText(result.result)}\n</decision>`
165
-
166
113
  // In the order the model saw the entries, not the order they were stored.
167
114
  /** @internal */
168
115
  export const fromEntries = (
@@ -172,9 +119,6 @@ export const fromEntries = (
172
119
  const history: Array<Prompt.Message> = []
173
120
  const held: Array<Prompt.Message> = []
174
121
  const open = new Set<string>()
175
- const requests = new Map<string, AssistantEvent.DecisionRequested>()
176
- // Parked calls by call id: their result came with the parking, so a later one reads as the decision's.
177
- const parked = new Map<string, AssistantEvent.DecisionRequested>()
178
122
  const called = new Set<string>()
179
123
 
180
124
  const flushIfClosed = () => {
@@ -209,11 +153,7 @@ export const fromEntries = (
209
153
  Match.tagsExhaustive({
210
154
  UserMessage: (message) =>
211
155
  input(index, userText(AssistantEvent.textWithAttachments(message.text, message.attachments))),
212
- BlockAnswer: (answer) =>
213
- input(
214
- index,
215
- userText(answer.kind === Decision.kind ? decisionAnswerText(answer, requests) : answerText(answer))
216
- ),
156
+ BlockAnswer: (answer) => input(index, userText(answerText(answer))),
217
157
  ModelOutput: (output) => {
218
158
  history.push(...Prompt.fromResponseParts(output.parts).content)
219
159
 
@@ -227,26 +167,14 @@ export const fromEntries = (
227
167
  flushIfClosed()
228
168
  },
229
169
  ToolResult: (result) => {
230
- const request = parked.get(result.callId)
231
-
232
- if (request !== undefined) return input(index, userText(approvedResultText(request, result.tool, result)))
233
-
234
170
  // A view that starts after the call (since a summary, say) keeps its result as text: a tool result needs its
235
- // call right before it. An approved call's result can come long after the call was parked.
171
+ // call right before it.
236
172
  if (!called.has(result.callId)) return input(index, userText(resultText(result)))
237
173
 
238
174
  history.push(toolMessage(result))
239
175
  open.delete(result.callId)
240
176
  flushIfClosed()
241
177
  },
242
- DecisionRequested: (request) => {
243
- requests.set(request.id, request)
244
- parked.set(request.origin.callId, request)
245
- history.push(parkedResult(request, request.origin))
246
- open.delete(request.origin.callId)
247
- flushIfClosed()
248
- },
249
- DecisionResolved: () => undefined,
250
178
  AssistantMessage: () => undefined,
251
179
  ToolStarted: () => undefined,
252
180
  TurnStarted: () => undefined,