effect-assistant 0.2.0 → 0.4.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/dist/Assistant.d.ts +77 -37
- package/dist/Assistant.d.ts.map +1 -1
- package/dist/Assistant.js +11 -4
- package/dist/Assistant.js.map +1 -1
- package/dist/AssistantActivity.d.ts +10 -16
- package/dist/AssistantActivity.d.ts.map +1 -1
- package/dist/AssistantActivity.js +11 -4
- package/dist/AssistantActivity.js.map +1 -1
- package/dist/AssistantEngine.d.ts +39 -1
- package/dist/AssistantEngine.d.ts.map +1 -1
- package/dist/AssistantEngine.js +81 -28
- package/dist/AssistantEngine.js.map +1 -1
- package/dist/AssistantEvent.d.ts +104 -11
- package/dist/AssistantEvent.d.ts.map +1 -1
- package/dist/AssistantEvent.js +88 -13
- package/dist/AssistantEvent.js.map +1 -1
- package/dist/AssistantLoop.d.ts +4 -2
- package/dist/AssistantLoop.d.ts.map +1 -1
- package/dist/AssistantLoop.js +4 -2
- package/dist/AssistantLoop.js.map +1 -1
- package/dist/BlockKit.d.ts +4 -4
- package/dist/BlockKit.js +1 -1
- package/dist/BlockKit.js.map +1 -1
- package/dist/ChatLog.js +1 -1
- package/dist/Compaction.d.ts +7 -3
- package/dist/Compaction.d.ts.map +1 -1
- package/dist/Compaction.js +19 -8
- package/dist/Compaction.js.map +1 -1
- package/dist/CurrentChat.d.ts +2 -1
- package/dist/CurrentChat.d.ts.map +1 -1
- package/dist/CurrentChat.js.map +1 -1
- package/dist/Instructions.js +1 -1
- package/dist/Skill.d.ts +86 -11
- package/dist/Skill.d.ts.map +1 -1
- package/dist/Skill.js +93 -18
- package/dist/Skill.js.map +1 -1
- package/dist/Specialist.d.ts +7 -7
- package/dist/Specialist.d.ts.map +1 -1
- package/dist/Specialist.js.map +1 -1
- package/dist/Timeline.d.ts +516 -0
- package/dist/Timeline.d.ts.map +1 -0
- package/dist/Timeline.js +631 -0
- package/dist/Timeline.js.map +1 -0
- package/dist/ToolApproval.d.ts +146 -0
- package/dist/ToolApproval.d.ts.map +1 -0
- package/dist/ToolApproval.js +201 -0
- package/dist/ToolApproval.js.map +1 -0
- package/dist/ToolDiscovery.d.ts +6 -4
- package/dist/ToolDiscovery.d.ts.map +1 -1
- package/dist/ToolDiscovery.js +5 -4
- package/dist/ToolDiscovery.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +4 -4
- package/dist/internal/chat.js +9 -7
- package/dist/internal/chat.js.map +1 -1
- package/dist/internal/events.d.ts +15 -16
- package/dist/internal/events.d.ts.map +1 -1
- package/dist/internal/events.js +29 -28
- package/dist/internal/events.js.map +1 -1
- package/dist/internal/history.js +53 -22
- package/dist/internal/history.js.map +1 -1
- package/dist/internal/input.js +7 -0
- package/dist/internal/input.js.map +1 -0
- package/dist/internal/loop.d.ts +10 -5
- package/dist/internal/loop.d.ts.map +1 -1
- package/dist/internal/loop.js +55 -22
- package/dist/internal/loop.js.map +1 -1
- package/dist/internal/specialists.js +10 -16
- package/dist/internal/specialists.js.map +1 -1
- package/dist/internal/toolkit.d.ts +5 -7
- package/dist/internal/toolkit.d.ts.map +1 -1
- package/dist/internal/toolkit.js +28 -18
- package/dist/internal/toolkit.js.map +1 -1
- package/dist/internal/tools.js +1 -1
- package/dist/internal/turns.d.ts.map +1 -1
- package/dist/internal/turns.js +35 -42
- package/dist/internal/turns.js.map +1 -1
- package/package.json +1 -1
- package/src/Assistant.ts +80 -40
- package/src/AssistantActivity.ts +11 -18
- package/src/AssistantEngine.ts +110 -43
- package/src/AssistantEvent.ts +106 -14
- package/src/AssistantLoop.ts +4 -2
- package/src/BlockKit.ts +6 -6
- package/src/Compaction.ts +25 -4
- package/src/CurrentChat.ts +2 -1
- package/src/Skill.ts +133 -23
- package/src/Specialist.ts +9 -9
- package/src/Timeline.ts +947 -0
- package/src/ToolApproval.ts +249 -0
- package/src/ToolDiscovery.ts +8 -3
- package/src/index.ts +10 -10
- package/src/internal/chat.ts +15 -5
- package/src/internal/events.ts +43 -43
- package/src/internal/history.ts +94 -32
- package/src/internal/input.ts +3 -0
- package/src/internal/loop.ts +120 -34
- package/src/internal/specialists.ts +7 -12
- package/src/internal/toolkit.ts +47 -30
- package/src/internal/turns.ts +52 -58
- package/dist/AppEvent.d.ts +0 -111
- package/dist/AppEvent.d.ts.map +0 -1
- package/dist/AppEvent.js +0 -71
- package/dist/AppEvent.js.map +0 -1
- package/dist/ChatView.d.ts +0 -408
- package/dist/ChatView.d.ts.map +0 -1
- package/dist/ChatView.js +0 -443
- package/dist/ChatView.js.map +0 -1
- package/dist/internal/skills.js +0 -28
- package/dist/internal/skills.js.map +0 -1
- package/src/AppEvent.ts +0 -134
- package/src/ChatView.ts +0 -587
- package/src/internal/skills.ts +0 -37
package/dist/index.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { AppEvent_exports } from "./AppEvent.js";
|
|
2
1
|
import { ChatId_exports } from "./ChatId.js";
|
|
3
2
|
import { InputId_exports } from "./InputId.js";
|
|
4
3
|
import { RunId_exports } from "./RunId.js";
|
|
@@ -7,9 +6,9 @@ import { ChatLog_exports } from "./ChatLog.js";
|
|
|
7
6
|
import { CurrentChat_exports } from "./CurrentChat.js";
|
|
8
7
|
import { Chats_exports } from "./Chats.js";
|
|
9
8
|
import { Specialist_exports } from "./Specialist.js";
|
|
9
|
+
import { ToolApproval_exports } from "./ToolApproval.js";
|
|
10
10
|
import { AssistantToolCall_exports } from "./AssistantToolCall.js";
|
|
11
11
|
import { ToolFailure_exports } from "./ToolFailure.js";
|
|
12
|
-
import { Skill_exports } from "./Skill.js";
|
|
13
12
|
import { Instructions_exports } from "./Instructions.js";
|
|
14
13
|
import { AssistantEngine_exports } from "./AssistantEngine.js";
|
|
15
14
|
import { Assistant_exports } from "./Assistant.js";
|
|
@@ -17,8 +16,9 @@ import { AssistantActivity_exports } from "./AssistantActivity.js";
|
|
|
17
16
|
import { AssistantLoop_exports } from "./AssistantLoop.js";
|
|
18
17
|
import { Block_exports } from "./Block.js";
|
|
19
18
|
import { BlockKit_exports } from "./BlockKit.js";
|
|
20
|
-
import { ChatView_exports } from "./ChatView.js";
|
|
21
19
|
import { Compaction_exports } from "./Compaction.js";
|
|
22
20
|
import { Memory_exports } from "./Memory.js";
|
|
21
|
+
import { Skill_exports } from "./Skill.js";
|
|
22
|
+
import { Timeline_exports } from "./Timeline.js";
|
|
23
23
|
import { ToolDiscovery_exports } from "./ToolDiscovery.js";
|
|
24
|
-
export {
|
|
24
|
+
export { Assistant_exports as Assistant, AssistantActivity_exports as AssistantActivity, AssistantEngine_exports as AssistantEngine, AssistantEvent_exports as AssistantEvent, AssistantLoop_exports as AssistantLoop, AssistantToolCall_exports as AssistantToolCall, Block_exports as Block, BlockKit_exports as BlockKit, ChatId_exports as ChatId, ChatLog_exports as ChatLog, Chats_exports as Chats, Compaction_exports as Compaction, CurrentChat_exports as CurrentChat, InputId_exports as InputId, Instructions_exports as Instructions, Memory_exports as Memory, RunId_exports as RunId, Skill_exports as Skill, Specialist_exports as Specialist, Timeline_exports as Timeline, ToolApproval_exports as ToolApproval, ToolDiscovery_exports as ToolDiscovery, ToolFailure_exports as ToolFailure };
|
package/dist/internal/chat.js
CHANGED
|
@@ -3,13 +3,14 @@ import { ChatLog } from "../ChatLog.js";
|
|
|
3
3
|
import { CurrentChat } from "../CurrentChat.js";
|
|
4
4
|
import { Chats } from "../Chats.js";
|
|
5
5
|
import { Runner } from "../Specialist.js";
|
|
6
|
-
import {
|
|
6
|
+
import { fromEntries } from "../ToolApproval.js";
|
|
7
|
+
import { decodeEntries, encode, isRegistered, markInput } from "./events.js";
|
|
7
8
|
import { needsTurn } from "./history.js";
|
|
8
9
|
import { makeRunner } from "./specialists.js";
|
|
9
10
|
import { run } from "./turns.js";
|
|
10
|
-
import * as Predicate from "effect/Predicate";
|
|
11
11
|
import * as Context from "effect/Context";
|
|
12
12
|
import * as Effect from "effect/Effect";
|
|
13
|
+
import * as Predicate from "effect/Predicate";
|
|
13
14
|
import * as Stream from "effect/Stream";
|
|
14
15
|
import * as Exit from "effect/Exit";
|
|
15
16
|
import * as Scope from "effect/Scope";
|
|
@@ -51,7 +52,6 @@ const refused = (input, why) => Effect.logDebug(`assistant chat ${why}: ${input.
|
|
|
51
52
|
const make = Effect.fnUntraced(function* (options) {
|
|
52
53
|
const { registration, chatId } = options;
|
|
53
54
|
const { app } = registration.definition;
|
|
54
|
-
const { wakes } = app;
|
|
55
55
|
const storage = yield* ChatLog;
|
|
56
56
|
const chats = yield* Chats;
|
|
57
57
|
const initial = yield* storage.read(chatId);
|
|
@@ -97,8 +97,10 @@ const make = Effect.fnUntraced(function* (options) {
|
|
|
97
97
|
yield* Semaphore.withPermit(writes)(Effect.gen(function* () {
|
|
98
98
|
if (yield* isDeleted) return yield* refused(input, "is deleted");
|
|
99
99
|
const id = idOf(input);
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
const all = yield* Ref.get(entries);
|
|
101
|
+
if (id !== void 0 && all.some(({ event }) => idOf(event) === id)) return;
|
|
102
|
+
if (Predicate.isTagged(input, "ApprovalDecision") && fromEntries(all).get(input.callId)?.status !== "pending") return yield* refused(input, "has no call waiting for that decision");
|
|
103
|
+
yield* write([isLibraryEvent(input) ? input : markInput(input)]);
|
|
102
104
|
}));
|
|
103
105
|
});
|
|
104
106
|
const handler = yield* inWorking(Effect.provideService(registration.open(handle), CurrentChat, handle));
|
|
@@ -115,7 +117,7 @@ const make = Effect.fnUntraced(function* (options) {
|
|
|
115
117
|
if (Exit.isFailure(exit) && !Cause.hasInterruptsOnly(exit.cause)) return yield* Effect.failCause(exit.cause);
|
|
116
118
|
}, Effect.orDie);
|
|
117
119
|
const drain = Semaphore.withPermit(turnsLock)(Effect.gen(function* () {
|
|
118
|
-
while (!(yield* isDeleted) && needsTurn(yield* chat.entries
|
|
120
|
+
while (!(yield* isDeleted) && needsTurn(yield* chat.entries)) yield* run({
|
|
119
121
|
registration,
|
|
120
122
|
handler,
|
|
121
123
|
chat,
|
|
@@ -158,7 +160,7 @@ const make = Effect.fnUntraced(function* (options) {
|
|
|
158
160
|
yield* inBackground(drain);
|
|
159
161
|
}),
|
|
160
162
|
resume: Effect.gen(function* () {
|
|
161
|
-
if (needsTurn(yield* chat.entries
|
|
163
|
+
if (needsTurn(yield* chat.entries)) yield* inBackground(drain);
|
|
162
164
|
}),
|
|
163
165
|
stream: (after) => Stream.unwrap(Effect.gen(function* () {
|
|
164
166
|
const subscription = yield* PubSub.subscribe(channel);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chat.js","names":["AssistantEvent.isLibraryEvent","AssistantEvent.isInput","Chats.Chats","AppEvents.isRegistered","AppEvents.encode","AppEvents.decodeEntries","History.needsTurn","Turns.run","Specialists.makeRunner","SpecialistService.Runner"],"sources":["../../src/internal/chat.ts"],"sourcesContent":["import * as Cause from \"effect/Cause\"\nimport * as Context from \"effect/Context\"\nimport * as Deferred from \"effect/Deferred\"\nimport * as Effect from \"effect/Effect\"\nimport * as Exit from \"effect/Exit\"\nimport * as Fiber from \"effect/Fiber\"\nimport * as FiberSet from \"effect/FiberSet\"\nimport * as Predicate from \"effect/Predicate\"\nimport * as PubSub from \"effect/PubSub\"\nimport * as Ref from \"effect/Ref\"\nimport * as Scope from \"effect/Scope\"\nimport * as Semaphore from \"effect/Semaphore\"\nimport * as Stream from \"effect/Stream\"\nimport * as SynchronizedRef from \"effect/SynchronizedRef\"\nimport * as AssistantEvent from \"../AssistantEvent.ts\"\nimport type { ChatId } from \"../ChatId.ts\"\nimport { ChatLog } from \"../ChatLog.ts\"\nimport { CurrentChat } from \"../CurrentChat.ts\"\nimport * as Chats from \"../Chats.ts\"\nimport * as SpecialistService from \"../Specialist.ts\"\nimport * as AppEvents from \"./events.ts\"\nimport * as History from \"./history.ts\"\nimport * as Specialists from \"./specialists.ts\"\nimport * as Turns from \"./turns.ts\"\nimport type { Writer } from \"./writer.ts\"\n\nconst idOf = (event: { readonly _tag: string }): string | undefined =>\n AssistantEvent.isLibraryEvent(event)\n ? AssistantEvent.isInput(event)\n ? event.id\n : undefined\n : Predicate.hasProperty(event, \"id\") && Predicate.isString(event.id)\n ? event.id\n : undefined\n\nconst makeChannelPubSub = Effect.acquireRelease(PubSub.unbounded<AssistantEvent.Entry>(), PubSub.shutdown)\n\nconst makeHolding = Effect.fnUntraced(function* (keepAlive: (enabled: boolean) => Effect.Effect<void>) {\n const holds = yield* SynchronizedRef.make(0)\n\n const acquire = SynchronizedRef.updateEffect(\n holds,\n Effect.fnUntraced(function* (n: number) {\n if (n !== 0) return n + 1\n\n yield* keepAlive(true)\n\n return 1\n })\n )\n\n const release = SynchronizedRef.updateEffect(\n holds,\n Effect.fnUntraced(function* (n: number) {\n if (n !== 1) return n - 1\n\n yield* keepAlive(false)\n\n return 0\n })\n )\n\n return (effect: Effect.Effect<void>) =>\n Effect.acquireUseRelease(\n acquire,\n () => effect,\n // An interrupted effect (a shutdown) keeps its hold, so the chat starts again and picks up where it was cut off.\n (_, exit) => (Exit.hasInterrupts(exit) ? Effect.void : release)\n )\n})\n\n/** @internal */\nexport interface Chat {\n readonly submit: (input: AssistantEvent.Input | AssistantEvent.StoredAppEvent) => Effect.Effect<void>\n readonly submitInBackground: (input: AssistantEvent.Input | AssistantEvent.StoredAppEvent) => Effect.Effect<void>\n // For inputs no turn has read, like a report stored during a shutdown.\n readonly resume: Effect.Effect<void>\n readonly stream: (after: number) => Stream.Stream<AssistantEvent.Entry>\n // Stops what works in the chat and ends its subscribers' streams; its log stays.\n readonly delete: Effect.Effect<void>\n}\n\nconst isChatDeleted = (entry: AssistantEvent.Entry) => Predicate.isTagged(entry.event, \"ChatDeleted\")\n\nconst deletedChat = (entries: ReadonlyArray<AssistantEvent.Entry>): Chat => ({\n submit: () => Effect.void,\n submitInBackground: () => Effect.void,\n resume: Effect.void,\n stream: (after) => Stream.fromIterable(entries.filter((entry) => entry.seq > after)),\n delete: Effect.void\n})\n\nconst refused = (input: { readonly _tag: string }, why: string) =>\n Effect.logDebug(`assistant chat ${why}: ${input._tag} not stored`)\n\n/** @internal */\nexport const make = Effect.fnUntraced(function* (options: {\n readonly registration: Turns.Registration\n readonly chatId: ChatId\n readonly keepAlive: (enabled: boolean) => Effect.Effect<void>\n}) {\n const { registration, chatId } = options\n const { app } = registration.definition\n const { wakes } = app\n const storage = yield* ChatLog\n const chats = yield* Chats.Chats\n const initial = yield* storage.read(chatId)\n\n if (initial.some(isChatDeleted)) return deletedChat(initial)\n\n const channel = yield* makeChannelPubSub\n const writes = yield* Semaphore.make(1)\n const turnsLock = yield* Semaphore.make(1)\n const holding = yield* makeHolding(options.keepAlive)\n const deleted = yield* Deferred.make<undefined>()\n // So deleting the chat stops all its work: its turns, runs, and the builds' resources.\n const working = yield* Scope.fork(yield* Effect.scope)\n const inWorking = Effect.provideService(Scope.Scope, working)\n const background = yield* inWorking(FiberSet.make())\n const requests = yield* inWorking(FiberSet.make())\n\n const isDeleted = Deferred.isDone(deleted)\n\n // The chat is its log's only writer, so it reads the log once and turns read memory.\n const entries = yield* Ref.make(initial)\n\n // Every writer goes through here, one at a time, so each append is numbered after the last. After a delete it\n // stores nothing.\n const write = Effect.fnUntraced(function* (\n events: ReadonlyArray<AssistantEvent.AssistantEvent | AssistantEvent.StoredAppEvent>\n ) {\n const unknown = events.find((event) => !AssistantEvent.isLibraryEvent(event) && !AppEvents.isRegistered(app, event))\n\n if (unknown !== undefined) {\n return yield* Effect.die(`${unknown._tag} isn't one of the ${registration.definition.name} assistant's events.`)\n }\n\n if (yield* isDeleted) return []\n\n return yield* stored(events)\n })\n\n // Uninterruptible: a stored append must reach memory and the subscribers too, or a turn misses it and replies twice.\n const stored = Effect.fnUntraced(function* (\n events: ReadonlyArray<AssistantEvent.AssistantEvent | AssistantEvent.StoredAppEvent>\n ) {\n const stored = yield* storage.append(chatId, events)\n\n yield* Ref.update(entries, (all) => [...all, ...stored])\n yield* PubSub.publishAll(channel, stored)\n\n return stored\n }, Effect.uninterruptible)\n\n const chat: Writer = {\n chatId,\n entries: Ref.get(entries),\n append: (events) => Semaphore.withPermit(writes)(write(events))\n }\n\n const toStored = (event: {\n readonly _tag: string\n }): Effect.Effect<AssistantEvent.AssistantEvent | AssistantEvent.StoredAppEvent> =>\n AssistantEvent.isLibraryEvent(event)\n ? Effect.succeed(event)\n : AppEvents.isRegistered(app, event)\n ? AppEvents.encode(app, event)\n : Effect.die(`${event._tag} isn't one of the ${registration.definition.name} assistant's events.`)\n\n // What handlers, tools, and runs see: the assistant's own events decoded, and encoded when appended.\n const handle: Turns.TypedChat = {\n chatId,\n entries: Effect.map(chat.entries, (all) => AppEvents.decodeEntries(app, all)),\n append: (...events) => Effect.asVoid(Effect.flatMap(Effect.forEach(events, toStored), chat.append))\n }\n\n // A chat the app deleted is deleted here too, which stops what works in it: a turn that checks stops there.\n const checkAvailable: Effect.Effect<boolean> = chats.get(chatId).pipe(\n Effect.as(true),\n Effect.catchTag(\"GetChatError\", (unavailable) =>\n unavailable.reason === \"Deleted\"\n ? Effect.as(\n Effect.suspend(() => remove),\n false\n )\n : Effect.succeed(false)\n )\n )\n\n const store = Effect.fnUntraced(function* (input: AssistantEvent.Input | AssistantEvent.StoredAppEvent) {\n if (yield* isDeleted) return yield* refused(input, \"is deleted\")\n\n if (!(yield* checkAvailable)) return yield* refused(input, \"is unavailable\")\n\n // Checked under the write lock: a retry that arrives while the first submit is storing is stored once.\n yield* Semaphore.withPermit(writes)(\n Effect.gen(function* () {\n if (yield* isDeleted) return yield* refused(input, \"is deleted\")\n\n const id = idOf(input)\n\n if (id !== undefined && (yield* Ref.get(entries)).some(({ event }) => idOf(event) === id)) return\n\n yield* write([input])\n })\n )\n })\n\n // The handlers' builds run in the chat's working scope: what they set up lives until the chat closes or is deleted.\n const handler = yield* inWorking(Effect.provideService(registration.open(handle), CurrentChat, handle))\n\n const specialists = yield* Effect.forEach(\n registration.specialists,\n Effect.fnUntraced(function* ({ specialist, built }) {\n const handler = yield* inWorking(Effect.provideService(built.open, CurrentChat, handle))\n\n return { specialist, handler }\n })\n )\n\n const inBackground = (effect: Effect.Effect<void>) => FiberSet.run(background, holding(effect)).pipe(Effect.asVoid)\n\n // A request's turns run in the working scope too, so a delete stops them; the request ends quietly then.\n const inRequest = Effect.fnUntraced(function* (effect: Effect.Effect<void>) {\n const fiber = yield* FiberSet.run(requests, effect)\n const exit = yield* Fiber.await(fiber).pipe(Effect.onInterrupt(() => Fiber.interrupt(fiber)))\n\n if (Exit.isFailure(exit) && !Cause.hasInterruptsOnly(exit.cause)) {\n return yield* Effect.failCause(exit.cause)\n }\n }, Effect.orDie)\n\n const drain: Effect.Effect<void> = Semaphore.withPermit(turnsLock)(\n Effect.gen(function* () {\n while (!(yield* isDeleted) && History.needsTurn(yield* chat.entries, wakes)) {\n yield* Turns.run({ registration, handler, chat, services, checkOpen: Effect.asVoid(checkAvailable) })\n }\n })\n )\n\n const runner = yield* inWorking(\n Specialists.makeRunner({\n chat,\n handle,\n specialists,\n model: registration.model,\n holding,\n report: Effect.fnUntraced(function* (finished) {\n yield* store(finished)\n yield* inBackground(drain)\n })\n })\n )\n\n const services = Context.make(CurrentChat, handle).pipe(\n Context.add(SpecialistService.Runner, runner),\n Context.merge(registration.model)\n )\n\n const remove = Effect.gen(function* () {\n // Under the write lock: an append in flight is stored before the `ChatDeleted`; none comes after.\n const first = yield* Semaphore.withPermit(writes)(\n Effect.gen(function* () {\n if (yield* isDeleted) return false\n\n yield* stored([AssistantEvent.ChatDeleted.make({})])\n\n return yield* Deferred.succeed(deleted, undefined)\n })\n ).pipe(Effect.uninterruptible)\n\n if (!first) return\n\n // Its own fiber: a tool that deletes its own chat is in the working scope, and closing it interrupts that tool.\n // Interrupted work keeps its hold (see `makeHolding`), so the chat lets go of it here.\n const closing = yield* Effect.forkDetach(\n Effect.gen(function* () {\n yield* Scope.close(working, Exit.void)\n yield* options.keepAlive(false)\n })\n )\n\n yield* Fiber.await(closing)\n })\n\n // A chat the app deleted while it was closed is deleted now, so its subscribers hear it.\n yield* checkAvailable\n\n const chatApi: Chat = {\n submit: Effect.fnUntraced(function* (input) {\n yield* store(input)\n yield* inRequest(drain)\n }),\n submitInBackground: Effect.fnUntraced(function* (input) {\n yield* store(input)\n yield* inBackground(drain)\n }),\n resume: Effect.gen(function* () {\n if (History.needsTurn(yield* chat.entries, wakes)) yield* inBackground(drain)\n }),\n stream: (after) =>\n Stream.unwrap(\n Effect.gen(function* () {\n // Subscribes before it reads, so nothing appended in between is missed.\n const subscription = yield* PubSub.subscribe(channel)\n const all = yield* chat.entries\n const stored = all.filter((entry) => entry.seq > after)\n const last = stored.at(-1)?.seq ?? after\n\n // Deleted: what the reader hasn't seen, then the end. Otherwise the stream ends with the `ChatDeleted`.\n if (all.some(isChatDeleted)) return Stream.fromIterable(stored)\n\n return Stream.concat(\n Stream.fromIterable(stored),\n Stream.fromSubscription(subscription).pipe(Stream.filter((entry) => entry.seq > last))\n ).pipe(Stream.takeUntil(isChatDeleted))\n })\n ),\n delete: remove\n }\n\n return chatApi\n})\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"chat.js","names":["AssistantEvent.isLibraryEvent","AssistantEvent.isInput","Chats.Chats","AppEvents.isRegistered","AppEvents.encode","AppEvents.decodeEntries","ToolApproval.fromEntries","AppEvents.markInput","History.needsTurn","Turns.run","Specialists.makeRunner","SpecialistService.Runner"],"sources":["../../src/internal/chat.ts"],"sourcesContent":["import * as Cause from \"effect/Cause\"\nimport * as Context from \"effect/Context\"\nimport * as Deferred from \"effect/Deferred\"\nimport * as Effect from \"effect/Effect\"\nimport * as Exit from \"effect/Exit\"\nimport * as Fiber from \"effect/Fiber\"\nimport * as FiberSet from \"effect/FiberSet\"\nimport * as Predicate from \"effect/Predicate\"\nimport * as PubSub from \"effect/PubSub\"\nimport * as Ref from \"effect/Ref\"\nimport * as Scope from \"effect/Scope\"\nimport * as Semaphore from \"effect/Semaphore\"\nimport * as Stream from \"effect/Stream\"\nimport * as SynchronizedRef from \"effect/SynchronizedRef\"\nimport * as AssistantEvent from \"../AssistantEvent.ts\"\nimport type { ChatId } from \"../ChatId.ts\"\nimport { ChatLog } from \"../ChatLog.ts\"\nimport { CurrentChat } from \"../CurrentChat.ts\"\nimport * as Chats from \"../Chats.ts\"\nimport * as SpecialistService from \"../Specialist.ts\"\nimport * as ToolApproval from \"../ToolApproval.ts\"\nimport * as AppEvents from \"./events.ts\"\nimport * as History from \"./history.ts\"\nimport * as Specialists from \"./specialists.ts\"\nimport * as Turns from \"./turns.ts\"\nimport type { Writer } from \"./writer.ts\"\n\nconst idOf = (event: { readonly _tag: string }): string | undefined =>\n AssistantEvent.isLibraryEvent(event)\n ? AssistantEvent.isInput(event)\n ? event.id\n : undefined\n : Predicate.hasProperty(event, \"id\") && Predicate.isString(event.id)\n ? event.id\n : undefined\n\nconst makeChannelPubSub = Effect.acquireRelease(PubSub.unbounded<AssistantEvent.Entry>(), PubSub.shutdown)\n\nconst makeHolding = Effect.fnUntraced(function* (keepAlive: (enabled: boolean) => Effect.Effect<void>) {\n const holds = yield* SynchronizedRef.make(0)\n\n const acquire = SynchronizedRef.updateEffect(\n holds,\n Effect.fnUntraced(function* (n: number) {\n if (n !== 0) return n + 1\n\n yield* keepAlive(true)\n\n return 1\n })\n )\n\n const release = SynchronizedRef.updateEffect(\n holds,\n Effect.fnUntraced(function* (n: number) {\n if (n !== 1) return n - 1\n\n yield* keepAlive(false)\n\n return 0\n })\n )\n\n return (effect: Effect.Effect<void>) =>\n Effect.acquireUseRelease(\n acquire,\n () => effect,\n // An interrupted effect (a shutdown) keeps its hold, so the chat starts again and picks up where it was cut off.\n (_, exit) => (Exit.hasInterrupts(exit) ? Effect.void : release)\n )\n})\n\n/** @internal */\nexport interface Chat {\n readonly submit: (input: AssistantEvent.Input | AssistantEvent.StoredAppEvent) => Effect.Effect<void>\n readonly submitInBackground: (input: AssistantEvent.Input | AssistantEvent.StoredAppEvent) => Effect.Effect<void>\n // For inputs no turn has read, like a report stored during a shutdown.\n readonly resume: Effect.Effect<void>\n readonly stream: (after: number) => Stream.Stream<AssistantEvent.Entry>\n // Stops what works in the chat and ends its subscribers' streams; its log stays.\n readonly delete: Effect.Effect<void>\n}\n\nconst isChatDeleted = (entry: AssistantEvent.Entry) => Predicate.isTagged(entry.event, \"ChatDeleted\")\n\nconst deletedChat = (entries: ReadonlyArray<AssistantEvent.Entry>): Chat => ({\n submit: () => Effect.void,\n submitInBackground: () => Effect.void,\n resume: Effect.void,\n stream: (after) => Stream.fromIterable(entries.filter((entry) => entry.seq > after)),\n delete: Effect.void\n})\n\nconst refused = (input: { readonly _tag: string }, why: string) =>\n Effect.logDebug(`assistant chat ${why}: ${input._tag} not stored`)\n\n/** @internal */\nexport const make = Effect.fnUntraced(function* (options: {\n readonly registration: Turns.Registration\n readonly chatId: ChatId\n readonly keepAlive: (enabled: boolean) => Effect.Effect<void>\n}) {\n const { registration, chatId } = options\n const { app } = registration.definition\n const storage = yield* ChatLog\n const chats = yield* Chats.Chats\n const initial = yield* storage.read(chatId)\n\n if (initial.some(isChatDeleted)) return deletedChat(initial)\n\n const channel = yield* makeChannelPubSub\n const writes = yield* Semaphore.make(1)\n const turnsLock = yield* Semaphore.make(1)\n const holding = yield* makeHolding(options.keepAlive)\n const deleted = yield* Deferred.make<undefined>()\n // So deleting the chat stops all its work: its turns, runs, and the builds' resources.\n const working = yield* Scope.fork(yield* Effect.scope)\n const inWorking = Effect.provideService(Scope.Scope, working)\n const background = yield* inWorking(FiberSet.make())\n const requests = yield* inWorking(FiberSet.make())\n\n const isDeleted = Deferred.isDone(deleted)\n\n // The chat is its log's only writer, so it reads the log once and turns read memory.\n const entries = yield* Ref.make(initial)\n\n // Every writer goes through here, one at a time, so each append is numbered after the last. After a delete it\n // stores nothing.\n const write = Effect.fnUntraced(function* (\n events: ReadonlyArray<AssistantEvent.AssistantEvent | AssistantEvent.StoredAppEvent>\n ) {\n const unknown = events.find((event) => !AssistantEvent.isLibraryEvent(event) && !AppEvents.isRegistered(app, event))\n\n if (unknown !== undefined) {\n return yield* Effect.die(`${unknown._tag} isn't one of the ${registration.definition.name} assistant's events.`)\n }\n\n if (yield* isDeleted) return []\n\n return yield* stored(events)\n })\n\n // Uninterruptible: a stored append must reach memory and the subscribers too, or a turn misses it and replies twice.\n const stored = Effect.fnUntraced(function* (\n events: ReadonlyArray<AssistantEvent.AssistantEvent | AssistantEvent.StoredAppEvent>\n ) {\n const stored = yield* storage.append(chatId, events)\n\n yield* Ref.update(entries, (all) => [...all, ...stored])\n yield* PubSub.publishAll(channel, stored)\n\n return stored\n }, Effect.uninterruptible)\n\n const chat: Writer = {\n chatId,\n entries: Ref.get(entries),\n append: (events) => Semaphore.withPermit(writes)(write(events))\n }\n\n const toStored = (event: {\n readonly _tag: string\n }): Effect.Effect<AssistantEvent.AssistantEvent | AssistantEvent.StoredAppEvent> =>\n AssistantEvent.isLibraryEvent(event)\n ? Effect.succeed(event)\n : AppEvents.isRegistered(app, event)\n ? AppEvents.encode(app, event)\n : Effect.die(`${event._tag} isn't one of the ${registration.definition.name} assistant's events.`)\n\n // What handlers, tools, and runs see: the assistant's own events decoded, and encoded when appended.\n const handle: Turns.TypedChat = {\n chatId,\n entries: Effect.map(chat.entries, (all) => AppEvents.decodeEntries(app, all)),\n append: (...events) => Effect.asVoid(Effect.flatMap(Effect.forEach(events, toStored), chat.append))\n }\n\n // A chat the app deleted is deleted here too, which stops what works in it: a turn that checks stops there.\n const checkAvailable: Effect.Effect<boolean> = chats.get(chatId).pipe(\n Effect.as(true),\n Effect.catchTag(\"GetChatError\", (unavailable) =>\n unavailable.reason === \"Deleted\"\n ? Effect.as(\n Effect.suspend(() => remove),\n false\n )\n : Effect.succeed(false)\n )\n )\n\n const store = Effect.fnUntraced(function* (input: AssistantEvent.Input | AssistantEvent.StoredAppEvent) {\n if (yield* isDeleted) return yield* refused(input, \"is deleted\")\n\n if (!(yield* checkAvailable)) return yield* refused(input, \"is unavailable\")\n\n // Checked under the write lock: a retry that arrives while the first submit is storing is stored once.\n yield* Semaphore.withPermit(writes)(\n Effect.gen(function* () {\n if (yield* isDeleted) return yield* refused(input, \"is deleted\")\n\n const id = idOf(input)\n const all = yield* Ref.get(entries)\n\n if (id !== undefined && all.some(({ event }) => idOf(event) === id)) return\n\n // A decision counts only for a call that still waits: not one decided, or expired, or unknown.\n if (\n Predicate.isTagged(input, \"ApprovalDecision\") &&\n ToolApproval.fromEntries(all).get(input.callId)?.status !== \"pending\"\n ) {\n return yield* refused(input, \"has no call waiting for that decision\")\n }\n\n // Stored as an input: it came in from outside a turn, so it wakes one, also after a restart.\n yield* write([AssistantEvent.isLibraryEvent(input) ? input : AppEvents.markInput(input)])\n })\n )\n })\n\n // The handlers' builds run in the chat's working scope: what they set up lives until the chat closes or is deleted.\n const handler = yield* inWorking(Effect.provideService(registration.open(handle), CurrentChat, handle))\n\n const specialists = yield* Effect.forEach(\n registration.specialists,\n Effect.fnUntraced(function* ({ specialist, built }) {\n const handler = yield* inWorking(Effect.provideService(built.open, CurrentChat, handle))\n\n return { specialist, handler }\n })\n )\n\n const inBackground = (effect: Effect.Effect<void>) => FiberSet.run(background, holding(effect)).pipe(Effect.asVoid)\n\n // A request's turns run in the working scope too, so a delete stops them; the request ends quietly then.\n const inRequest = Effect.fnUntraced(function* (effect: Effect.Effect<void>) {\n const fiber = yield* FiberSet.run(requests, effect)\n const exit = yield* Fiber.await(fiber).pipe(Effect.onInterrupt(() => Fiber.interrupt(fiber)))\n\n if (Exit.isFailure(exit) && !Cause.hasInterruptsOnly(exit.cause)) {\n return yield* Effect.failCause(exit.cause)\n }\n }, Effect.orDie)\n\n const drain: Effect.Effect<void> = Semaphore.withPermit(turnsLock)(\n Effect.gen(function* () {\n while (!(yield* isDeleted) && History.needsTurn(yield* chat.entries)) {\n yield* Turns.run({ registration, handler, chat, services, checkOpen: Effect.asVoid(checkAvailable) })\n }\n })\n )\n\n const runner = yield* inWorking(\n Specialists.makeRunner({\n chat,\n handle,\n specialists,\n model: registration.model,\n holding,\n report: Effect.fnUntraced(function* (finished) {\n yield* store(finished)\n yield* inBackground(drain)\n })\n })\n )\n\n const services = Context.make(CurrentChat, handle).pipe(\n Context.add(SpecialistService.Runner, runner),\n Context.merge(registration.model)\n )\n\n const remove = Effect.gen(function* () {\n // Under the write lock: an append in flight is stored before the `ChatDeleted`; none comes after.\n const first = yield* Semaphore.withPermit(writes)(\n Effect.gen(function* () {\n if (yield* isDeleted) return false\n\n yield* stored([AssistantEvent.ChatDeleted.make({})])\n\n return yield* Deferred.succeed(deleted, undefined)\n })\n ).pipe(Effect.uninterruptible)\n\n if (!first) return\n\n // Its own fiber: a tool that deletes its own chat is in the working scope, and closing it interrupts that tool.\n // Interrupted work keeps its hold (see `makeHolding`), so the chat lets go of it here.\n const closing = yield* Effect.forkDetach(\n Effect.gen(function* () {\n yield* Scope.close(working, Exit.void)\n yield* options.keepAlive(false)\n })\n )\n\n yield* Fiber.await(closing)\n })\n\n // A chat the app deleted while it was closed is deleted now, so its subscribers hear it.\n yield* checkAvailable\n\n const chatApi: Chat = {\n submit: Effect.fnUntraced(function* (input) {\n yield* store(input)\n yield* inRequest(drain)\n }),\n submitInBackground: Effect.fnUntraced(function* (input) {\n yield* store(input)\n yield* inBackground(drain)\n }),\n resume: Effect.gen(function* () {\n if (History.needsTurn(yield* chat.entries)) yield* inBackground(drain)\n }),\n stream: (after) =>\n Stream.unwrap(\n Effect.gen(function* () {\n // Subscribes before it reads, so nothing appended in between is missed.\n const subscription = yield* PubSub.subscribe(channel)\n const all = yield* chat.entries\n const stored = all.filter((entry) => entry.seq > after)\n const last = stored.at(-1)?.seq ?? after\n\n // Deleted: what the reader hasn't seen, then the end. Otherwise the stream ends with the `ChatDeleted`.\n if (all.some(isChatDeleted)) return Stream.fromIterable(stored)\n\n return Stream.concat(\n Stream.fromIterable(stored),\n Stream.fromSubscription(subscription).pipe(Stream.filter((entry) => entry.seq > last))\n ).pipe(Stream.takeUntil(isChatDeleted))\n })\n ),\n delete: remove\n }\n\n return chatApi\n})\n"],"mappings":"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"}
|
|
@@ -1,46 +1,45 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { AnyEntry, AssistantEvent_d_exports } from "../AssistantEvent.js";
|
|
1
|
+
import { AnyEntry, AppEventSchema, AssistantEvent_d_exports, StoredAppEvent } from "../AssistantEvent.js";
|
|
3
2
|
import { Assistant_d_exports, Entry } from "../Assistant.js";
|
|
4
3
|
import * as Effect from "effect/Effect";
|
|
5
4
|
import * as Schema from "effect/Schema";
|
|
6
5
|
import * as Option from "effect/Option";
|
|
7
6
|
declare namespace events_d_exports {
|
|
8
|
-
export { AppEvents, DecodedEntries, decodeEntries, decodeEntry, encode, isRegistered, make,
|
|
7
|
+
export { AppEvents, DecodedEntries, decodeEntries, decodeEntry, decodeWith, encode, isRegistered, make, markInput, tagOf };
|
|
9
8
|
}
|
|
10
9
|
/** @internal */
|
|
11
10
|
export interface AppEvents {
|
|
12
|
-
readonly byTag: ReadonlyMap<string,
|
|
13
|
-
readonly wakes: ReadonlySet<string>;
|
|
14
|
-
}
|
|
15
|
-
interface Registered {
|
|
16
|
-
readonly schema: EventSchema;
|
|
17
|
-
readonly render: Render<{
|
|
18
|
-
readonly _tag: string;
|
|
19
|
-
}> | undefined;
|
|
11
|
+
readonly byTag: ReadonlyMap<string, AppEventSchema>;
|
|
20
12
|
}
|
|
21
13
|
/** @internal */
|
|
22
|
-
export declare const
|
|
14
|
+
export declare const tagOf: (schema: AppEventSchema) => string;
|
|
15
|
+
/** @internal */
|
|
16
|
+
export declare const make: (events: ReadonlyArray<AppEventSchema>) => AppEvents;
|
|
23
17
|
/** @internal */
|
|
24
18
|
export declare const encode: (app: AppEvents, event: {
|
|
25
19
|
readonly _tag: string;
|
|
26
20
|
}) => Effect.Effect<{
|
|
27
21
|
readonly [x: string]: Schema.Json;
|
|
28
22
|
readonly _tag: string & import("effect/Brand").Brand<"~effect-assistant/AppTag">;
|
|
23
|
+
readonly "~effect-assistant/input"?: true | undefined;
|
|
29
24
|
}, never, never>;
|
|
30
25
|
/** @internal */
|
|
31
26
|
export declare const isRegistered: (app: AppEvents, event: {
|
|
32
27
|
readonly _tag: string;
|
|
33
28
|
}) => boolean;
|
|
34
29
|
/** @internal */
|
|
30
|
+
export declare const decodeWith: (schema: AppEventSchema, event: {
|
|
31
|
+
readonly _tag: string;
|
|
32
|
+
}) => Option.Option<{
|
|
33
|
+
readonly _tag: string;
|
|
34
|
+
}>;
|
|
35
|
+
/** @internal */
|
|
36
|
+
export declare const markInput: (event: StoredAppEvent) => StoredAppEvent;
|
|
37
|
+
/** @internal */
|
|
35
38
|
export declare const decodeEntry: (app: AppEvents, entry: AnyEntry) => Option.Option<Entry<any>>;
|
|
36
39
|
/** @internal */
|
|
37
40
|
export declare const decodeEntries: (app: AppEvents, entries: ReadonlyArray<AnyEntry>) => ReadonlyArray<Entry<any>>;
|
|
38
41
|
/** @internal */
|
|
39
42
|
export type DecodedEntries = ReturnType<typeof decodeEntries>;
|
|
40
|
-
/** @internal */
|
|
41
|
-
export declare const read: (app: AppEvents, event: {
|
|
42
|
-
readonly _tag: string;
|
|
43
|
-
}) => string | undefined;
|
|
44
43
|
//#endregion
|
|
45
44
|
export { events_d_exports };
|
|
46
45
|
//# sourceMappingURL=events.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"events.d.ts","names":[],"sources":["../../src/internal/events.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"events.d.ts","names":[],"sources":["../../src/internal/events.ts"],"mappings":";;;;;;;;;iBAWiB;WACN,OAAO,oBAAoB;;;qBAIzB,QAAK,QAAY;;qBASjB,OAAI,QAAY,cAAc,oBAAiC;;qBAa/D,SAAM,KAAA,WAAA;WAAwE;;;;;;;qBAY9E,eAAY,KAAS,WAAS;WAAoB;;;qBAQlD,aAAU,QACb,gBAA6B;WACnB;MACjB,OAAO;WAAkB;;;qBAUf,YAAS,OAAW,mBAAgC;;qBAOpD,cAAW,KAAS,WAAS,OAAS,aAA0B,OAAO,OAAO;;qBAS9E,gBAAa,KACnB,WAAS,SACL,cAAc,cACtB,cAAc;;YAGL,iBAAiB,kBAAkB"}
|
package/dist/internal/events.js
CHANGED
|
@@ -1,31 +1,46 @@
|
|
|
1
|
+
import { inputKey } from "./input.js";
|
|
1
2
|
import { StoredAppEvent, isLibraryEvent } from "../AssistantEvent.js";
|
|
2
|
-
import { toJsonText } from "./schema.js";
|
|
3
3
|
import * as Effect from "effect/Effect";
|
|
4
|
+
import * as Predicate from "effect/Predicate";
|
|
4
5
|
import * as Schema from "effect/Schema";
|
|
5
6
|
import * as Option from "effect/Option";
|
|
7
|
+
import * as Record from "effect/Record";
|
|
8
|
+
import * as SchemaAST from "effect/SchemaAST";
|
|
6
9
|
//#region src/internal/events.ts
|
|
7
10
|
/** @internal */
|
|
11
|
+
const tagOf = (schema) => {
|
|
12
|
+
const ast = schema.fields._tag.ast;
|
|
13
|
+
if (SchemaAST.isLiteral(ast) && Predicate.isString(ast.literal)) return ast.literal;
|
|
14
|
+
throw new Error("An app event is a tagged struct or class.");
|
|
15
|
+
};
|
|
16
|
+
/** @internal */
|
|
8
17
|
const make = (events) => {
|
|
9
|
-
const byTag = new Map(events.map((
|
|
10
|
-
schema: event.schema,
|
|
11
|
-
render: event.render
|
|
12
|
-
}]));
|
|
18
|
+
const byTag = new Map(events.map((schema) => [tagOf(schema), schema]));
|
|
13
19
|
for (const tag of byTag.keys()) if (isLibraryEvent({ _tag: tag })) throw new Error(`${tag} is one of the library's events.`);
|
|
14
20
|
if (byTag.size !== events.length) throw new Error("An assistant's events have different tags.");
|
|
15
|
-
return {
|
|
16
|
-
byTag,
|
|
17
|
-
wakes: new Set(events.flatMap((event) => event.wakes ? [event.tag] : []))
|
|
18
|
-
};
|
|
21
|
+
return { byTag };
|
|
19
22
|
};
|
|
20
23
|
/** @internal */
|
|
21
24
|
const encode = Effect.fnUntraced(function* (app, event) {
|
|
22
|
-
const
|
|
23
|
-
if (
|
|
24
|
-
const json = yield* Schema.encodeUnknownEffect(Schema.toCodecJson(
|
|
25
|
+
const schema = app.byTag.get(event._tag);
|
|
26
|
+
if (schema === void 0) return yield* Effect.die(`${event._tag} isn't one of this assistant's events.`);
|
|
27
|
+
const json = yield* Schema.encodeUnknownEffect(Schema.toCodecJson(schema))(event);
|
|
25
28
|
return yield* Schema.decodeUnknownEffect(StoredAppEvent)(json);
|
|
26
29
|
}, Effect.orDie);
|
|
27
30
|
/** @internal */
|
|
28
31
|
const isRegistered = (app, event) => app.byTag.has(event._tag);
|
|
32
|
+
const unmarked = (event) => Predicate.hasProperty(event, "~effect-assistant/input") ? Record.remove(event, inputKey) : event;
|
|
33
|
+
/** @internal */
|
|
34
|
+
const decodeWith = (schema, event) => Schema.decodeUnknownOption(Schema.toCodecJson(schema))(unmarked(event));
|
|
35
|
+
const decodeEvent = (app, event) => {
|
|
36
|
+
const schema = app.byTag.get(event._tag);
|
|
37
|
+
return schema === void 0 ? Option.none() : decodeWith(schema, event);
|
|
38
|
+
};
|
|
39
|
+
/** @internal */
|
|
40
|
+
const markInput = (event) => ({
|
|
41
|
+
...event,
|
|
42
|
+
[inputKey]: true
|
|
43
|
+
});
|
|
29
44
|
/** @internal */
|
|
30
45
|
const decodeEntry = (app, entry) => {
|
|
31
46
|
const { event } = entry;
|
|
@@ -33,33 +48,19 @@ const decodeEntry = (app, entry) => {
|
|
|
33
48
|
...toEntry(entry),
|
|
34
49
|
event
|
|
35
50
|
});
|
|
36
|
-
|
|
37
|
-
if (registered === void 0) return Option.none();
|
|
38
|
-
return Option.map(Schema.decodeUnknownOption(Schema.toCodecJson(registered.schema))(event), (decoded) => ({
|
|
51
|
+
return Option.map(decodeEvent(app, event), (decoded) => ({
|
|
39
52
|
...toEntry(entry),
|
|
40
53
|
event: decoded
|
|
41
54
|
}));
|
|
42
55
|
};
|
|
43
56
|
/** @internal */
|
|
44
57
|
const decodeEntries = (app, entries) => entries.flatMap((entry) => Option.toArray(decodeEntry(app, entry)));
|
|
45
|
-
const decodeStored = Schema.decodeUnknownOption(StoredAppEvent);
|
|
46
58
|
const toEntry = (entry) => ({
|
|
47
59
|
chatId: entry.chatId,
|
|
48
60
|
seq: entry.seq,
|
|
49
61
|
at: entry.at
|
|
50
62
|
});
|
|
51
|
-
/** @internal */
|
|
52
|
-
const read = (app, event) => {
|
|
53
|
-
const registered = app.byTag.get(event._tag);
|
|
54
|
-
if (registered?.render === void 0) return void 0;
|
|
55
|
-
if (registered.render !== "json") return registered.render(event);
|
|
56
|
-
const stored = Schema.encodeUnknownOption(Schema.toCodecJson(registered.schema))(event).pipe(Option.flatMap(decodeStored));
|
|
57
|
-
return Option.match(stored, {
|
|
58
|
-
onNone: () => void 0,
|
|
59
|
-
onSome: ({ _tag, ...fields }) => `<${_tag}>\n${toJsonText(fields)}\n</${_tag}>`
|
|
60
|
-
});
|
|
61
|
-
};
|
|
62
63
|
//#endregion
|
|
63
|
-
export { decodeEntries, decodeEntry, encode, isRegistered, make,
|
|
64
|
+
export { decodeEntries, decodeEntry, decodeWith, encode, isRegistered, make, markInput, tagOf };
|
|
64
65
|
|
|
65
66
|
//# sourceMappingURL=events.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"events.js","names":["AssistantEvent.isLibraryEvent","AssistantEvent.StoredAppEvent"],"sources":["../../src/internal/events.ts"],"sourcesContent":["import * as Effect from \"effect/Effect\"\nimport * as Option from \"effect/Option\"\nimport * as Schema from \"effect/Schema\"\nimport
|
|
1
|
+
{"version":3,"file":"events.js","names":["AssistantEvent.isLibraryEvent","AssistantEvent.StoredAppEvent"],"sources":["../../src/internal/events.ts"],"sourcesContent":["import * as Effect from \"effect/Effect\"\nimport * as Option from \"effect/Option\"\nimport * as Predicate from \"effect/Predicate\"\nimport * as Record from \"effect/Record\"\nimport * as Schema from \"effect/Schema\"\nimport * as SchemaAST from \"effect/SchemaAST\"\nimport type * as Assistant from \"../Assistant.ts\"\nimport * as AssistantEvent from \"../AssistantEvent.ts\"\nimport { inputKey } from \"./input.ts\"\n\n/** @internal */\nexport interface AppEvents {\n readonly byTag: ReadonlyMap<string, AssistantEvent.AppEventSchema>\n}\n\n/** @internal */\nexport const tagOf = (schema: AssistantEvent.AppEventSchema): string => {\n const ast = schema.fields._tag.ast\n\n if (SchemaAST.isLiteral(ast) && Predicate.isString(ast.literal)) return ast.literal\n\n throw new Error(\"An app event is a tagged struct or class.\")\n}\n\n/** @internal */\nexport const make = (events: ReadonlyArray<AssistantEvent.AppEventSchema>): AppEvents => {\n const byTag = new Map(events.map((schema): [string, AssistantEvent.AppEventSchema] => [tagOf(schema), schema]))\n\n for (const tag of byTag.keys()) {\n if (AssistantEvent.isLibraryEvent({ _tag: tag })) throw new Error(`${tag} is one of the library's events.`)\n }\n\n if (byTag.size !== events.length) throw new Error(\"An assistant's events have different tags.\")\n\n return { byTag }\n}\n\n/** @internal */\nexport const encode = Effect.fnUntraced(function* (app: AppEvents, event: { readonly _tag: string }) {\n const schema = app.byTag.get(event._tag)\n\n // The types only let registered events through, so one with no schema here is a defect.\n if (schema === undefined) return yield* Effect.die(`${event._tag} isn't one of this assistant's events.`)\n\n const json = yield* Schema.encodeUnknownEffect(Schema.toCodecJson(schema))(event)\n\n return yield* Schema.decodeUnknownEffect(AssistantEvent.StoredAppEvent)(json)\n}, Effect.orDie)\n\n/** @internal */\nexport const isRegistered = (app: AppEvents, event: { readonly _tag: string }): boolean => app.byTag.has(event._tag)\n\n// The app's schema decodes the event as the app wrote it.\nconst unmarked = (event: { readonly _tag: string }) =>\n Predicate.hasProperty(event, inputKey) ? Record.remove(event, inputKey) : event\n\n// An event as the log stores it, decoded with its schema.\n/** @internal */\nexport const decodeWith = (\n schema: AssistantEvent.AppEventSchema,\n event: { readonly _tag: string }\n): Option.Option<{ readonly _tag: string }> => Schema.decodeUnknownOption(Schema.toCodecJson(schema))(unmarked(event))\n\nconst decodeEvent = (app: AppEvents, event: { readonly _tag: string }): Option.Option<{ readonly _tag: string }> => {\n const schema = app.byTag.get(event._tag)\n\n return schema === undefined ? Option.none() : decodeWith(schema, event)\n}\n\n// What came in through `submit`, from outside a turn, is an input: it wakes a turn.\n/** @internal */\nexport const markInput = (event: AssistantEvent.StoredAppEvent): AssistantEvent.StoredAppEvent => ({\n ...event,\n [inputKey]: true\n})\n\n// An app event that doesn't decode (say, one from an older version of its schema) is left out.\n/** @internal */\nexport const decodeEntry = (app: AppEvents, entry: AssistantEvent.AnyEntry): Option.Option<Assistant.Entry<any>> => {\n const { event } = entry\n\n if (AssistantEvent.isLibraryEvent(event)) return Option.some({ ...toEntry(entry), event })\n\n return Option.map(decodeEvent(app, event), (decoded) => ({ ...toEntry(entry), event: decoded }))\n}\n\n/** @internal */\nexport const decodeEntries = (\n app: AppEvents,\n entries: ReadonlyArray<AssistantEvent.AnyEntry>\n): ReadonlyArray<Assistant.Entry<any>> => entries.flatMap((entry) => Option.toArray(decodeEntry(app, entry)))\n\n/** @internal */\nexport type DecodedEntries = ReturnType<typeof decodeEntries>\n\nconst toEntry = (entry: AssistantEvent.AnyEntry) => ({\n chatId: entry.chatId,\n seq: entry.seq,\n at: entry.at\n})\n"],"mappings":";;;;;;;;;;AAgBA,MAAa,SAAS,WAAkD;CACtE,MAAM,MAAM,OAAO,OAAO,KAAK;CAE/B,IAAI,UAAU,UAAU,GAAG,KAAK,UAAU,SAAS,IAAI,OAAO,GAAG,OAAO,IAAI;CAE5E,MAAM,IAAI,MAAM,2CAA2C;AAC7D;;AAGA,MAAa,QAAQ,WAAoE;CACvF,MAAM,QAAQ,IAAI,IAAI,OAAO,KAAK,WAAoD,CAAC,MAAM,MAAM,GAAG,MAAM,CAAC,CAAC;CAE9G,KAAK,MAAM,OAAO,MAAM,KAAK,GAC3B,IAAIA,eAA8B,EAAE,MAAM,IAAI,CAAC,GAAG,MAAM,IAAI,MAAM,GAAG,IAAI,iCAAiC;CAG5G,IAAI,MAAM,SAAS,OAAO,QAAQ,MAAM,IAAI,MAAM,4CAA4C;CAE9F,OAAO,EAAE,MAAM;AACjB;;AAGA,MAAa,SAAS,OAAO,WAAW,WAAW,KAAgB,OAAkC;CACnG,MAAM,SAAS,IAAI,MAAM,IAAI,MAAM,IAAI;CAGvC,IAAI,WAAW,KAAA,GAAW,OAAO,OAAO,OAAO,IAAI,GAAG,MAAM,KAAK,uCAAuC;CAExG,MAAM,OAAO,OAAO,OAAO,oBAAoB,OAAO,YAAY,MAAM,CAAC,CAAC,CAAC,KAAK;CAEhF,OAAO,OAAO,OAAO,oBAAoBC,cAA6B,CAAC,CAAC,IAAI;AAC9E,GAAG,OAAO,KAAK;;AAGf,MAAa,gBAAgB,KAAgB,UAA8C,IAAI,MAAM,IAAI,MAAM,IAAI;AAGnH,MAAM,YAAY,UAChB,UAAU,YAAY,OAAA,yBAAe,IAAI,OAAO,OAAO,OAAO,QAAQ,IAAI;;AAI5E,MAAa,cACX,QACA,UAC6C,OAAO,oBAAoB,OAAO,YAAY,MAAM,CAAC,CAAC,CAAC,SAAS,KAAK,CAAC;AAErH,MAAM,eAAe,KAAgB,UAA+E;CAClH,MAAM,SAAS,IAAI,MAAM,IAAI,MAAM,IAAI;CAEvC,OAAO,WAAW,KAAA,IAAY,OAAO,KAAK,IAAI,WAAW,QAAQ,KAAK;AACxE;;AAIA,MAAa,aAAa,WAAyE;CACjG,GAAG;EACF,WAAW;AACd;;AAIA,MAAa,eAAe,KAAgB,UAAwE;CAClH,MAAM,EAAE,UAAU;CAElB,IAAID,eAA8B,KAAK,GAAG,OAAO,OAAO,KAAK;EAAE,GAAG,QAAQ,KAAK;EAAG;CAAM,CAAC;CAEzF,OAAO,OAAO,IAAI,YAAY,KAAK,KAAK,IAAI,aAAa;EAAE,GAAG,QAAQ,KAAK;EAAG,OAAO;CAAQ,EAAE;AACjG;;AAGA,MAAa,iBACX,KACA,YACwC,QAAQ,SAAS,UAAU,OAAO,QAAQ,YAAY,KAAK,KAAK,CAAC,CAAC;AAK5G,MAAM,WAAW,WAAoC;CACnD,QAAQ,MAAM;CACd,KAAK,MAAM;CACX,IAAI,MAAM;AACZ"}
|
package/dist/internal/history.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { fromKey } from "./uuid.js";
|
|
2
|
-
import { isInput as isInput$1, isLibraryEvent, isTurnEnd, isUserInput, isWaking, textWithAttachments } from "../AssistantEvent.js";
|
|
2
|
+
import { isAppInput, isInput as isInput$1, isLibraryEvent, isTurnEnd, isUserInput, isWaking, textWithAttachments } from "../AssistantEvent.js";
|
|
3
3
|
import { toJson, toJsonText } from "./schema.js";
|
|
4
4
|
import * as Predicate from "effect/Predicate";
|
|
5
5
|
import * as Schema from "effect/Schema";
|
|
@@ -54,11 +54,28 @@ const finishedText = (entries, index, finished) => {
|
|
|
54
54
|
const answerText = (answer) => `<answer message="${answer.messageId}" kind="${answer.kind}">\nThe user answered with the block on your message ${answer.messageId}.\n${toJsonText(answer.value)}\n</answer>`;
|
|
55
55
|
const resultText = (result) => `<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>`;
|
|
56
56
|
/** @internal */
|
|
57
|
-
const
|
|
57
|
+
const awaitingApprovalOutput = "Waiting for the user's approval: they see this call and approve or reject it. Its outcome arrives later in this chat; don't call it again meanwhile.";
|
|
58
|
+
const awaitingText = (request) => `<approval tool="${request.tool}" call="${request.callId}" status="pending">\nYour call to ${request.tool} from earlier in this chat waits for the user's approval.\n${toJsonText(request.params)}\n</approval>`;
|
|
59
|
+
const approvedText = (result) => `<result tool="${result.tool}" call="${result.callId}" outcome="${result.outcome}">\nThe user approved your call to ${result.tool} from earlier in this chat, and it ran.\n${toJsonText(result.result)}\n</result>`;
|
|
60
|
+
const rejectedText = (decision, tool) => `<approval tool="${tool}" call="${decision.callId}" decision="rejected">\nThe user rejected your call to ${tool}; it didn't run.${decision.reason === void 0 ? "" : `\nTheir reason: ${decision.reason}`}\n</approval>`;
|
|
61
|
+
const expiredText = (callId, tool) => `<approval tool="${tool}" call="${callId}" status="expired">\nThe user moved on without approving your call to ${tool}; it didn't run.\n</approval>`;
|
|
62
|
+
const awaitingMessage = (request) => Prompt.toolMessage({ content: [Prompt.toolResultPart({
|
|
63
|
+
id: request.callId,
|
|
64
|
+
name: request.tool,
|
|
65
|
+
isFailure: false,
|
|
66
|
+
result: awaitingApprovalOutput,
|
|
67
|
+
providerExecuted: false
|
|
68
|
+
})] });
|
|
69
|
+
const isEntry = (item) => Predicate.hasProperty(item, "seq");
|
|
70
|
+
/** @internal */
|
|
71
|
+
const fromEntries = (items) => {
|
|
72
|
+
const entries = items.filter(isEntry);
|
|
58
73
|
const history = [];
|
|
59
74
|
const held = [];
|
|
60
75
|
const open = /* @__PURE__ */ new Set();
|
|
61
76
|
const called = /* @__PURE__ */ new Set();
|
|
77
|
+
const requests = /* @__PURE__ */ new Map();
|
|
78
|
+
const toolOf = (callId) => requests.get(callId)?.tool ?? "a tool";
|
|
62
79
|
const flushIfClosed = () => {
|
|
63
80
|
if (open.size === 0) history.push(...held.splice(0));
|
|
64
81
|
};
|
|
@@ -66,20 +83,24 @@ const fromEntries = (entries, readApp) => {
|
|
|
66
83
|
const next = entries.slice(from + 1).find(({ event }) => Predicate.isTagged(event, "ModelOutput"))?.event;
|
|
67
84
|
return next !== void 0 && isLibraryEvent(next) && Predicate.isTagged(next, "ModelOutput") ? next.seenThroughSeq : void 0;
|
|
68
85
|
};
|
|
69
|
-
const input = (index, item) => {
|
|
86
|
+
const input = (index, seq, item) => {
|
|
70
87
|
const seen = nextSeen(index);
|
|
71
|
-
if (open.size > 0 || seen !== void 0 && seen <
|
|
88
|
+
if (open.size > 0 || seen !== void 0 && seen < seq) held.push(item);
|
|
72
89
|
else history.push(item);
|
|
73
90
|
};
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
91
|
+
let index = -1;
|
|
92
|
+
for (const item of items) {
|
|
93
|
+
if (!isEntry(item)) {
|
|
94
|
+
input(index, (entries[index]?.seq ?? 0) + .5, item);
|
|
95
|
+
continue;
|
|
79
96
|
}
|
|
97
|
+
index += 1;
|
|
98
|
+
const { event, seq } = item;
|
|
99
|
+
const at = index;
|
|
100
|
+
if (!isLibraryEvent(event)) continue;
|
|
80
101
|
Match.value(event).pipe(Match.tagsExhaustive({
|
|
81
|
-
UserMessage: (message) => input(
|
|
82
|
-
BlockAnswer: (answer) => input(
|
|
102
|
+
UserMessage: (message) => input(at, seq, userText(textWithAttachments(message.text, message.attachments))),
|
|
103
|
+
BlockAnswer: (answer) => input(at, seq, userText(answerText(answer))),
|
|
83
104
|
ModelOutput: (output) => {
|
|
84
105
|
history.push(...Prompt.fromResponseParts(output.parts).content);
|
|
85
106
|
for (const part of output.parts) if (part.type === "tool-call") {
|
|
@@ -89,7 +110,8 @@ const fromEntries = (entries, readApp) => {
|
|
|
89
110
|
flushIfClosed();
|
|
90
111
|
},
|
|
91
112
|
ToolResult: (result) => {
|
|
92
|
-
if (
|
|
113
|
+
if (requests.has(result.callId)) return input(at, seq, userText(approvedText(result)));
|
|
114
|
+
if (!called.has(result.callId)) return input(at, seq, userText(resultText(result)));
|
|
93
115
|
history.push(toolMessage(result));
|
|
94
116
|
open.delete(result.callId);
|
|
95
117
|
flushIfClosed();
|
|
@@ -99,32 +121,41 @@ const fromEntries = (entries, readApp) => {
|
|
|
99
121
|
TurnStarted: () => void 0,
|
|
100
122
|
TurnCompleted: () => void 0,
|
|
101
123
|
TurnFailed: () => void 0,
|
|
102
|
-
SpecialistFinished: (finished) => input(
|
|
124
|
+
SpecialistFinished: (finished) => input(at, seq, userText(finishedText(entries, at, finished))),
|
|
103
125
|
SpecialistStarted: () => void 0,
|
|
104
126
|
BlocksClosed: () => void 0,
|
|
127
|
+
ApprovalRequested: (request) => {
|
|
128
|
+
requests.set(request.callId, request);
|
|
129
|
+
if (!called.has(request.callId)) return input(at, seq, userText(awaitingText(request)));
|
|
130
|
+
history.push(awaitingMessage(request));
|
|
131
|
+
open.delete(request.callId);
|
|
132
|
+
flushIfClosed();
|
|
133
|
+
},
|
|
134
|
+
ApprovalDecision: (decision) => decision.decision === "rejected" ? input(at, seq, userText(rejectedText(decision, toolOf(decision.callId)))) : void 0,
|
|
135
|
+
ApprovalsExpired: (expired) => expired.callIds.forEach((callId) => input(at, seq, userText(expiredText(callId, toolOf(callId))))),
|
|
105
136
|
ChatDeleted: () => void 0
|
|
106
137
|
}));
|
|
107
|
-
}
|
|
138
|
+
}
|
|
108
139
|
return [...history, ...held];
|
|
109
140
|
};
|
|
110
141
|
/** @internal */
|
|
111
|
-
const isInput = (event
|
|
142
|
+
const isInput = (event) => isInput$1(event) || isAppInput(event);
|
|
112
143
|
/** @internal */
|
|
113
144
|
const handledThrough = (entries) => {
|
|
114
145
|
const last = entries.findLast(({ event }) => isTurnEnd(event))?.event;
|
|
115
146
|
return last !== void 0 && isTurnEnd(last) ? last.throughSeq : 0;
|
|
116
147
|
};
|
|
117
148
|
/** @internal */
|
|
118
|
-
const unhandledInputs = (entries
|
|
149
|
+
const unhandledInputs = (entries) => {
|
|
119
150
|
const seen = handledThrough(entries);
|
|
120
|
-
return entries.flatMap(({ event, seq }) => seq > seen && isInput(event
|
|
151
|
+
return entries.flatMap(({ event, seq }) => seq > seen && isInput(event) && !(isInput$1(event) && !isWaking(event)) ? [event] : []);
|
|
121
152
|
};
|
|
122
153
|
/** @internal */
|
|
123
|
-
const needsTurn = (entries
|
|
154
|
+
const needsTurn = (entries) => unhandledInputs(entries).length > 0;
|
|
124
155
|
/** @internal */
|
|
125
156
|
const owesReply = (entries, throughSeq) => {
|
|
126
157
|
const latest = entries.findLast(({ event, seq }) => seq <= throughSeq && isInput$1(event) && isUserInput(event));
|
|
127
|
-
return latest !== void 0 && !entries.some(({ event, seq }) => seq > latest.seq && (Predicate.isTagged(event, "AssistantMessage") || Predicate.isTagged(event, "TurnFailed")));
|
|
158
|
+
return latest !== void 0 && !entries.some(({ event, seq }) => seq > latest.seq && (Predicate.isTagged(event, "AssistantMessage") || Predicate.isTagged(event, "TurnFailed") || Predicate.isTagged(event, "ApprovalRequested")));
|
|
128
159
|
};
|
|
129
160
|
const sentToWaitOn = Schema.decodeUnknownOption(Schema.Struct({ wait: Schema.Literal(false) }));
|
|
130
161
|
const progressMessages = (entries) => new Set(entries.flatMap(({ event }) => isLibraryEvent(event) && Predicate.isTagged(event, "ModelOutput") ? event.parts.flatMap((part) => part.type === "tool-call" && part.name === "send_user_message" && Option.isSome(sentToWaitOn(part.params)) ? [fromKey(`message:${part.id}`)] : []) : []));
|
|
@@ -137,11 +168,11 @@ const owesDelivery = (entries, throughSeq) => {
|
|
|
137
168
|
const progress = progressMessages(entries);
|
|
138
169
|
const started = after.flatMap((event) => Predicate.isTagged(event, "SpecialistStarted") ? [event.runId] : []);
|
|
139
170
|
const finished = new Set(entries.flatMap(({ event }) => isLibraryEvent(event) && Predicate.isTagged(event, "SpecialistFinished") ? [event.runId] : []));
|
|
140
|
-
return messages.length > 0 && messages.every((message) => message.block === void 0 && progress.has(message.id)) && !after.some((event) => Predicate.isTagged(event, "TurnFailed")) && started.every((runId) => finished.has(runId));
|
|
171
|
+
return messages.length > 0 && messages.every((message) => message.block === void 0 && progress.has(message.id)) && !after.some((event) => Predicate.isTagged(event, "TurnFailed") || Predicate.isTagged(event, "ApprovalRequested")) && started.every((runId) => finished.has(runId));
|
|
141
172
|
};
|
|
142
173
|
/** @internal */
|
|
143
|
-
const unreadAfter = (entries, throughSeq
|
|
174
|
+
const unreadAfter = (entries, throughSeq) => entries.some(({ event, seq }) => seq > throughSeq && isInput(event));
|
|
144
175
|
//#endregion
|
|
145
|
-
export { fromEntries, handledThrough, isInput, mayHaveRunOutput, needsTurn, notRunOutput, owesDelivery, owesReply, toolMessage, unansweredCalls, unhandledInputs, unreadAfter, userText };
|
|
176
|
+
export { awaitingApprovalOutput, fromEntries, handledThrough, isInput, mayHaveRunOutput, needsTurn, notRunOutput, owesDelivery, owesReply, toolMessage, unansweredCalls, unhandledInputs, unreadAfter, userText };
|
|
146
177
|
|
|
147
178
|
//# sourceMappingURL=history.js.map
|