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/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
|
|
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)
|
|
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
|
|
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
|
|
224
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
|
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:
|
|
59
|
-
*
|
|
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)
|
package/src/ToolDiscovery.ts
CHANGED
|
@@ -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
|
-
*
|
|
98
|
-
*
|
|
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
|
package/src/ToolFailure.ts
CHANGED
|
@@ -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
|
-
* `"
|
|
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
|
*/
|
package/src/internal/chat.ts
CHANGED
|
@@ -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
|
-
|
|
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 &&
|
|
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
|
|
package/src/internal/history.ts
CHANGED
|
@@ -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.
|
|
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,
|