effect-assistant 0.7.0 → 0.8.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/AssistantEngine.d.ts +2 -2
- package/dist/AssistantEngine.js +2 -2
- package/dist/AssistantEngine.js.map +1 -1
- package/dist/ChatLog.d.ts +33 -6
- package/dist/ChatLog.d.ts.map +1 -1
- package/dist/ChatLog.js +24 -10
- package/dist/ChatLog.js.map +1 -1
- package/dist/internal/history.js +2 -2
- package/dist/internal/history.js.map +1 -1
- package/package.json +1 -1
- package/src/AssistantEngine.ts +2 -2
- package/src/ChatLog.ts +79 -52
- package/src/internal/history.ts +7 -5
|
@@ -40,7 +40,7 @@ export declare class AssistantEngine extends AssistantEngine_base {}
|
|
|
40
40
|
*
|
|
41
41
|
* **Details**
|
|
42
42
|
*
|
|
43
|
-
* With `ChatLog.layerSql`, the engine resumes chats at startup. When an assistant registers, the
|
|
43
|
+
* With `ChatLog.layerSql()`, the engine resumes chats at startup. When an assistant registers, the
|
|
44
44
|
* engine finds the chats that assistant was working on when the process stopped (a turn cut off
|
|
45
45
|
* partway, or an input that no turn read) and runs a turn for each. Run this engine in one process
|
|
46
46
|
* only, because two processes would run the same chat and write its log at the same time.
|
|
@@ -97,7 +97,7 @@ export declare const layerMemory: Layer.Layer<AssistantEngine, never, ChatLog>;
|
|
|
97
97
|
* Layer.provideMerge(AssistantEngine.layerCluster),
|
|
98
98
|
* Layer.provideMerge(
|
|
99
99
|
* Layer.merge(
|
|
100
|
-
* ChatLog.layerSql,
|
|
100
|
+
* ChatLog.layerSql(),
|
|
101
101
|
* SingleRunner.layer({ runnerStorage: "sql" }).pipe(Layer.provide(NodeCrypto.layer))
|
|
102
102
|
* )
|
|
103
103
|
* ),
|
package/dist/AssistantEngine.js
CHANGED
|
@@ -54,7 +54,7 @@ const notRegistered = (assistant) => Effect.die(`The ${assistant} assistant isn'
|
|
|
54
54
|
*
|
|
55
55
|
* **Details**
|
|
56
56
|
*
|
|
57
|
-
* With `ChatLog.layerSql`, the engine resumes chats at startup. When an assistant registers, the
|
|
57
|
+
* With `ChatLog.layerSql()`, the engine resumes chats at startup. When an assistant registers, the
|
|
58
58
|
* engine finds the chats that assistant was working on when the process stopped (a turn cut off
|
|
59
59
|
* partway, or an input that no turn read) and runs a turn for each. Run this engine in one process
|
|
60
60
|
* only, because two processes would run the same chat and write its log at the same time.
|
|
@@ -211,7 +211,7 @@ const makeClusterClient = Effect.gen(function* () {
|
|
|
211
211
|
* Layer.provideMerge(AssistantEngine.layerCluster),
|
|
212
212
|
* Layer.provideMerge(
|
|
213
213
|
* Layer.merge(
|
|
214
|
-
* ChatLog.layerSql,
|
|
214
|
+
* ChatLog.layerSql(),
|
|
215
215
|
* SingleRunner.layer({ runnerStorage: "sql" }).pipe(Layer.provide(NodeCrypto.layer))
|
|
216
216
|
* )
|
|
217
217
|
* ),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AssistantEngine.js","names":["Chat.make","History.needsTurn","AssistantEvent.Input","AssistantEvent.StoredAppEvent","AssistantEvent.Entry"],"sources":["../src/AssistantEngine.ts"],"sourcesContent":["/**\n * This module provides the `AssistantEngine` service, which runs assistants' chats, and its\n * implementations.\n *\n * An assistant's `toLayer` registers the assistant with the engine, and the assistant's `submit`\n * and `subscribe` go through it. Pick one engine for the app, as with a workflow engine.\n * {@link layerMemory} runs chats in the process, for tests and single-process apps.\n * {@link layerCluster} runs each chat as a cluster entity, so a turn cut off by a crash resumes, and\n * {@link layerClusterClient} reaches those chats from a process that runs none.\n *\n * @since 0.1.0\n */\nimport * as ClusterSchema from \"effect/cluster/ClusterSchema\"\nimport * as Entity from \"effect/cluster/Entity\"\nimport * as Sharding from \"effect/cluster/Sharding\"\nimport * as Context from \"effect/Context\"\nimport * as Data from \"effect/Data\"\nimport * as Effect from \"effect/Effect\"\nimport * as Exit from \"effect/Exit\"\nimport * as Layer from \"effect/Layer\"\nimport * as MutableHashMap from \"effect/MutableHashMap\"\nimport * as Option from \"effect/Option\"\nimport * as Predicate from \"effect/Predicate\"\nimport * as RcMap from \"effect/RcMap\"\nimport * as Rpc from \"effect/rpc/Rpc\"\nimport * as Schema from \"effect/Schema\"\nimport * as Scope from \"effect/Scope\"\nimport * as Stream from \"effect/Stream\"\nimport * as AssistantEvent from \"./AssistantEvent.ts\"\nimport { ChatId } from \"./ChatId.ts\"\nimport { ChatLog } from \"./ChatLog.ts\"\nimport * as Chat from \"./internal/chat.ts\"\nimport * as History from \"./internal/history.ts\"\nimport type * as Turns from \"./internal/turns.ts\"\n\n/**\n * Service that runs assistants' chats. An assistant's `toLayer` registers the assistant with it,\n * and the assistant's `submit` and `subscribe` use it.\n *\n * @category services\n * @since 0.1.0\n */\nexport class AssistantEngine extends Context.Service<\n AssistantEngine,\n {\n /** Registers an assistant for as long as the scope is open. */\n readonly register: (registration: Turns.Registration) => Effect.Effect<void, never, Scope.Scope>\n /**\n * Stores an input in one of the assistant's chats. The assistant works on it in the\n * background.\n */\n readonly submit: (\n assistant: string,\n chatId: ChatId,\n input: AssistantEvent.Input | AssistantEvent.StoredAppEvent\n ) => Effect.Effect<void>\n /** Streams the chat's stored entries after the `after` sequence, then live ones. */\n readonly subscribe: (assistant: string, chatId: ChatId, after: number) => Stream.Stream<AssistantEvent.Entry>\n /**\n * Deletes one of the assistant's chats. The chat's log records the deletion, and everything\n * that works in the chat stops.\n */\n readonly delete: (assistant: string, chatId: ChatId) => Effect.Effect<void>\n }\n>()(\"effect-assistant/AssistantEngine\") {}\n\nclass ChatKey extends Data.Class<{ readonly assistant: string; readonly chatId: ChatId }> {}\n\nconst notRegistered = (assistant: string) =>\n Effect.die(`The ${assistant} assistant isn't registered: provide its toLayer.`)\n\n/**\n * Layer that runs chats in the current process. Each chat's turns run as fibers, one at a time.\n *\n * **Details**\n *\n * With `ChatLog.layerSql`, the engine resumes chats at startup. When an assistant registers, the\n * engine finds the chats that assistant was working on when the process stopped (a turn cut off\n * partway, or an input that no turn read) and runs a turn for each. Run this engine in one process\n * only, because two processes would run the same chat and write its log at the same time.\n *\n * **Example** (Running chats in memory)\n *\n * ```ts\n * import { Assistant, AssistantEngine, ChatLog } from \"effect-assistant\"\n * import type * as LanguageModel from \"effect/ai/LanguageModel\"\n * import * as Layer from \"effect/Layer\"\n *\n * declare const ModelLive: Layer.Layer<LanguageModel.LanguageModel>\n *\n * const MyAssistant = Assistant.make(\"my-assistant\")\n *\n * const AppLive = MyAssistant.toLayer((turn) => turn.run({ instructions: \"Be brief.\" })).pipe(\n * Layer.provide(ModelLive),\n * Layer.provideMerge(AssistantEngine.layerMemory),\n * Layer.provideMerge(ChatLog.layerMemory)\n * )\n * ```\n *\n * @see {@link layerCluster} for running each chat as a cluster entity\n *\n * @category layers\n * @since 0.1.0\n */\nexport const layerMemory: Layer.Layer<AssistantEngine, never, ChatLog> = Layer.effect(\n AssistantEngine,\n Effect.gen(function* () {\n const context = yield* Effect.context<ChatLog>()\n const log = yield* ChatLog\n const registered = new Map<string, Turns.Registration>()\n // A chat's keep-alive: a hold on its entry, so it stays open while runs or turns work in the background.\n const held = MutableHashMap.empty<ChatKey, Scope.Closeable>()\n\n // One entry per open chat, like a cluster entity: opened when used, closed five minutes after the last use.\n const chats: RcMap.RcMap<ChatKey, Chat.Chat> = yield* RcMap.make({\n lookup: Effect.fnUntraced(function* (key: ChatKey) {\n const registration = registered.get(key.assistant)\n\n if (registration === undefined) return yield* notRegistered(key.assistant)\n\n return yield* Chat.make({\n registration,\n chatId: key.chatId,\n keepAlive: (enabled) => keepAlive(key, enabled)\n }).pipe(Effect.provide(context))\n }),\n idleTimeToLive: \"5 minutes\"\n })\n\n // On: a hold on the chat's entry, in a scope of its own. Off: closing that scope lets the entry go idle.\n const keepAlive = Effect.fnUntraced(function* (key: ChatKey, enabled: boolean) {\n if (enabled) {\n const scope = yield* Scope.make()\n\n yield* Effect.provideService(RcMap.get(chats, key), Scope.Scope, scope)\n\n return MutableHashMap.set(held, key, scope)\n }\n\n const scope = MutableHashMap.get(held, key)\n\n MutableHashMap.remove(held, key)\n\n if (Option.isSome(scope)) yield* Scope.close(scope.value, Exit.void)\n })\n\n const withChat = Effect.fnUntraced(function* <A>(\n assistant: string,\n chatId: ChatId,\n f: (chat: Chat.Chat) => Effect.Effect<A>\n ) {\n const chat = yield* RcMap.get(chats, new ChatKey({ assistant, chatId }))\n\n return yield* f(chat)\n }, Effect.scoped)\n\n // A chat owes a turn when its last turn was cut off, or an input came after.\n const resumeAll = Effect.fnUntraced(function* (registration: Turns.Registration) {\n const { name } = registration.definition\n\n for (const chatId of yield* log.unfinished) {\n const entries = yield* log.read(chatId)\n\n const lastTurn = entries.findLast(({ event }) => Predicate.isTagged(event, \"TurnStarted\"))?.event\n const ours = Predicate.isTagged(lastTurn, \"TurnStarted\") && lastTurn.assistant === name\n\n if (ours && History.needsTurn(entries)) yield* withChat(name, chatId, (chat) => chat.resume)\n }\n })\n\n return AssistantEngine.of({\n register: Effect.fn(\"AssistantEngine.register\")(function* (registration: Turns.Registration) {\n registered.set(registration.definition.name, registration)\n // Chats this assistant was working on when the process stopped: pick them up again.\n yield* Effect.forkScoped(resumeAll(registration))\n }),\n submit: (assistant, chatId, input) => withChat(assistant, chatId, (chat) => chat.submitInBackground(input)),\n // The chat deletes itself, so a chat that's open stops; the entry goes, and the next open finds it deleted.\n delete: Effect.fn(\"AssistantEngine.delete\")(function* (assistant: string, chatId: ChatId) {\n yield* withChat(assistant, chatId, (chat) => chat.delete)\n yield* RcMap.invalidate(chats, new ChatKey({ assistant, chatId }))\n }),\n // The subscriber holds the chat open while it streams.\n subscribe: (assistant, chatId, after) =>\n Stream.unwrap(\n Effect.gen(function* () {\n const chat = yield* RcMap.get(chats, new ChatKey({ assistant, chatId }))\n\n return chat.stream(after)\n })\n )\n })\n })\n)\n\ninterface EntityClient {\n readonly submit: (chatId: ChatId, input: AssistantEvent.Input | AssistantEvent.StoredAppEvent) => Effect.Effect<void>\n readonly subscribe: (chatId: ChatId, after: number) => Stream.Stream<AssistantEvent.Entry>\n readonly delete: (chatId: ChatId) => Effect.Effect<void>\n}\n\nconst Submit = Rpc.make(\"Submit\", {\n payload: { input: Schema.Union([AssistantEvent.Input, AssistantEvent.StoredAppEvent]) }\n}).annotate(ClusterSchema.Persisted, true)\n\nconst Subscribe = Rpc.make(\"Subscribe\", {\n payload: { after: Schema.Int },\n success: AssistantEvent.Entry,\n stream: true\n})\n\nconst Delete = Rpc.make(\"Delete\")\n\nconst chatEntity = (assistant: string) => Entity.make(`Assistant/${assistant}`, [Submit, Subscribe, Delete])\n\ntype EngineClient = Omit<AssistantEngine[\"Service\"], \"register\">\n\n// Entity clients are made on first use, so a process that registers no assistant can still reach its chats.\nconst makeClusterClient = Effect.gen(function* () {\n const context = yield* Effect.context<Sharding.Sharding>()\n const clients = new Map<string, EntityClient>()\n\n const clientOf = Effect.fnUntraced(function* (assistant: string) {\n const known = clients.get(assistant)\n\n if (known !== undefined) return known\n\n const makeClient = yield* chatEntity(assistant).client.pipe(Effect.provide(context))\n\n const client: EntityClient = {\n submit: (chatId, input) => makeClient(chatId).Submit({ input }, { discard: true }).pipe(Effect.orDie),\n subscribe: (chatId, after) => makeClient(chatId).Subscribe({ after }).pipe(Stream.orDie),\n delete: (chatId) => makeClient(chatId).Delete().pipe(Effect.orDie)\n }\n\n clients.set(assistant, client)\n\n return client\n })\n\n const engine: EngineClient = {\n submit: Effect.fn(\"AssistantEngine.submit\")(function* (\n assistant: string,\n chatId: ChatId,\n input: AssistantEvent.Input | AssistantEvent.StoredAppEvent\n ) {\n const client = yield* clientOf(assistant)\n\n yield* client.submit(chatId, input)\n }),\n subscribe: (assistant, chatId, after) =>\n Stream.unwrap(\n Effect.gen(function* () {\n const client = yield* clientOf(assistant)\n\n return client.subscribe(chatId, after)\n })\n ),\n delete: Effect.fn(\"AssistantEngine.delete\")(function* (assistant: string, chatId: ChatId) {\n const client = yield* clientOf(assistant)\n\n yield* client.delete(chatId)\n })\n }\n\n return engine\n})\n\n/**\n * Layer that runs chats as cluster entities, one entity per chat, keyed by chat id.\n *\n * **Details**\n *\n * The layer requires `Sharding`. Use a `SingleRunner` for one process with SQL storage, a runner\n * of the app's cluster, or `TestRunner` in tests.\n *\n * **Example** (Running chats on a single runner with Postgres)\n *\n * ```ts\n * import * as NodeCrypto from \"@effect/platform-node/NodeCrypto\"\n * import * as PgClient from \"@effect/sql-pg/PgClient\"\n * import { Assistant, AssistantEngine, ChatLog } from \"effect-assistant\"\n * import type * as LanguageModel from \"effect/ai/LanguageModel\"\n * import * as SingleRunner from \"effect/cluster/SingleRunner\"\n * import * as Config from \"effect/Config\"\n * import * as Layer from \"effect/Layer\"\n *\n * declare const ModelLive: Layer.Layer<LanguageModel.LanguageModel>\n *\n * const MyAssistant = Assistant.make(\"my-assistant\")\n *\n * const AppLive = MyAssistant.toLayer((turn) => turn.run({ instructions: \"Be brief.\" })).pipe(\n * Layer.provide(ModelLive),\n * Layer.provideMerge(AssistantEngine.layerCluster),\n * Layer.provideMerge(\n * Layer.merge(\n * ChatLog.layerSql,\n * SingleRunner.layer({ runnerStorage: \"sql\" }).pipe(Layer.provide(NodeCrypto.layer))\n * )\n * ),\n * Layer.provide(PgClient.layerConfig({ url: Config.Redacted(\"DATABASE_URL\") }))\n * )\n * ```\n *\n * @see {@link layerMemory} for running chats in one process\n * @see {@link layerClusterClient} for a process that only reaches chats the cluster runs\n *\n * @category layers\n * @since 0.1.0\n */\nexport const layerCluster: Layer.Layer<AssistantEngine, never, ChatLog | Sharding.Sharding> = Layer.effect(\n AssistantEngine,\n Effect.gen(function* () {\n const context = yield* Effect.context<ChatLog | Sharding.Sharding>()\n const sharding = yield* Sharding.Sharding\n\n const register = Effect.fnUntraced(function* (registration: Turns.Registration) {\n const ChatEntity = chatEntity(registration.definition.name)\n\n const handlers = Effect.gen(function* () {\n const address = yield* Entity.CurrentAddress\n const chatId = yield* Schema.decodeUnknownEffect(ChatId)(address.entityId).pipe(Effect.orDie)\n // The entity's own: `Entity.keepAlive` needs its address and keep-alive latch, which is in there too.\n const entityContext = yield* Effect.context<Entity.CurrentAddress | Sharding.Sharding>()\n\n const keepAlive = (enabled: boolean) =>\n Entity.keepAlive(enabled).pipe(\n Effect.provide(entityContext),\n Effect.catchCause((cause) => Effect.logWarning(\"assistant chat keep-alive failed\", cause))\n )\n\n // A keep-alive left on from before a restart (by a run cut off) has done its job: it started the entity.\n yield* keepAlive(false)\n\n const chat = yield* Chat.make({ registration, chatId, keepAlive })\n yield* chat.resume\n\n return ChatEntity.of({\n // The turns run inside the request, so a crash leaves it unprocessed and the cluster redelivers it.\n Submit: (request) => chat.submit(request.payload.input),\n Subscribe: (request) => chat.stream(request.payload.after),\n Delete: () => chat.delete\n })\n })\n\n yield* sharding.registerEntity(ChatEntity, handlers.pipe(Effect.provide(context)), {\n maxIdleTime: \"5 minutes\",\n concurrency: \"unbounded\"\n })\n })\n\n const client = yield* makeClusterClient\n\n return AssistantEngine.of({\n register: (registration) => register(registration).pipe(Effect.provide(context)),\n ...client\n })\n })\n)\n\n/**\n * Layer that reaches chats running as cluster entities elsewhere, and runs none itself.\n *\n * **When to use**\n *\n * Use it in a process that only submits to, subscribes to, and deletes chats that the cluster's\n * runners run with {@link layerCluster}, such as a web server that provides an assistant's\n * `layerClient`. It needs no `ChatLog`, since the runners store the chats.\n *\n * **Details**\n *\n * The layer requires `Sharding`. An assistant's `toLayer` can't register with it: it dies, since\n * no chats run in this process.\n *\n * **Example** (A web server in front of the cluster)\n *\n * ```ts\n * import { Assistant, AssistantEngine } from \"effect-assistant\"\n * import type * as Sharding from \"effect/cluster/Sharding\"\n * import * as Layer from \"effect/Layer\"\n *\n * declare const ShardingLive: Layer.Layer<Sharding.Sharding>\n *\n * export const MyAssistant = Assistant.make(\"my-assistant\")\n *\n * const MyAssistantClient = MyAssistant.layerClient.pipe(\n * Layer.provide(AssistantEngine.layerClusterClient),\n * Layer.provide(ShardingLive)\n * )\n * ```\n *\n * @see {@link layerCluster} for the runners that run the chats\n *\n * @category layers\n * @since 0.3.0\n */\nexport const layerClusterClient: Layer.Layer<AssistantEngine, never, Sharding.Sharding> = Layer.effect(\n AssistantEngine,\n Effect.gen(function* () {\n const client = yield* makeClusterClient\n\n return AssistantEngine.of({\n register: (registration) =>\n Effect.die(\n `The ${registration.definition.name} assistant can't register with AssistantEngine.layerClusterClient, which runs no chats: provide AssistantEngine.layerCluster where its toLayer runs.`\n ),\n ...client\n })\n })\n)\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AssistantEngine.js","names":["Chat.make","History.needsTurn","AssistantEvent.Input","AssistantEvent.StoredAppEvent","AssistantEvent.Entry"],"sources":["../src/AssistantEngine.ts"],"sourcesContent":["/**\n * This module provides the `AssistantEngine` service, which runs assistants' chats, and its\n * implementations.\n *\n * An assistant's `toLayer` registers the assistant with the engine, and the assistant's `submit`\n * and `subscribe` go through it. Pick one engine for the app, as with a workflow engine.\n * {@link layerMemory} runs chats in the process, for tests and single-process apps.\n * {@link layerCluster} runs each chat as a cluster entity, so a turn cut off by a crash resumes, and\n * {@link layerClusterClient} reaches those chats from a process that runs none.\n *\n * @since 0.1.0\n */\nimport * as ClusterSchema from \"effect/cluster/ClusterSchema\"\nimport * as Entity from \"effect/cluster/Entity\"\nimport * as Sharding from \"effect/cluster/Sharding\"\nimport * as Context from \"effect/Context\"\nimport * as Data from \"effect/Data\"\nimport * as Effect from \"effect/Effect\"\nimport * as Exit from \"effect/Exit\"\nimport * as Layer from \"effect/Layer\"\nimport * as MutableHashMap from \"effect/MutableHashMap\"\nimport * as Option from \"effect/Option\"\nimport * as Predicate from \"effect/Predicate\"\nimport * as RcMap from \"effect/RcMap\"\nimport * as Rpc from \"effect/rpc/Rpc\"\nimport * as Schema from \"effect/Schema\"\nimport * as Scope from \"effect/Scope\"\nimport * as Stream from \"effect/Stream\"\nimport * as AssistantEvent from \"./AssistantEvent.ts\"\nimport { ChatId } from \"./ChatId.ts\"\nimport { ChatLog } from \"./ChatLog.ts\"\nimport * as Chat from \"./internal/chat.ts\"\nimport * as History from \"./internal/history.ts\"\nimport type * as Turns from \"./internal/turns.ts\"\n\n/**\n * Service that runs assistants' chats. An assistant's `toLayer` registers the assistant with it,\n * and the assistant's `submit` and `subscribe` use it.\n *\n * @category services\n * @since 0.1.0\n */\nexport class AssistantEngine extends Context.Service<\n AssistantEngine,\n {\n /** Registers an assistant for as long as the scope is open. */\n readonly register: (registration: Turns.Registration) => Effect.Effect<void, never, Scope.Scope>\n /**\n * Stores an input in one of the assistant's chats. The assistant works on it in the\n * background.\n */\n readonly submit: (\n assistant: string,\n chatId: ChatId,\n input: AssistantEvent.Input | AssistantEvent.StoredAppEvent\n ) => Effect.Effect<void>\n /** Streams the chat's stored entries after the `after` sequence, then live ones. */\n readonly subscribe: (assistant: string, chatId: ChatId, after: number) => Stream.Stream<AssistantEvent.Entry>\n /**\n * Deletes one of the assistant's chats. The chat's log records the deletion, and everything\n * that works in the chat stops.\n */\n readonly delete: (assistant: string, chatId: ChatId) => Effect.Effect<void>\n }\n>()(\"effect-assistant/AssistantEngine\") {}\n\nclass ChatKey extends Data.Class<{ readonly assistant: string; readonly chatId: ChatId }> {}\n\nconst notRegistered = (assistant: string) =>\n Effect.die(`The ${assistant} assistant isn't registered: provide its toLayer.`)\n\n/**\n * Layer that runs chats in the current process. Each chat's turns run as fibers, one at a time.\n *\n * **Details**\n *\n * With `ChatLog.layerSql()`, the engine resumes chats at startup. When an assistant registers, the\n * engine finds the chats that assistant was working on when the process stopped (a turn cut off\n * partway, or an input that no turn read) and runs a turn for each. Run this engine in one process\n * only, because two processes would run the same chat and write its log at the same time.\n *\n * **Example** (Running chats in memory)\n *\n * ```ts\n * import { Assistant, AssistantEngine, ChatLog } from \"effect-assistant\"\n * import type * as LanguageModel from \"effect/ai/LanguageModel\"\n * import * as Layer from \"effect/Layer\"\n *\n * declare const ModelLive: Layer.Layer<LanguageModel.LanguageModel>\n *\n * const MyAssistant = Assistant.make(\"my-assistant\")\n *\n * const AppLive = MyAssistant.toLayer((turn) => turn.run({ instructions: \"Be brief.\" })).pipe(\n * Layer.provide(ModelLive),\n * Layer.provideMerge(AssistantEngine.layerMemory),\n * Layer.provideMerge(ChatLog.layerMemory)\n * )\n * ```\n *\n * @see {@link layerCluster} for running each chat as a cluster entity\n *\n * @category layers\n * @since 0.1.0\n */\nexport const layerMemory: Layer.Layer<AssistantEngine, never, ChatLog> = Layer.effect(\n AssistantEngine,\n Effect.gen(function* () {\n const context = yield* Effect.context<ChatLog>()\n const log = yield* ChatLog\n const registered = new Map<string, Turns.Registration>()\n // A chat's keep-alive: a hold on its entry, so it stays open while runs or turns work in the background.\n const held = MutableHashMap.empty<ChatKey, Scope.Closeable>()\n\n // One entry per open chat, like a cluster entity: opened when used, closed five minutes after the last use.\n const chats: RcMap.RcMap<ChatKey, Chat.Chat> = yield* RcMap.make({\n lookup: Effect.fnUntraced(function* (key: ChatKey) {\n const registration = registered.get(key.assistant)\n\n if (registration === undefined) return yield* notRegistered(key.assistant)\n\n return yield* Chat.make({\n registration,\n chatId: key.chatId,\n keepAlive: (enabled) => keepAlive(key, enabled)\n }).pipe(Effect.provide(context))\n }),\n idleTimeToLive: \"5 minutes\"\n })\n\n // On: a hold on the chat's entry, in a scope of its own. Off: closing that scope lets the entry go idle.\n const keepAlive = Effect.fnUntraced(function* (key: ChatKey, enabled: boolean) {\n if (enabled) {\n const scope = yield* Scope.make()\n\n yield* Effect.provideService(RcMap.get(chats, key), Scope.Scope, scope)\n\n return MutableHashMap.set(held, key, scope)\n }\n\n const scope = MutableHashMap.get(held, key)\n\n MutableHashMap.remove(held, key)\n\n if (Option.isSome(scope)) yield* Scope.close(scope.value, Exit.void)\n })\n\n const withChat = Effect.fnUntraced(function* <A>(\n assistant: string,\n chatId: ChatId,\n f: (chat: Chat.Chat) => Effect.Effect<A>\n ) {\n const chat = yield* RcMap.get(chats, new ChatKey({ assistant, chatId }))\n\n return yield* f(chat)\n }, Effect.scoped)\n\n // A chat owes a turn when its last turn was cut off, or an input came after.\n const resumeAll = Effect.fnUntraced(function* (registration: Turns.Registration) {\n const { name } = registration.definition\n\n for (const chatId of yield* log.unfinished) {\n const entries = yield* log.read(chatId)\n\n const lastTurn = entries.findLast(({ event }) => Predicate.isTagged(event, \"TurnStarted\"))?.event\n const ours = Predicate.isTagged(lastTurn, \"TurnStarted\") && lastTurn.assistant === name\n\n if (ours && History.needsTurn(entries)) yield* withChat(name, chatId, (chat) => chat.resume)\n }\n })\n\n return AssistantEngine.of({\n register: Effect.fn(\"AssistantEngine.register\")(function* (registration: Turns.Registration) {\n registered.set(registration.definition.name, registration)\n // Chats this assistant was working on when the process stopped: pick them up again.\n yield* Effect.forkScoped(resumeAll(registration))\n }),\n submit: (assistant, chatId, input) => withChat(assistant, chatId, (chat) => chat.submitInBackground(input)),\n // The chat deletes itself, so a chat that's open stops; the entry goes, and the next open finds it deleted.\n delete: Effect.fn(\"AssistantEngine.delete\")(function* (assistant: string, chatId: ChatId) {\n yield* withChat(assistant, chatId, (chat) => chat.delete)\n yield* RcMap.invalidate(chats, new ChatKey({ assistant, chatId }))\n }),\n // The subscriber holds the chat open while it streams.\n subscribe: (assistant, chatId, after) =>\n Stream.unwrap(\n Effect.gen(function* () {\n const chat = yield* RcMap.get(chats, new ChatKey({ assistant, chatId }))\n\n return chat.stream(after)\n })\n )\n })\n })\n)\n\ninterface EntityClient {\n readonly submit: (chatId: ChatId, input: AssistantEvent.Input | AssistantEvent.StoredAppEvent) => Effect.Effect<void>\n readonly subscribe: (chatId: ChatId, after: number) => Stream.Stream<AssistantEvent.Entry>\n readonly delete: (chatId: ChatId) => Effect.Effect<void>\n}\n\nconst Submit = Rpc.make(\"Submit\", {\n payload: { input: Schema.Union([AssistantEvent.Input, AssistantEvent.StoredAppEvent]) }\n}).annotate(ClusterSchema.Persisted, true)\n\nconst Subscribe = Rpc.make(\"Subscribe\", {\n payload: { after: Schema.Int },\n success: AssistantEvent.Entry,\n stream: true\n})\n\nconst Delete = Rpc.make(\"Delete\")\n\nconst chatEntity = (assistant: string) => Entity.make(`Assistant/${assistant}`, [Submit, Subscribe, Delete])\n\ntype EngineClient = Omit<AssistantEngine[\"Service\"], \"register\">\n\n// Entity clients are made on first use, so a process that registers no assistant can still reach its chats.\nconst makeClusterClient = Effect.gen(function* () {\n const context = yield* Effect.context<Sharding.Sharding>()\n const clients = new Map<string, EntityClient>()\n\n const clientOf = Effect.fnUntraced(function* (assistant: string) {\n const known = clients.get(assistant)\n\n if (known !== undefined) return known\n\n const makeClient = yield* chatEntity(assistant).client.pipe(Effect.provide(context))\n\n const client: EntityClient = {\n submit: (chatId, input) => makeClient(chatId).Submit({ input }, { discard: true }).pipe(Effect.orDie),\n subscribe: (chatId, after) => makeClient(chatId).Subscribe({ after }).pipe(Stream.orDie),\n delete: (chatId) => makeClient(chatId).Delete().pipe(Effect.orDie)\n }\n\n clients.set(assistant, client)\n\n return client\n })\n\n const engine: EngineClient = {\n submit: Effect.fn(\"AssistantEngine.submit\")(function* (\n assistant: string,\n chatId: ChatId,\n input: AssistantEvent.Input | AssistantEvent.StoredAppEvent\n ) {\n const client = yield* clientOf(assistant)\n\n yield* client.submit(chatId, input)\n }),\n subscribe: (assistant, chatId, after) =>\n Stream.unwrap(\n Effect.gen(function* () {\n const client = yield* clientOf(assistant)\n\n return client.subscribe(chatId, after)\n })\n ),\n delete: Effect.fn(\"AssistantEngine.delete\")(function* (assistant: string, chatId: ChatId) {\n const client = yield* clientOf(assistant)\n\n yield* client.delete(chatId)\n })\n }\n\n return engine\n})\n\n/**\n * Layer that runs chats as cluster entities, one entity per chat, keyed by chat id.\n *\n * **Details**\n *\n * The layer requires `Sharding`. Use a `SingleRunner` for one process with SQL storage, a runner\n * of the app's cluster, or `TestRunner` in tests.\n *\n * **Example** (Running chats on a single runner with Postgres)\n *\n * ```ts\n * import * as NodeCrypto from \"@effect/platform-node/NodeCrypto\"\n * import * as PgClient from \"@effect/sql-pg/PgClient\"\n * import { Assistant, AssistantEngine, ChatLog } from \"effect-assistant\"\n * import type * as LanguageModel from \"effect/ai/LanguageModel\"\n * import * as SingleRunner from \"effect/cluster/SingleRunner\"\n * import * as Config from \"effect/Config\"\n * import * as Layer from \"effect/Layer\"\n *\n * declare const ModelLive: Layer.Layer<LanguageModel.LanguageModel>\n *\n * const MyAssistant = Assistant.make(\"my-assistant\")\n *\n * const AppLive = MyAssistant.toLayer((turn) => turn.run({ instructions: \"Be brief.\" })).pipe(\n * Layer.provide(ModelLive),\n * Layer.provideMerge(AssistantEngine.layerCluster),\n * Layer.provideMerge(\n * Layer.merge(\n * ChatLog.layerSql(),\n * SingleRunner.layer({ runnerStorage: \"sql\" }).pipe(Layer.provide(NodeCrypto.layer))\n * )\n * ),\n * Layer.provide(PgClient.layerConfig({ url: Config.Redacted(\"DATABASE_URL\") }))\n * )\n * ```\n *\n * @see {@link layerMemory} for running chats in one process\n * @see {@link layerClusterClient} for a process that only reaches chats the cluster runs\n *\n * @category layers\n * @since 0.1.0\n */\nexport const layerCluster: Layer.Layer<AssistantEngine, never, ChatLog | Sharding.Sharding> = Layer.effect(\n AssistantEngine,\n Effect.gen(function* () {\n const context = yield* Effect.context<ChatLog | Sharding.Sharding>()\n const sharding = yield* Sharding.Sharding\n\n const register = Effect.fnUntraced(function* (registration: Turns.Registration) {\n const ChatEntity = chatEntity(registration.definition.name)\n\n const handlers = Effect.gen(function* () {\n const address = yield* Entity.CurrentAddress\n const chatId = yield* Schema.decodeUnknownEffect(ChatId)(address.entityId).pipe(Effect.orDie)\n // The entity's own: `Entity.keepAlive` needs its address and keep-alive latch, which is in there too.\n const entityContext = yield* Effect.context<Entity.CurrentAddress | Sharding.Sharding>()\n\n const keepAlive = (enabled: boolean) =>\n Entity.keepAlive(enabled).pipe(\n Effect.provide(entityContext),\n Effect.catchCause((cause) => Effect.logWarning(\"assistant chat keep-alive failed\", cause))\n )\n\n // A keep-alive left on from before a restart (by a run cut off) has done its job: it started the entity.\n yield* keepAlive(false)\n\n const chat = yield* Chat.make({ registration, chatId, keepAlive })\n yield* chat.resume\n\n return ChatEntity.of({\n // The turns run inside the request, so a crash leaves it unprocessed and the cluster redelivers it.\n Submit: (request) => chat.submit(request.payload.input),\n Subscribe: (request) => chat.stream(request.payload.after),\n Delete: () => chat.delete\n })\n })\n\n yield* sharding.registerEntity(ChatEntity, handlers.pipe(Effect.provide(context)), {\n maxIdleTime: \"5 minutes\",\n concurrency: \"unbounded\"\n })\n })\n\n const client = yield* makeClusterClient\n\n return AssistantEngine.of({\n register: (registration) => register(registration).pipe(Effect.provide(context)),\n ...client\n })\n })\n)\n\n/**\n * Layer that reaches chats running as cluster entities elsewhere, and runs none itself.\n *\n * **When to use**\n *\n * Use it in a process that only submits to, subscribes to, and deletes chats that the cluster's\n * runners run with {@link layerCluster}, such as a web server that provides an assistant's\n * `layerClient`. It needs no `ChatLog`, since the runners store the chats.\n *\n * **Details**\n *\n * The layer requires `Sharding`. An assistant's `toLayer` can't register with it: it dies, since\n * no chats run in this process.\n *\n * **Example** (A web server in front of the cluster)\n *\n * ```ts\n * import { Assistant, AssistantEngine } from \"effect-assistant\"\n * import type * as Sharding from \"effect/cluster/Sharding\"\n * import * as Layer from \"effect/Layer\"\n *\n * declare const ShardingLive: Layer.Layer<Sharding.Sharding>\n *\n * export const MyAssistant = Assistant.make(\"my-assistant\")\n *\n * const MyAssistantClient = MyAssistant.layerClient.pipe(\n * Layer.provide(AssistantEngine.layerClusterClient),\n * Layer.provide(ShardingLive)\n * )\n * ```\n *\n * @see {@link layerCluster} for the runners that run the chats\n *\n * @category layers\n * @since 0.3.0\n */\nexport const layerClusterClient: Layer.Layer<AssistantEngine, never, Sharding.Sharding> = Layer.effect(\n AssistantEngine,\n Effect.gen(function* () {\n const client = yield* makeClusterClient\n\n return AssistantEngine.of({\n register: (registration) =>\n Effect.die(\n `The ${registration.definition.name} assistant can't register with AssistantEngine.layerClusterClient, which runs no chats: provide AssistantEngine.layerCluster where its toLayer runs.`\n ),\n ...client\n })\n })\n)\n"],"mappings":"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"}
|
package/dist/ChatLog.d.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { ChatId } from "./ChatId.js";
|
|
2
|
-
import { AssistantEvent, AssistantEvent_d_exports, Entry, StoredAppEvent } from "./AssistantEvent.js";
|
|
2
|
+
import { ApprovalDecision, ApprovalRequested, ApprovalsExpired, AssistantEvent, AssistantEvent_d_exports, AssistantMessage, BlockAnswer, BlocksClosed, ChatDeleted, Entry, ModelOutput, SpecialistFinished, SpecialistStarted, StoredAppEvent, ToolResult, ToolStarted, TurnCompleted, TurnFailed, TurnStarted, UserMessage } from "./AssistantEvent.js";
|
|
3
3
|
import * as Context from "effect/Context";
|
|
4
4
|
import * as Effect from "effect/Effect";
|
|
5
5
|
import * as Layer from "effect/Layer";
|
|
6
|
+
import * as Schema from "effect/Schema";
|
|
6
7
|
import * as SqlClient from "effect/sql/SqlClient";
|
|
7
8
|
declare namespace ChatLog_d_exports {
|
|
8
|
-
export { ChatLog, ReadOptions, Storage, layerMemory, layerSql, make };
|
|
9
|
+
export { ChatLog, EventJson, ReadOptions, SqlOptions, Storage, layerMemory, layerSql, make };
|
|
9
10
|
}
|
|
10
11
|
declare const ChatLog_base: Context.ServiceClass<ChatLog, "effect-assistant/ChatLog", {
|
|
11
12
|
/** Appends events to a chat and returns them as stored entries. */
|
|
@@ -68,12 +69,38 @@ export declare const make: (storage: Storage) => ChatLog["Service"];
|
|
|
68
69
|
*/
|
|
69
70
|
export declare const layerMemory: Layer.Layer<ChatLog>;
|
|
70
71
|
/**
|
|
71
|
-
*
|
|
72
|
+
* Schema for an event as the log stores it: JSON text.
|
|
73
|
+
*
|
|
74
|
+
* **Details**
|
|
75
|
+
*
|
|
76
|
+
* `layerSql` stores events with it. A storage of your own, for `make`, can use it to store the
|
|
77
|
+
* same text.
|
|
78
|
+
*
|
|
79
|
+
* @category schemas
|
|
80
|
+
* @since 0.8.0
|
|
81
|
+
*/
|
|
82
|
+
export declare const EventJson: Schema.fromJsonString<Schema.Union<readonly [Schema.Union<readonly [typeof UserMessage, typeof BlockAnswer, typeof AssistantMessage, typeof ModelOutput, typeof ToolStarted, typeof ToolResult, typeof TurnStarted, typeof TurnCompleted, typeof TurnFailed, typeof SpecialistStarted, typeof SpecialistFinished, typeof BlocksClosed, typeof ApprovalRequested, typeof ApprovalDecision, typeof ApprovalsExpired, typeof ChatDeleted]>, Schema.StructWithRest<Schema.Struct<{
|
|
83
|
+
readonly _tag: Schema.brand<Schema.String, "~effect-assistant/AppTag">;
|
|
84
|
+
readonly "~effect-assistant/input": Schema.optionalKey<Schema.Literal<true>>;
|
|
85
|
+
}>, readonly [Schema.$Record<Schema.String, Schema.Codec<Schema.Json, Schema.Json, never, never>>]>]>>;
|
|
86
|
+
/**
|
|
87
|
+
* Represents the options accepted by `layerSql`.
|
|
88
|
+
*
|
|
89
|
+
* @category models
|
|
90
|
+
* @since 0.8.0
|
|
91
|
+
*/
|
|
92
|
+
export interface SqlOptions {
|
|
93
|
+
/** The table's name. Defaults to `chat_log`. */
|
|
94
|
+
readonly table?: string | undefined;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Layer that stores chats in a SQL table, `chat_log` unless `table` names another.
|
|
72
98
|
*
|
|
73
99
|
* **Details**
|
|
74
100
|
*
|
|
75
101
|
* The table must already exist. It has the columns `chat_id`, `sequence`, `at`, and `event`, with the
|
|
76
|
-
* primary key `(chat_id, sequence)`. The layer requires a `SqlClient`.
|
|
102
|
+
* primary key `(chat_id, sequence)`. The layer requires a `SqlClient`. In MySQL, `event` needs
|
|
103
|
+
* `LONGTEXT`, since a tool's result can be larger than `TEXT` holds.
|
|
77
104
|
*
|
|
78
105
|
* **Example** (Storing chats in Postgres)
|
|
79
106
|
*
|
|
@@ -83,7 +110,7 @@ export declare const layerMemory: Layer.Layer<ChatLog>;
|
|
|
83
110
|
* import * as Config from "effect/Config"
|
|
84
111
|
* import * as Layer from "effect/Layer"
|
|
85
112
|
*
|
|
86
|
-
* const ChatLogLive = ChatLog.layerSql.pipe(
|
|
113
|
+
* const ChatLogLive = ChatLog.layerSql().pipe(
|
|
87
114
|
* Layer.provide(PgClient.layerConfig({ url: Config.Redacted("DATABASE_URL") }))
|
|
88
115
|
* )
|
|
89
116
|
* ```
|
|
@@ -103,7 +130,7 @@ export declare const layerMemory: Layer.Layer<ChatLog>;
|
|
|
103
130
|
* @category layers
|
|
104
131
|
* @since 0.1.0
|
|
105
132
|
*/
|
|
106
|
-
export declare const layerSql: Layer.Layer<ChatLog, never, SqlClient.SqlClient>;
|
|
133
|
+
export declare const layerSql: (options?: SqlOptions) => Layer.Layer<ChatLog, never, SqlClient.SqlClient>;
|
|
107
134
|
//#endregion
|
|
108
135
|
export { ChatLog_d_exports };
|
|
109
136
|
//# sourceMappingURL=ChatLog.d.ts.map
|
package/dist/ChatLog.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ChatLog.d.ts","names":[],"sources":["../src/ChatLog.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"ChatLog.d.ts","names":[],"sources":["../src/ChatLog.ts"],"mappings":";;;;;;;;;;;;WA+BqB,SACf,QAAQ,QACR,QAAQ,cAAc,iBAAgC,oBACnD,OAAO,OAAO,cAAc;;WAElB,OAAC,QAAQ,QAAQ,UAAU,gBAAgB,OAAO,OAAO,cAAc;;;;;WAKjE,YAAA,OAAO,OAAO,cAAc;;;;;;;;qBAdxC,gBAAgB;;;;;;;iBAwBZ;;WAEN;;;;;;;;;;;;iBAaM;WACN,OAAO,QAAQ,QAAQ,UAAU,gBAAgB,OAAO,OAAO,cAAc;WAC7E,eAAe,QAAQ,WAAW,OAAO;WACzC,QAAQ,QAAQ,QAAQ,SAAS,cAAc,WAA0B,OAAO;;WAEhF,aAAa,OAAO,OAAO,cAAc;;;;;;;;;qBAUvC,OAAI,SAAa,YAAU;;;;;;;qBAoC3B,aAAa,MAAM,MAAM;;;;;;;;;;;;qBAsCzB,WAAS,OAAA,eAAA,OAAA,gBAAA,OAAA,uBAAA,oBAAA,oBAAA,yBAAA,oBAAA,oBAAA,mBAAA,oBAAA,sBAAA,mBAAA,0BAAA,2BAAA,qBAAA,0BAAA,yBAAA,yBAAA,eAAA,OAAA,eAAA,OAAA;;;cAErB,OAAA,QAAA,OAAA,QAAA,OAAA,MAAA,OAAA,MAAA,OAAA;;;;;;;iBAQgB;;WAEN;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAiDE,WAAQ,UAAc,eAAa,MAAM,MAAM,gBAAgB,UAAU"}
|
package/dist/ChatLog.js
CHANGED
|
@@ -22,6 +22,7 @@ import * as SqlClient from "effect/sql/SqlClient";
|
|
|
22
22
|
*/
|
|
23
23
|
var ChatLog_exports = /* @__PURE__ */ __exportAll({
|
|
24
24
|
ChatLog: () => ChatLog,
|
|
25
|
+
EventJson: () => EventJson,
|
|
25
26
|
layerMemory: () => layerMemory,
|
|
26
27
|
layerSql: () => layerSql,
|
|
27
28
|
make: () => make
|
|
@@ -80,6 +81,17 @@ const layerMemory = Layer.effect(ChatLog, Effect.gen(function* () {
|
|
|
80
81
|
})
|
|
81
82
|
});
|
|
82
83
|
}));
|
|
84
|
+
/**
|
|
85
|
+
* Schema for an event as the log stores it: JSON text.
|
|
86
|
+
*
|
|
87
|
+
* **Details**
|
|
88
|
+
*
|
|
89
|
+
* `layerSql` stores events with it. A storage of your own, for `make`, can use it to store the
|
|
90
|
+
* same text.
|
|
91
|
+
*
|
|
92
|
+
* @category schemas
|
|
93
|
+
* @since 0.8.0
|
|
94
|
+
*/
|
|
83
95
|
const EventJson = Schema.fromJsonString(Schema.Union([AssistantEvent, StoredAppEvent]));
|
|
84
96
|
const SqlNumber = Schema.Union([Schema.Finite, Schema.FiniteFromString]);
|
|
85
97
|
const Row = Schema.Struct({
|
|
@@ -90,12 +102,13 @@ const Row = Schema.Struct({
|
|
|
90
102
|
});
|
|
91
103
|
const decodeLastSequence = Schema.decodeUnknownEffect(Schema.Array(Schema.Struct({ sequence: Schema.NullOr(SqlNumber) })));
|
|
92
104
|
/**
|
|
93
|
-
* Layer that stores chats in a SQL table
|
|
105
|
+
* Layer that stores chats in a SQL table, `chat_log` unless `table` names another.
|
|
94
106
|
*
|
|
95
107
|
* **Details**
|
|
96
108
|
*
|
|
97
109
|
* The table must already exist. It has the columns `chat_id`, `sequence`, `at`, and `event`, with the
|
|
98
|
-
* primary key `(chat_id, sequence)`. The layer requires a `SqlClient`.
|
|
110
|
+
* primary key `(chat_id, sequence)`. The layer requires a `SqlClient`. In MySQL, `event` needs
|
|
111
|
+
* `LONGTEXT`, since a tool's result can be larger than `TEXT` holds.
|
|
99
112
|
*
|
|
100
113
|
* **Example** (Storing chats in Postgres)
|
|
101
114
|
*
|
|
@@ -105,7 +118,7 @@ const decodeLastSequence = Schema.decodeUnknownEffect(Schema.Array(Schema.Struct
|
|
|
105
118
|
* import * as Config from "effect/Config"
|
|
106
119
|
* import * as Layer from "effect/Layer"
|
|
107
120
|
*
|
|
108
|
-
* const ChatLogLive = ChatLog.layerSql.pipe(
|
|
121
|
+
* const ChatLogLive = ChatLog.layerSql().pipe(
|
|
109
122
|
* Layer.provide(PgClient.layerConfig({ url: Config.Redacted("DATABASE_URL") }))
|
|
110
123
|
* )
|
|
111
124
|
* ```
|
|
@@ -125,8 +138,9 @@ const decodeLastSequence = Schema.decodeUnknownEffect(Schema.Array(Schema.Struct
|
|
|
125
138
|
* @category layers
|
|
126
139
|
* @since 0.1.0
|
|
127
140
|
*/
|
|
128
|
-
const layerSql = Layer.effect(ChatLog, Effect.gen(function* () {
|
|
141
|
+
const layerSql = (options) => Layer.effect(ChatLog, Effect.gen(function* () {
|
|
129
142
|
const sql = (yield* SqlClient.SqlClient).withoutTransforms();
|
|
143
|
+
const table = sql(options?.table ?? "chat_log");
|
|
130
144
|
const toEntries = Effect.fnUntraced(function* (rows) {
|
|
131
145
|
return (yield* Schema.decodeUnknownEffect(Schema.Array(Row))(rows)).map((row) => Entry.make({
|
|
132
146
|
chatId: row.chat_id,
|
|
@@ -139,13 +153,13 @@ const layerSql = Layer.effect(ChatLog, Effect.gen(function* () {
|
|
|
139
153
|
return make({
|
|
140
154
|
read: Effect.fnUntraced(function* (chatId, options) {
|
|
141
155
|
const rows = yield* sql`
|
|
142
|
-
SELECT chat_id, sequence, at, event FROM
|
|
156
|
+
SELECT chat_id, sequence, at, event FROM ${table} WHERE chat_id = ${chatId} AND sequence > ${options?.after ?? 0} ORDER BY sequence
|
|
143
157
|
`;
|
|
144
158
|
return yield* toEntries(rows);
|
|
145
159
|
}, Effect.orDie),
|
|
146
160
|
lastSequence: Effect.fnUntraced(function* (chatId) {
|
|
147
161
|
const result = yield* sql`
|
|
148
|
-
SELECT MAX(sequence) AS sequence FROM
|
|
162
|
+
SELECT MAX(sequence) AS sequence FROM ${table} WHERE chat_id = ${chatId}
|
|
149
163
|
`;
|
|
150
164
|
return (yield* decodeLastSequence(result))[0]?.sequence ?? 0;
|
|
151
165
|
}, Effect.orDie),
|
|
@@ -159,12 +173,12 @@ const layerSql = Layer.effect(ChatLog, Effect.gen(function* () {
|
|
|
159
173
|
event
|
|
160
174
|
};
|
|
161
175
|
}));
|
|
162
|
-
yield* sql`INSERT INTO
|
|
176
|
+
yield* sql`INSERT INTO ${table} ${sql.insert(rows)}`;
|
|
163
177
|
}, Effect.orDie),
|
|
164
178
|
lastEntries: Effect.gen(function* () {
|
|
165
179
|
const rows = yield* sql`
|
|
166
|
-
SELECT e.chat_id, e.sequence, e.at, e.event FROM
|
|
167
|
-
JOIN (SELECT chat_id, MAX(sequence) AS sequence FROM
|
|
180
|
+
SELECT e.chat_id, e.sequence, e.at, e.event FROM ${table} e
|
|
181
|
+
JOIN (SELECT chat_id, MAX(sequence) AS sequence FROM ${table} GROUP BY chat_id) last
|
|
168
182
|
ON last.chat_id = e.chat_id AND last.sequence = e.sequence
|
|
169
183
|
`;
|
|
170
184
|
return yield* toEntries(rows);
|
|
@@ -172,6 +186,6 @@ const layerSql = Layer.effect(ChatLog, Effect.gen(function* () {
|
|
|
172
186
|
});
|
|
173
187
|
}));
|
|
174
188
|
//#endregion
|
|
175
|
-
export { ChatLog, ChatLog_exports, layerMemory, layerSql, make };
|
|
189
|
+
export { ChatLog, ChatLog_exports, EventJson, layerMemory, layerSql, make };
|
|
176
190
|
|
|
177
191
|
//# sourceMappingURL=ChatLog.js.map
|
package/dist/ChatLog.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ChatLog.js","names":["AssistantEvent.isTurnEnd","AssistantEvent.AssistantEvent","AssistantEvent.StoredAppEvent"],"sources":["../src/ChatLog.ts"],"sourcesContent":["/**\n * This module provides the `ChatLog` service, which stores each chat as an append-only list of\n * `AssistantEvent`s.\n *\n * A log is built on a {@link Storage}. {@link layerMemory} keeps every chat in memory, for tests\n * and local runs, and {@link layerSql} stores chats in a SQL table so they survive restarts. To\n * use another store, implement a `Storage` and pass it to {@link make}.\n *\n * @since 0.1.0\n */\nimport * as Clock from \"effect/Clock\"\nimport * as Context from \"effect/Context\"\nimport * as Effect from \"effect/Effect\"\nimport * as Layer from \"effect/Layer\"\nimport * as Predicate from \"effect/Predicate\"\nimport * as Ref from \"effect/Ref\"\nimport * as Schema from \"effect/Schema\"\nimport * as SqlClient from \"effect/sql/SqlClient\"\nimport * as AssistantEvent from \"./AssistantEvent.ts\"\nimport { ChatId } from \"./ChatId.ts\"\n\n/**\n * Service that stores each chat's events as an ordered list of entries.\n *\n * @category services\n * @since 0.1.0\n */\nexport class ChatLog extends Context.Service<\n ChatLog,\n {\n /** Appends events to a chat and returns them as stored entries. */\n readonly append: (\n chatId: ChatId,\n events: ReadonlyArray<AssistantEvent.AssistantEvent | AssistantEvent.StoredAppEvent>\n ) => Effect.Effect<ReadonlyArray<AssistantEvent.Entry>>\n /** Reads a chat's stored entries in order, either all of them or only those after a `sequence`. */\n readonly read: (chatId: ChatId, options?: ReadOptions) => Effect.Effect<ReadonlyArray<AssistantEvent.Entry>>\n /**\n * Returns the chats whose last entry ends neither a turn nor the chat. After a restart, these\n * are the chats that may still owe a turn.\n */\n readonly unfinished: Effect.Effect<ReadonlyArray<ChatId>>\n }\n>()(\"effect-assistant/ChatLog\") {}\n\n/**\n * Represents the options that select which of a chat's entries to read.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface ReadOptions {\n /** Reads only the entries after this `sequence`. */\n readonly after?: number | undefined\n}\n\n/**\n * Represents the storage that a `ChatLog` is built on.\n *\n * A storage reads a chat's entries, finds a chat's last `sequence`, writes new entries, and finds every\n * chat's last entry. Entries are never removed, so a deleted chat keeps its log, which ends in\n * `ChatDeleted`.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface Storage {\n readonly read: (chatId: ChatId, options?: ReadOptions) => Effect.Effect<ReadonlyArray<AssistantEvent.Entry>>\n readonly lastSequence: (chatId: ChatId) => Effect.Effect<number>\n readonly write: (chatId: ChatId, entries: ReadonlyArray<AssistantEvent.Entry>) => Effect.Effect<void>\n /** Returns the last entry of every chat. */\n readonly lastEntries: Effect.Effect<ReadonlyArray<AssistantEvent.Entry>>\n}\n\n/**\n * Creates a `ChatLog` from a {@link Storage}. `append` numbers new entries after the chat's last\n * `sequence` and stamps them with the current time.\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const make = (storage: Storage): ChatLog[\"Service\"] =>\n ChatLog.of({\n read: storage.read,\n append: Effect.fn(\"ChatLog.append\")(function* (\n chatId: ChatId,\n events: ReadonlyArray<AssistantEvent.AssistantEvent | AssistantEvent.StoredAppEvent>\n ) {\n if (events.length === 0) return []\n\n const sequence = yield* storage.lastSequence(chatId)\n const at = yield* Clock.currentTimeMillis\n\n const entries = events.map((event, n) =>\n AssistantEvent.Entry.make({ chatId, sequence: sequence + n + 1, at, event })\n )\n\n yield* storage.write(chatId, entries)\n\n return entries\n }),\n unfinished: Effect.gen(function* () {\n const last = yield* storage.lastEntries\n\n // A deleted chat owes nothing.\n return last.flatMap((entry) =>\n AssistantEvent.isTurnEnd(entry.event) || Predicate.isTagged(entry.event, \"ChatDeleted\") ? [] : [entry.chatId]\n )\n })\n })\n\n/**\n * Layer that keeps every chat in memory. The log is lost when the process stops.\n *\n * @category layers\n * @since 0.1.0\n */\nexport const layerMemory: Layer.Layer<ChatLog> = Layer.effect(\n ChatLog,\n Effect.gen(function* () {\n const chats = yield* Ref.make(new Map<ChatId, ReadonlyArray<AssistantEvent.Entry>>())\n\n return make({\n read: Effect.fnUntraced(function* (chatId: ChatId, options?: ReadOptions) {\n const all = yield* Ref.get(chats)\n\n return (all.get(chatId) ?? []).filter((entry) => entry.sequence > (options?.after ?? 0))\n }),\n lastSequence: Effect.fnUntraced(function* (chatId: ChatId) {\n const all = yield* Ref.get(chats)\n\n return all.get(chatId)?.at(-1)?.sequence ?? 0\n }),\n write: (chatId, entries) =>\n Ref.update(chats, (all) => new Map(all).set(chatId, [...(all.get(chatId) ?? []), ...entries])),\n lastEntries: Effect.gen(function* () {\n const all = yield* Ref.get(chats)\n\n return [...all.values()].flatMap((entries) => entries.slice(-1))\n })\n })\n })\n)\n\nconst EventJson = Schema.fromJsonString(Schema.Union([AssistantEvent.AssistantEvent, AssistantEvent.StoredAppEvent]))\n\n// Postgres returns BIGINT and DOUBLE PRECISION columns as strings; other databases as numbers.\nconst SqlNumber = Schema.Union([Schema.Finite, Schema.FiniteFromString])\n\nconst Row = Schema.Struct({ chat_id: ChatId, sequence: SqlNumber, at: SqlNumber, event: EventJson })\n\nconst decodeLastSequence = Schema.decodeUnknownEffect(\n Schema.Array(Schema.Struct({ sequence: Schema.NullOr(SqlNumber) }))\n)\n\n/**\n * Layer that stores chats in a SQL table named `chat_log`.\n *\n * **Details**\n *\n * The table must already exist. It has the columns `chat_id`, `sequence`, `at`, and `event`, with the\n * primary key `(chat_id, sequence)`. The layer requires a `SqlClient`.\n *\n * **Example** (Storing chats in Postgres)\n *\n * ```ts\n * import * as PgClient from \"@effect/sql-pg/PgClient\"\n * import { ChatLog } from \"effect-assistant\"\n * import * as Config from \"effect/Config\"\n * import * as Layer from \"effect/Layer\"\n *\n * const ChatLogLive = ChatLog.layerSql.pipe(\n * Layer.provide(PgClient.layerConfig({ url: Config.Redacted(\"DATABASE_URL\") }))\n * )\n * ```\n *\n * The table, in Postgres:\n *\n * ```sql\n * CREATE TABLE chat_log (\n * chat_id TEXT NOT NULL,\n * sequence INTEGER NOT NULL,\n * at DOUBLE PRECISION NOT NULL,\n * event TEXT NOT NULL,\n * PRIMARY KEY (chat_id, sequence)\n * );\n * ```\n *\n * @category layers\n * @since 0.1.0\n */\nexport const layerSql: Layer.Layer<ChatLog, never, SqlClient.SqlClient> = Layer.effect(\n ChatLog,\n Effect.gen(function* () {\n const sql = (yield* SqlClient.SqlClient).withoutTransforms()\n\n const toEntries = Effect.fnUntraced(function* (rows: ReadonlyArray<unknown>) {\n const decoded = yield* Schema.decodeUnknownEffect(Schema.Array(Row))(rows)\n\n return decoded.map((row) =>\n AssistantEvent.Entry.make({\n chatId: row.chat_id,\n sequence: row.sequence,\n at: row.at,\n event: row.event\n })\n )\n })\n\n const encodeEvent = Schema.encodeEffect(EventJson)\n\n return make({\n read: Effect.fnUntraced(function* (chatId: ChatId, options?: ReadOptions) {\n const rows = yield* sql`\n SELECT chat_id, sequence, at, event FROM chat_log WHERE chat_id = ${chatId} AND sequence > ${options?.after ?? 0} ORDER BY sequence\n `\n\n return yield* toEntries(rows)\n }, Effect.orDie),\n lastSequence: Effect.fnUntraced(function* (chatId: ChatId) {\n const result = yield* sql`\n SELECT MAX(sequence) AS sequence FROM chat_log WHERE chat_id = ${chatId}\n `\n\n const rows = yield* decodeLastSequence(result)\n\n return rows[0]?.sequence ?? 0\n }, Effect.orDie),\n write: Effect.fnUntraced(function* (chatId: ChatId, entries: ReadonlyArray<AssistantEvent.Entry>) {\n const rows = yield* Effect.forEach(\n entries,\n Effect.fnUntraced(function* (entry: AssistantEvent.Entry) {\n const event = yield* encodeEvent(entry.event)\n\n return { chat_id: chatId, sequence: entry.sequence, at: entry.at, event }\n })\n )\n\n yield* sql`INSERT INTO chat_log ${sql.insert(rows)}`\n }, Effect.orDie),\n lastEntries: Effect.gen(function* () {\n const rows = yield* sql`\n SELECT e.chat_id, e.sequence, e.at, e.event FROM chat_log e\n JOIN (SELECT chat_id, MAX(sequence) AS sequence FROM chat_log GROUP BY chat_id) last\n ON last.chat_id = e.chat_id AND last.sequence = e.sequence\n `\n\n return yield* toEntries(rows)\n }).pipe(Effect.orDie)\n })\n })\n)\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ChatLog.js","names":["AssistantEvent.isTurnEnd","AssistantEvent.AssistantEvent","AssistantEvent.StoredAppEvent"],"sources":["../src/ChatLog.ts"],"sourcesContent":["/**\n * This module provides the `ChatLog` service, which stores each chat as an append-only list of\n * `AssistantEvent`s.\n *\n * A log is built on a {@link Storage}. {@link layerMemory} keeps every chat in memory, for tests\n * and local runs, and {@link layerSql} stores chats in a SQL table so they survive restarts. To\n * use another store, implement a `Storage` and pass it to {@link make}.\n *\n * @since 0.1.0\n */\nimport * as Clock from \"effect/Clock\"\nimport * as Context from \"effect/Context\"\nimport * as Effect from \"effect/Effect\"\nimport * as Layer from \"effect/Layer\"\nimport * as Predicate from \"effect/Predicate\"\nimport * as Ref from \"effect/Ref\"\nimport * as Schema from \"effect/Schema\"\nimport * as SqlClient from \"effect/sql/SqlClient\"\nimport * as AssistantEvent from \"./AssistantEvent.ts\"\nimport { ChatId } from \"./ChatId.ts\"\n\n/**\n * Service that stores each chat's events as an ordered list of entries.\n *\n * @category services\n * @since 0.1.0\n */\nexport class ChatLog extends Context.Service<\n ChatLog,\n {\n /** Appends events to a chat and returns them as stored entries. */\n readonly append: (\n chatId: ChatId,\n events: ReadonlyArray<AssistantEvent.AssistantEvent | AssistantEvent.StoredAppEvent>\n ) => Effect.Effect<ReadonlyArray<AssistantEvent.Entry>>\n /** Reads a chat's stored entries in order, either all of them or only those after a `sequence`. */\n readonly read: (chatId: ChatId, options?: ReadOptions) => Effect.Effect<ReadonlyArray<AssistantEvent.Entry>>\n /**\n * Returns the chats whose last entry ends neither a turn nor the chat. After a restart, these\n * are the chats that may still owe a turn.\n */\n readonly unfinished: Effect.Effect<ReadonlyArray<ChatId>>\n }\n>()(\"effect-assistant/ChatLog\") {}\n\n/**\n * Represents the options that select which of a chat's entries to read.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface ReadOptions {\n /** Reads only the entries after this `sequence`. */\n readonly after?: number | undefined\n}\n\n/**\n * Represents the storage that a `ChatLog` is built on.\n *\n * A storage reads a chat's entries, finds a chat's last `sequence`, writes new entries, and finds every\n * chat's last entry. Entries are never removed, so a deleted chat keeps its log, which ends in\n * `ChatDeleted`.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface Storage {\n readonly read: (chatId: ChatId, options?: ReadOptions) => Effect.Effect<ReadonlyArray<AssistantEvent.Entry>>\n readonly lastSequence: (chatId: ChatId) => Effect.Effect<number>\n readonly write: (chatId: ChatId, entries: ReadonlyArray<AssistantEvent.Entry>) => Effect.Effect<void>\n /** Returns the last entry of every chat. */\n readonly lastEntries: Effect.Effect<ReadonlyArray<AssistantEvent.Entry>>\n}\n\n/**\n * Creates a `ChatLog` from a {@link Storage}. `append` numbers new entries after the chat's last\n * `sequence` and stamps them with the current time.\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const make = (storage: Storage): ChatLog[\"Service\"] =>\n ChatLog.of({\n read: storage.read,\n append: Effect.fn(\"ChatLog.append\")(function* (\n chatId: ChatId,\n events: ReadonlyArray<AssistantEvent.AssistantEvent | AssistantEvent.StoredAppEvent>\n ) {\n if (events.length === 0) return []\n\n const sequence = yield* storage.lastSequence(chatId)\n const at = yield* Clock.currentTimeMillis\n\n const entries = events.map((event, n) =>\n AssistantEvent.Entry.make({ chatId, sequence: sequence + n + 1, at, event })\n )\n\n yield* storage.write(chatId, entries)\n\n return entries\n }),\n unfinished: Effect.gen(function* () {\n const last = yield* storage.lastEntries\n\n // A deleted chat owes nothing.\n return last.flatMap((entry) =>\n AssistantEvent.isTurnEnd(entry.event) || Predicate.isTagged(entry.event, \"ChatDeleted\") ? [] : [entry.chatId]\n )\n })\n })\n\n/**\n * Layer that keeps every chat in memory. The log is lost when the process stops.\n *\n * @category layers\n * @since 0.1.0\n */\nexport const layerMemory: Layer.Layer<ChatLog> = Layer.effect(\n ChatLog,\n Effect.gen(function* () {\n const chats = yield* Ref.make(new Map<ChatId, ReadonlyArray<AssistantEvent.Entry>>())\n\n return make({\n read: Effect.fnUntraced(function* (chatId: ChatId, options?: ReadOptions) {\n const all = yield* Ref.get(chats)\n\n return (all.get(chatId) ?? []).filter((entry) => entry.sequence > (options?.after ?? 0))\n }),\n lastSequence: Effect.fnUntraced(function* (chatId: ChatId) {\n const all = yield* Ref.get(chats)\n\n return all.get(chatId)?.at(-1)?.sequence ?? 0\n }),\n write: (chatId, entries) =>\n Ref.update(chats, (all) => new Map(all).set(chatId, [...(all.get(chatId) ?? []), ...entries])),\n lastEntries: Effect.gen(function* () {\n const all = yield* Ref.get(chats)\n\n return [...all.values()].flatMap((entries) => entries.slice(-1))\n })\n })\n })\n)\n\n/**\n * Schema for an event as the log stores it: JSON text.\n *\n * **Details**\n *\n * `layerSql` stores events with it. A storage of your own, for `make`, can use it to store the\n * same text.\n *\n * @category schemas\n * @since 0.8.0\n */\nexport const EventJson = Schema.fromJsonString(\n Schema.Union([AssistantEvent.AssistantEvent, AssistantEvent.StoredAppEvent])\n)\n\n/**\n * Represents the options accepted by `layerSql`.\n *\n * @category models\n * @since 0.8.0\n */\nexport interface SqlOptions {\n /** The table's name. Defaults to `chat_log`. */\n readonly table?: string | undefined\n}\n\n// Postgres returns BIGINT and DOUBLE PRECISION columns as strings; other databases as numbers.\nconst SqlNumber = Schema.Union([Schema.Finite, Schema.FiniteFromString])\n\nconst Row = Schema.Struct({ chat_id: ChatId, sequence: SqlNumber, at: SqlNumber, event: EventJson })\n\nconst decodeLastSequence = Schema.decodeUnknownEffect(\n Schema.Array(Schema.Struct({ sequence: Schema.NullOr(SqlNumber) }))\n)\n\n/**\n * Layer that stores chats in a SQL table, `chat_log` unless `table` names another.\n *\n * **Details**\n *\n * The table must already exist. It has the columns `chat_id`, `sequence`, `at`, and `event`, with the\n * primary key `(chat_id, sequence)`. The layer requires a `SqlClient`. In MySQL, `event` needs\n * `LONGTEXT`, since a tool's result can be larger than `TEXT` holds.\n *\n * **Example** (Storing chats in Postgres)\n *\n * ```ts\n * import * as PgClient from \"@effect/sql-pg/PgClient\"\n * import { ChatLog } from \"effect-assistant\"\n * import * as Config from \"effect/Config\"\n * import * as Layer from \"effect/Layer\"\n *\n * const ChatLogLive = ChatLog.layerSql().pipe(\n * Layer.provide(PgClient.layerConfig({ url: Config.Redacted(\"DATABASE_URL\") }))\n * )\n * ```\n *\n * The table, in Postgres:\n *\n * ```sql\n * CREATE TABLE chat_log (\n * chat_id TEXT NOT NULL,\n * sequence INTEGER NOT NULL,\n * at DOUBLE PRECISION NOT NULL,\n * event TEXT NOT NULL,\n * PRIMARY KEY (chat_id, sequence)\n * );\n * ```\n *\n * @category layers\n * @since 0.1.0\n */\nexport const layerSql = (options?: SqlOptions): Layer.Layer<ChatLog, never, SqlClient.SqlClient> =>\n Layer.effect(\n ChatLog,\n Effect.gen(function* () {\n const sql = (yield* SqlClient.SqlClient).withoutTransforms()\n const table = sql(options?.table ?? \"chat_log\")\n\n const toEntries = Effect.fnUntraced(function* (rows: ReadonlyArray<unknown>) {\n const decoded = yield* Schema.decodeUnknownEffect(Schema.Array(Row))(rows)\n\n return decoded.map((row) =>\n AssistantEvent.Entry.make({\n chatId: row.chat_id,\n sequence: row.sequence,\n at: row.at,\n event: row.event\n })\n )\n })\n\n const encodeEvent = Schema.encodeEffect(EventJson)\n\n return make({\n read: Effect.fnUntraced(function* (chatId: ChatId, options?: ReadOptions) {\n const rows = yield* sql`\n SELECT chat_id, sequence, at, event FROM ${table} WHERE chat_id = ${chatId} AND sequence > ${options?.after ?? 0} ORDER BY sequence\n `\n\n return yield* toEntries(rows)\n }, Effect.orDie),\n lastSequence: Effect.fnUntraced(function* (chatId: ChatId) {\n const result = yield* sql`\n SELECT MAX(sequence) AS sequence FROM ${table} WHERE chat_id = ${chatId}\n `\n\n const rows = yield* decodeLastSequence(result)\n\n return rows[0]?.sequence ?? 0\n }, Effect.orDie),\n write: Effect.fnUntraced(function* (chatId: ChatId, entries: ReadonlyArray<AssistantEvent.Entry>) {\n const rows = yield* Effect.forEach(\n entries,\n Effect.fnUntraced(function* (entry: AssistantEvent.Entry) {\n const event = yield* encodeEvent(entry.event)\n\n return { chat_id: chatId, sequence: entry.sequence, at: entry.at, event }\n })\n )\n\n yield* sql`INSERT INTO ${table} ${sql.insert(rows)}`\n }, Effect.orDie),\n lastEntries: Effect.gen(function* () {\n const rows = yield* sql`\n SELECT e.chat_id, e.sequence, e.at, e.event FROM ${table} e\n JOIN (SELECT chat_id, MAX(sequence) AS sequence FROM ${table} GROUP BY chat_id) last\n ON last.chat_id = e.chat_id AND last.sequence = e.sequence\n `\n\n return yield* toEntries(rows)\n }).pipe(Effect.orDie)\n })\n })\n )\n"],"mappings":"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"}
|
package/dist/internal/history.js
CHANGED
|
@@ -164,11 +164,11 @@ const owesDelivery = (entries, throughSequence) => {
|
|
|
164
164
|
const latest = entries.findLast(({ event, sequence }) => sequence <= throughSequence && isInput$1(event) && isUserInput(event));
|
|
165
165
|
if (latest === void 0) return false;
|
|
166
166
|
const after = entries.flatMap(({ sequence, event }) => sequence > latest.sequence && isLibraryEvent(event) ? [event] : []);
|
|
167
|
-
const
|
|
167
|
+
const last = after.findLast((event) => Predicate.isTagged(event, "AssistantMessage"));
|
|
168
168
|
const progress = progressMessages(entries);
|
|
169
169
|
const started = after.flatMap((event) => Predicate.isTagged(event, "SpecialistStarted") ? [event.runId] : []);
|
|
170
170
|
const finished = new Set(entries.flatMap(({ event }) => isLibraryEvent(event) && Predicate.isTagged(event, "SpecialistFinished") ? [event.runId] : []));
|
|
171
|
-
return
|
|
171
|
+
return last !== void 0 && last.block === void 0 && progress.has(last.id) && !after.some((event) => Predicate.isTagged(event, "TurnFailed") || Predicate.isTagged(event, "ApprovalRequested")) && started.every((runId) => finished.has(runId));
|
|
172
172
|
};
|
|
173
173
|
/** @internal */
|
|
174
174
|
const unreadAfter = (entries, throughSequence) => entries.some(({ event, sequence }) => sequence > throughSequence && isInput(event));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"history.js","names":["AssistantEvent.isLibraryEvent","AssistantEvent.textWithAttachments","AssistantEvent.isInput","AssistantEvent.isAppInput","AssistantEvent.isTurnEnd","AssistantEvent.isWaking","AssistantEvent.isUserInput","Uuid.fromKey"],"sources":["../../src/internal/history.ts"],"sourcesContent":["import * as Prompt from \"effect/ai/Prompt\"\nimport * as Response from \"effect/ai/Response\"\nimport * as Match from \"effect/Match\"\nimport * as Option from \"effect/Option\"\nimport * as Predicate from \"effect/Predicate\"\nimport * as Schema from \"effect/Schema\"\nimport * as AssistantEvent from \"../AssistantEvent.ts\"\nimport { toJson, toJsonText } from \"./schema.ts\"\nimport * as Uuid from \"./uuid.ts\"\n\n/** @internal */\nexport const userText = (text: string): Prompt.UserMessage =>\n Prompt.userMessage({ content: [Prompt.textPart({ text })] })\n\n/** @internal */\nexport const toolMessage = (result: AssistantEvent.ToolResult): Prompt.ToolMessage =>\n Prompt.toolMessage({\n content: [\n Prompt.toolResultPart({\n id: result.callId,\n name: result.tool,\n isFailure: result.outcome === \"failure\",\n result: result.result,\n providerExecuted: false\n })\n ]\n })\n\n// A call with no result is one a crash cut off.\n/** @internal */\nexport const unansweredCalls = (history: ReadonlyArray<Prompt.Message>): ReadonlyArray<AssistantEvent.ToolCallPart> => {\n const answered = new Set(\n history.flatMap((message) =>\n message.role === \"tool\" ? message.content.flatMap((part) => (part.type === \"tool-result\" ? [part.id] : [])) : []\n )\n )\n\n return history.flatMap((message) =>\n message.role === \"assistant\"\n ? message.content.flatMap((part) =>\n part.type === \"tool-call\" && !part.providerExecuted && !answered.has(part.id)\n ? [\n Response.makePart(\"tool-call\", {\n id: part.id,\n name: part.name,\n params: toJson(part.params),\n providerExecuted: false,\n metadata: part.options\n })\n ]\n : []\n )\n : []\n )\n}\n\n/** @internal */\nexport const notRunOutput = \"Not run: the turn ended before this call ran.\"\n\n/** @internal */\nexport const mayHaveRunOutput =\n \"Stopped: the turn ended while this call ran. It may have done its work; check before doing it again.\"\n\nconst ago = (millis: number) => {\n const seconds = Math.max(0, Math.round(millis / 1000))\n\n return seconds < 90 ? `${seconds} seconds` : `${Math.round(seconds / 60)} minutes`\n}\n\nconst finishedText = (\n entries: ReadonlyArray<AssistantEvent.AnyEntry>,\n index: number,\n finished: AssistantEvent.SpecialistFinished\n) => {\n const at = entries[index]!\n\n const isStart = ({ event }: AssistantEvent.AnyEntry) =>\n AssistantEvent.isLibraryEvent(event) &&\n Predicate.isTagged(event, \"SpecialistStarted\") &&\n event.runId === finished.runId\n\n const from = entries.slice(0, index).findLastIndex(isStart)\n const started = from === -1 ? undefined : entries[from]\n const since = started === undefined ? [] : entries.slice(from + 1, index)\n const wrote = since.some(({ event }) => Predicate.isTagged(event, \"UserMessage\"))\n\n const lines = [\n \"A report from a specialist you started. The user hasn't seen it.\",\n started === undefined\n ? `You started it for: ${finished.goal}`\n : `You started it ${ago(at.at - started.at)} ago for: ${finished.goal}`,\n wrote ? \"The user has written since you started it.\" : \"The user hasn't written since you started it.\",\n Match.value(finished.status).pipe(\n Match.when(\"answered\", () => `Report:\\n${finished.report ?? \"\"}`),\n Match.when(\"failed\", () => `It failed: ${finished.error ?? \"\"}`),\n Match.when(\"cut_off\", () => \"A restart cut it off before it finished. Spawn it again if it's still needed.\"),\n Match.when(\"stopped\", () => \"You stopped it.\"),\n Match.exhaustive\n )\n ]\n\n return `<specialist name=\"${finished.specialist}\" run=\"${finished.runId}\" status=\"${finished.status}\">\\n${lines.join(\n \"\\n\"\n )}\\n</specialist>`\n}\n\nconst answerText = (answer: AssistantEvent.BlockAnswer) =>\n `<answer message=\"${answer.messageId}\" kind=\"${answer.kind}\">\\nThe user answered with the block on your message ${answer.messageId}.\\n${toJsonText(answer.value)}\\n</answer>`\n\nconst resultText = (result: AssistantEvent.ToolResult) =>\n `<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>`\n\n/** @internal */\nexport const awaitingApprovalOutput =\n \"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.\"\n\nconst awaitingText = (request: AssistantEvent.ApprovalRequested) =>\n `<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>`\n\nconst approvedText = (result: AssistantEvent.ToolResult) =>\n `<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>`\n\nconst rejectedText = (decision: AssistantEvent.ApprovalDecision, tool: string) =>\n `<approval tool=\"${tool}\" call=\"${decision.callId}\" decision=\"rejected\">\\nThe user rejected your call to ${tool}; it didn't run.${decision.reason === undefined ? \"\" : `\\nTheir reason: ${decision.reason}`}\\n</approval>`\n\nconst expiredText = (callId: string, tool: string) =>\n `<approval tool=\"${tool}\" call=\"${callId}\" status=\"expired\">\\nThe user moved on without approving your call to ${tool}; it didn't run.\\n</approval>`\n\nconst awaitingMessage = (request: AssistantEvent.ApprovalRequested): Prompt.ToolMessage =>\n Prompt.toolMessage({\n content: [\n Prompt.toolResultPart({\n id: request.callId,\n name: request.tool,\n isFailure: false,\n result: awaitingApprovalOutput,\n providerExecuted: false\n })\n ]\n })\n\nconst isEntry = (item: AssistantEvent.AnyEntry | Prompt.Message): item is AssistantEvent.AnyEntry =>\n Predicate.hasProperty(item, \"sequence\")\n\n// In the order the model saw the entries, not the order they were stored. A message a history view put in stands\n// where it is, right after the entry before it, and waits like an input for a call's result or for the model's next\n// read; the app's own events reach the model only as such messages.\n/** @internal */\nexport const fromEntries = (\n items: ReadonlyArray<AssistantEvent.AnyEntry | Prompt.Message>\n): ReadonlyArray<Prompt.Message> => {\n const entries = items.filter(isEntry)\n const history: Array<Prompt.Message> = []\n const held: Array<Prompt.Message> = []\n const open = new Set<string>()\n const called = new Set<string>()\n // Calls that waited for approval: the placeholder answered them, so what came of them later is an input.\n const requests = new Map<string, AssistantEvent.ApprovalRequested>()\n const toolOf = (callId: string) => requests.get(callId)?.tool ?? \"a tool\"\n\n const flushIfClosed = () => {\n if (open.size === 0) history.push(...held.splice(0))\n }\n\n const nextSeen = (from: number) => {\n const next = entries.slice(from + 1).find(({ event }) => Predicate.isTagged(event, \"ModelOutput\"))?.event\n\n return next !== undefined && AssistantEvent.isLibraryEvent(next) && Predicate.isTagged(next, \"ModelOutput\")\n ? next.seenThroughSequence\n : undefined\n }\n\n // `index` is the last entry at or before the item, and `sequence` where the item stands.\n const input = (index: number, sequence: number, item: Prompt.Message) => {\n const seen = nextSeen(index)\n\n if (open.size > 0 || (seen !== undefined && seen < sequence)) held.push(item)\n else history.push(item)\n }\n\n let index = -1\n\n for (const item of items) {\n if (!isEntry(item)) {\n input(index, (entries[index]?.sequence ?? 0) + 0.5, item)\n\n continue\n }\n\n index += 1\n\n const { event, sequence } = item\n const at = index\n\n if (!AssistantEvent.isLibraryEvent(event)) continue\n\n Match.value(event).pipe(\n Match.tagsExhaustive({\n UserMessage: (message) =>\n input(at, sequence, userText(AssistantEvent.textWithAttachments(message.text, message.attachments))),\n BlockAnswer: (answer) => input(at, sequence, userText(answerText(answer))),\n ModelOutput: (output) => {\n history.push(...Prompt.fromResponseParts(output.parts).content)\n\n for (const part of output.parts) {\n if (part.type === \"tool-call\") {\n open.add(part.id)\n called.add(part.id)\n }\n }\n\n flushIfClosed()\n },\n ToolResult: (result) => {\n if (requests.has(result.callId)) return input(at, sequence, userText(approvedText(result)))\n\n // A view that starts after the call (since a summary, say) keeps its result as text: a tool result needs its\n // call right before it.\n if (!called.has(result.callId)) return input(at, sequence, userText(resultText(result)))\n\n history.push(toolMessage(result))\n open.delete(result.callId)\n flushIfClosed()\n },\n AssistantMessage: () => undefined,\n ToolStarted: () => undefined,\n TurnStarted: () => undefined,\n TurnCompleted: () => undefined,\n TurnFailed: () => undefined,\n SpecialistFinished: (finished) => input(at, sequence, userText(finishedText(entries, at, finished))),\n // Every run starts in a call of the model's own (a spawn or a follow-up), whose result says so.\n SpecialistStarted: () => undefined,\n BlocksClosed: () => undefined,\n // The call's result, for the provider's pairing; what the user decides comes later.\n ApprovalRequested: (request) => {\n requests.set(request.callId, request)\n\n if (!called.has(request.callId)) return input(at, sequence, userText(awaitingText(request)))\n\n history.push(awaitingMessage(request))\n open.delete(request.callId)\n flushIfClosed()\n },\n // An approved call's result says so.\n ApprovalDecision: (decision) =>\n decision.decision === \"rejected\"\n ? input(at, sequence, userText(rejectedText(decision, toolOf(decision.callId))))\n : undefined,\n ApprovalsExpired: (expired) =>\n expired.callIds.forEach((callId) => input(at, sequence, userText(expiredText(callId, toolOf(callId))))),\n ChatDeleted: () => undefined\n })\n )\n }\n\n return [...history, ...held]\n}\n\n/** @internal */\nexport const isInput = (event: { readonly _tag: string }): boolean =>\n AssistantEvent.isInput(event) || AssistantEvent.isAppInput(event)\n\n/** @internal */\nexport const handledThrough = (entries: ReadonlyArray<AssistantEvent.AnyEntry>): number => {\n const last = entries.findLast(({ event }) => AssistantEvent.isTurnEnd(event))?.event\n\n return last !== undefined && AssistantEvent.isTurnEnd(last) ? last.throughSequence : 0\n}\n\n// A run the assistant stopped isn't an input here: it wakes nothing.\n/** @internal */\nexport const unhandledInputs = (\n entries: ReadonlyArray<AssistantEvent.AnyEntry>\n): ReadonlyArray<{ readonly _tag: string }> => {\n const seen = handledThrough(entries)\n\n return entries.flatMap(({ event, sequence }) =>\n sequence > seen && isInput(event) && !(AssistantEvent.isInput(event) && !AssistantEvent.isWaking(event))\n ? [event]\n : []\n )\n}\n\n/** @internal */\nexport const needsTurn = (entries: ReadonlyArray<AssistantEvent.AnyEntry>): boolean =>\n unhandledInputs(entries).length > 0\n\n// A failure the user was shown counts as a reply, and so does a call they're asked to approve.\n/** @internal */\nexport const owesReply = (entries: ReadonlyArray<AssistantEvent.AnyEntry>, throughSequence: number): boolean => {\n const latest = entries.findLast(\n ({ event, sequence }) =>\n sequence <= throughSequence && AssistantEvent.isInput(event) && AssistantEvent.isUserInput(event)\n )\n\n return (\n latest !== undefined &&\n !entries.some(\n ({ event, sequence }) =>\n sequence > latest.sequence &&\n (Predicate.isTagged(event, \"AssistantMessage\") ||\n Predicate.isTagged(event, \"TurnFailed\") ||\n Predicate.isTagged(event, \"ApprovalRequested\"))\n )\n )\n}\n\nconst sentToWaitOn = Schema.decodeUnknownOption(Schema.Struct({ wait: Schema.Literal(false) }))\n\n// Messages sent with `wait: false` are progress while the model kept working, like \"On it\".\nconst progressMessages = (entries: ReadonlyArray<AssistantEvent.AnyEntry>): ReadonlySet<string> =>\n new Set(\n entries.flatMap(({ event }) =>\n AssistantEvent.isLibraryEvent(event) && Predicate.isTagged(event, \"ModelOutput\")\n ? event.parts.flatMap((part) =>\n part.type === \"tool-call\" && part.name === \"send_user_message\" && Option.isSome(sentToWaitOn(part.params))\n ? [Uuid.fromKey(`message:${part.id}`)]\n : []\n )\n : []\n )\n )\n\n// The model said it was on it and stopped without the result: only progress messages since the user's latest input.\n// A specialist still working doesn't count, since its report is what comes next.\n/** @internal */\nexport const owesDelivery = (entries: ReadonlyArray<AssistantEvent.AnyEntry>, throughSequence: number): boolean => {\n const latest = entries.findLast(\n ({ event, sequence }) =>\n sequence <= throughSequence && AssistantEvent.isInput(event) && AssistantEvent.isUserInput(event)\n )\n\n if (latest === undefined) return false\n\n const after = entries.flatMap(({ sequence, event }) =>\n sequence > latest.sequence && AssistantEvent.isLibraryEvent(event) ? [event] : []\n )\n\n const messages = after.flatMap((event) => (Predicate.isTagged(event, \"AssistantMessage\") ? [event] : []))\n const progress = progressMessages(entries)\n const started = after.flatMap((event) => (Predicate.isTagged(event, \"SpecialistStarted\") ? [event.runId] : []))\n\n const finished = new Set(\n entries.flatMap(({ event }) =>\n AssistantEvent.isLibraryEvent(event) && Predicate.isTagged(event, \"SpecialistFinished\") ? [event.runId] : []\n )\n )\n\n // A call waiting for the user's approval is what comes next, like a specialist's report.\n return (\n messages.length > 0 &&\n messages.every((message) => message.block === undefined && progress.has(message.id)) &&\n !after.some((event) => Predicate.isTagged(event, \"TurnFailed\") || Predicate.isTagged(event, \"ApprovalRequested\")) &&\n started.every((runId) => finished.has(runId))\n )\n}\n\n/** @internal */\nexport const unreadAfter = (entries: ReadonlyArray<AssistantEvent.AnyEntry>, throughSequence: number): boolean =>\n entries.some(({ event, sequence }) => sequence > throughSequence && isInput(event))\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"history.js","names":["AssistantEvent.isLibraryEvent","AssistantEvent.textWithAttachments","AssistantEvent.isInput","AssistantEvent.isAppInput","AssistantEvent.isTurnEnd","AssistantEvent.isWaking","AssistantEvent.isUserInput","Uuid.fromKey"],"sources":["../../src/internal/history.ts"],"sourcesContent":["import * as Prompt from \"effect/ai/Prompt\"\nimport * as Response from \"effect/ai/Response\"\nimport * as Match from \"effect/Match\"\nimport * as Option from \"effect/Option\"\nimport * as Predicate from \"effect/Predicate\"\nimport * as Schema from \"effect/Schema\"\nimport * as AssistantEvent from \"../AssistantEvent.ts\"\nimport { toJson, toJsonText } from \"./schema.ts\"\nimport * as Uuid from \"./uuid.ts\"\n\n/** @internal */\nexport const userText = (text: string): Prompt.UserMessage =>\n Prompt.userMessage({ content: [Prompt.textPart({ text })] })\n\n/** @internal */\nexport const toolMessage = (result: AssistantEvent.ToolResult): Prompt.ToolMessage =>\n Prompt.toolMessage({\n content: [\n Prompt.toolResultPart({\n id: result.callId,\n name: result.tool,\n isFailure: result.outcome === \"failure\",\n result: result.result,\n providerExecuted: false\n })\n ]\n })\n\n// A call with no result is one a crash cut off.\n/** @internal */\nexport const unansweredCalls = (history: ReadonlyArray<Prompt.Message>): ReadonlyArray<AssistantEvent.ToolCallPart> => {\n const answered = new Set(\n history.flatMap((message) =>\n message.role === \"tool\" ? message.content.flatMap((part) => (part.type === \"tool-result\" ? [part.id] : [])) : []\n )\n )\n\n return history.flatMap((message) =>\n message.role === \"assistant\"\n ? message.content.flatMap((part) =>\n part.type === \"tool-call\" && !part.providerExecuted && !answered.has(part.id)\n ? [\n Response.makePart(\"tool-call\", {\n id: part.id,\n name: part.name,\n params: toJson(part.params),\n providerExecuted: false,\n metadata: part.options\n })\n ]\n : []\n )\n : []\n )\n}\n\n/** @internal */\nexport const notRunOutput = \"Not run: the turn ended before this call ran.\"\n\n/** @internal */\nexport const mayHaveRunOutput =\n \"Stopped: the turn ended while this call ran. It may have done its work; check before doing it again.\"\n\nconst ago = (millis: number) => {\n const seconds = Math.max(0, Math.round(millis / 1000))\n\n return seconds < 90 ? `${seconds} seconds` : `${Math.round(seconds / 60)} minutes`\n}\n\nconst finishedText = (\n entries: ReadonlyArray<AssistantEvent.AnyEntry>,\n index: number,\n finished: AssistantEvent.SpecialistFinished\n) => {\n const at = entries[index]!\n\n const isStart = ({ event }: AssistantEvent.AnyEntry) =>\n AssistantEvent.isLibraryEvent(event) &&\n Predicate.isTagged(event, \"SpecialistStarted\") &&\n event.runId === finished.runId\n\n const from = entries.slice(0, index).findLastIndex(isStart)\n const started = from === -1 ? undefined : entries[from]\n const since = started === undefined ? [] : entries.slice(from + 1, index)\n const wrote = since.some(({ event }) => Predicate.isTagged(event, \"UserMessage\"))\n\n const lines = [\n \"A report from a specialist you started. The user hasn't seen it.\",\n started === undefined\n ? `You started it for: ${finished.goal}`\n : `You started it ${ago(at.at - started.at)} ago for: ${finished.goal}`,\n wrote ? \"The user has written since you started it.\" : \"The user hasn't written since you started it.\",\n Match.value(finished.status).pipe(\n Match.when(\"answered\", () => `Report:\\n${finished.report ?? \"\"}`),\n Match.when(\"failed\", () => `It failed: ${finished.error ?? \"\"}`),\n Match.when(\"cut_off\", () => \"A restart cut it off before it finished. Spawn it again if it's still needed.\"),\n Match.when(\"stopped\", () => \"You stopped it.\"),\n Match.exhaustive\n )\n ]\n\n return `<specialist name=\"${finished.specialist}\" run=\"${finished.runId}\" status=\"${finished.status}\">\\n${lines.join(\n \"\\n\"\n )}\\n</specialist>`\n}\n\nconst answerText = (answer: AssistantEvent.BlockAnswer) =>\n `<answer message=\"${answer.messageId}\" kind=\"${answer.kind}\">\\nThe user answered with the block on your message ${answer.messageId}.\\n${toJsonText(answer.value)}\\n</answer>`\n\nconst resultText = (result: AssistantEvent.ToolResult) =>\n `<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>`\n\n/** @internal */\nexport const awaitingApprovalOutput =\n \"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.\"\n\nconst awaitingText = (request: AssistantEvent.ApprovalRequested) =>\n `<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>`\n\nconst approvedText = (result: AssistantEvent.ToolResult) =>\n `<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>`\n\nconst rejectedText = (decision: AssistantEvent.ApprovalDecision, tool: string) =>\n `<approval tool=\"${tool}\" call=\"${decision.callId}\" decision=\"rejected\">\\nThe user rejected your call to ${tool}; it didn't run.${decision.reason === undefined ? \"\" : `\\nTheir reason: ${decision.reason}`}\\n</approval>`\n\nconst expiredText = (callId: string, tool: string) =>\n `<approval tool=\"${tool}\" call=\"${callId}\" status=\"expired\">\\nThe user moved on without approving your call to ${tool}; it didn't run.\\n</approval>`\n\nconst awaitingMessage = (request: AssistantEvent.ApprovalRequested): Prompt.ToolMessage =>\n Prompt.toolMessage({\n content: [\n Prompt.toolResultPart({\n id: request.callId,\n name: request.tool,\n isFailure: false,\n result: awaitingApprovalOutput,\n providerExecuted: false\n })\n ]\n })\n\nconst isEntry = (item: AssistantEvent.AnyEntry | Prompt.Message): item is AssistantEvent.AnyEntry =>\n Predicate.hasProperty(item, \"sequence\")\n\n// In the order the model saw the entries, not the order they were stored. A message a history view put in stands\n// where it is, right after the entry before it, and waits like an input for a call's result or for the model's next\n// read; the app's own events reach the model only as such messages.\n/** @internal */\nexport const fromEntries = (\n items: ReadonlyArray<AssistantEvent.AnyEntry | Prompt.Message>\n): ReadonlyArray<Prompt.Message> => {\n const entries = items.filter(isEntry)\n const history: Array<Prompt.Message> = []\n const held: Array<Prompt.Message> = []\n const open = new Set<string>()\n const called = new Set<string>()\n // Calls that waited for approval: the placeholder answered them, so what came of them later is an input.\n const requests = new Map<string, AssistantEvent.ApprovalRequested>()\n const toolOf = (callId: string) => requests.get(callId)?.tool ?? \"a tool\"\n\n const flushIfClosed = () => {\n if (open.size === 0) history.push(...held.splice(0))\n }\n\n const nextSeen = (from: number) => {\n const next = entries.slice(from + 1).find(({ event }) => Predicate.isTagged(event, \"ModelOutput\"))?.event\n\n return next !== undefined && AssistantEvent.isLibraryEvent(next) && Predicate.isTagged(next, \"ModelOutput\")\n ? next.seenThroughSequence\n : undefined\n }\n\n // `index` is the last entry at or before the item, and `sequence` where the item stands.\n const input = (index: number, sequence: number, item: Prompt.Message) => {\n const seen = nextSeen(index)\n\n if (open.size > 0 || (seen !== undefined && seen < sequence)) held.push(item)\n else history.push(item)\n }\n\n let index = -1\n\n for (const item of items) {\n if (!isEntry(item)) {\n input(index, (entries[index]?.sequence ?? 0) + 0.5, item)\n\n continue\n }\n\n index += 1\n\n const { event, sequence } = item\n const at = index\n\n if (!AssistantEvent.isLibraryEvent(event)) continue\n\n Match.value(event).pipe(\n Match.tagsExhaustive({\n UserMessage: (message) =>\n input(at, sequence, userText(AssistantEvent.textWithAttachments(message.text, message.attachments))),\n BlockAnswer: (answer) => input(at, sequence, userText(answerText(answer))),\n ModelOutput: (output) => {\n history.push(...Prompt.fromResponseParts(output.parts).content)\n\n for (const part of output.parts) {\n if (part.type === \"tool-call\") {\n open.add(part.id)\n called.add(part.id)\n }\n }\n\n flushIfClosed()\n },\n ToolResult: (result) => {\n if (requests.has(result.callId)) return input(at, sequence, userText(approvedText(result)))\n\n // A view that starts after the call (since a summary, say) keeps its result as text: a tool result needs its\n // call right before it.\n if (!called.has(result.callId)) return input(at, sequence, userText(resultText(result)))\n\n history.push(toolMessage(result))\n open.delete(result.callId)\n flushIfClosed()\n },\n AssistantMessage: () => undefined,\n ToolStarted: () => undefined,\n TurnStarted: () => undefined,\n TurnCompleted: () => undefined,\n TurnFailed: () => undefined,\n SpecialistFinished: (finished) => input(at, sequence, userText(finishedText(entries, at, finished))),\n // Every run starts in a call of the model's own (a spawn or a follow-up), whose result says so.\n SpecialistStarted: () => undefined,\n BlocksClosed: () => undefined,\n // The call's result, for the provider's pairing; what the user decides comes later.\n ApprovalRequested: (request) => {\n requests.set(request.callId, request)\n\n if (!called.has(request.callId)) return input(at, sequence, userText(awaitingText(request)))\n\n history.push(awaitingMessage(request))\n open.delete(request.callId)\n flushIfClosed()\n },\n // An approved call's result says so.\n ApprovalDecision: (decision) =>\n decision.decision === \"rejected\"\n ? input(at, sequence, userText(rejectedText(decision, toolOf(decision.callId))))\n : undefined,\n ApprovalsExpired: (expired) =>\n expired.callIds.forEach((callId) => input(at, sequence, userText(expiredText(callId, toolOf(callId))))),\n ChatDeleted: () => undefined\n })\n )\n }\n\n return [...history, ...held]\n}\n\n/** @internal */\nexport const isInput = (event: { readonly _tag: string }): boolean =>\n AssistantEvent.isInput(event) || AssistantEvent.isAppInput(event)\n\n/** @internal */\nexport const handledThrough = (entries: ReadonlyArray<AssistantEvent.AnyEntry>): number => {\n const last = entries.findLast(({ event }) => AssistantEvent.isTurnEnd(event))?.event\n\n return last !== undefined && AssistantEvent.isTurnEnd(last) ? last.throughSequence : 0\n}\n\n// A run the assistant stopped isn't an input here: it wakes nothing.\n/** @internal */\nexport const unhandledInputs = (\n entries: ReadonlyArray<AssistantEvent.AnyEntry>\n): ReadonlyArray<{ readonly _tag: string }> => {\n const seen = handledThrough(entries)\n\n return entries.flatMap(({ event, sequence }) =>\n sequence > seen && isInput(event) && !(AssistantEvent.isInput(event) && !AssistantEvent.isWaking(event))\n ? [event]\n : []\n )\n}\n\n/** @internal */\nexport const needsTurn = (entries: ReadonlyArray<AssistantEvent.AnyEntry>): boolean =>\n unhandledInputs(entries).length > 0\n\n// A failure the user was shown counts as a reply, and so does a call they're asked to approve.\n/** @internal */\nexport const owesReply = (entries: ReadonlyArray<AssistantEvent.AnyEntry>, throughSequence: number): boolean => {\n const latest = entries.findLast(\n ({ event, sequence }) =>\n sequence <= throughSequence && AssistantEvent.isInput(event) && AssistantEvent.isUserInput(event)\n )\n\n return (\n latest !== undefined &&\n !entries.some(\n ({ event, sequence }) =>\n sequence > latest.sequence &&\n (Predicate.isTagged(event, \"AssistantMessage\") ||\n Predicate.isTagged(event, \"TurnFailed\") ||\n Predicate.isTagged(event, \"ApprovalRequested\"))\n )\n )\n}\n\nconst sentToWaitOn = Schema.decodeUnknownOption(Schema.Struct({ wait: Schema.Literal(false) }))\n\n// Messages sent with `wait: false` are progress while the model kept working, like \"On it\".\nconst progressMessages = (entries: ReadonlyArray<AssistantEvent.AnyEntry>): ReadonlySet<string> =>\n new Set(\n entries.flatMap(({ event }) =>\n AssistantEvent.isLibraryEvent(event) && Predicate.isTagged(event, \"ModelOutput\")\n ? event.parts.flatMap((part) =>\n part.type === \"tool-call\" && part.name === \"send_user_message\" && Option.isSome(sentToWaitOn(part.params))\n ? [Uuid.fromKey(`message:${part.id}`)]\n : []\n )\n : []\n )\n )\n\n// The model said it was on it and stopped without the result: its last message since the user's latest input was\n// progress. An earlier message may have answered something else, such as an input that came before. A specialist still\n// working doesn't count, since its report is what comes next.\n/** @internal */\nexport const owesDelivery = (entries: ReadonlyArray<AssistantEvent.AnyEntry>, throughSequence: number): boolean => {\n const latest = entries.findLast(\n ({ event, sequence }) =>\n sequence <= throughSequence && AssistantEvent.isInput(event) && AssistantEvent.isUserInput(event)\n )\n\n if (latest === undefined) return false\n\n const after = entries.flatMap(({ sequence, event }) =>\n sequence > latest.sequence && AssistantEvent.isLibraryEvent(event) ? [event] : []\n )\n\n const last = after.findLast((event) => Predicate.isTagged(event, \"AssistantMessage\"))\n const progress = progressMessages(entries)\n const started = after.flatMap((event) => (Predicate.isTagged(event, \"SpecialistStarted\") ? [event.runId] : []))\n\n const finished = new Set(\n entries.flatMap(({ event }) =>\n AssistantEvent.isLibraryEvent(event) && Predicate.isTagged(event, \"SpecialistFinished\") ? [event.runId] : []\n )\n )\n\n // A call waiting for the user's approval is what comes next, like a specialist's report.\n return (\n last !== undefined &&\n last.block === undefined &&\n progress.has(last.id) &&\n !after.some((event) => Predicate.isTagged(event, \"TurnFailed\") || Predicate.isTagged(event, \"ApprovalRequested\")) &&\n started.every((runId) => finished.has(runId))\n )\n}\n\n/** @internal */\nexport const unreadAfter = (entries: ReadonlyArray<AssistantEvent.AnyEntry>, throughSequence: number): boolean =>\n entries.some(({ event, sequence }) => sequence > throughSequence && isInput(event))\n"],"mappings":"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"}
|
package/package.json
CHANGED
package/src/AssistantEngine.ts
CHANGED
|
@@ -74,7 +74,7 @@ const notRegistered = (assistant: string) =>
|
|
|
74
74
|
*
|
|
75
75
|
* **Details**
|
|
76
76
|
*
|
|
77
|
-
* With `ChatLog.layerSql`, the engine resumes chats at startup. When an assistant registers, the
|
|
77
|
+
* With `ChatLog.layerSql()`, the engine resumes chats at startup. When an assistant registers, the
|
|
78
78
|
* engine finds the chats that assistant was working on when the process stopped (a turn cut off
|
|
79
79
|
* partway, or an input that no turn read) and runs a turn for each. Run this engine in one process
|
|
80
80
|
* only, because two processes would run the same chat and write its log at the same time.
|
|
@@ -294,7 +294,7 @@ const makeClusterClient = Effect.gen(function* () {
|
|
|
294
294
|
* Layer.provideMerge(AssistantEngine.layerCluster),
|
|
295
295
|
* Layer.provideMerge(
|
|
296
296
|
* Layer.merge(
|
|
297
|
-
* ChatLog.layerSql,
|
|
297
|
+
* ChatLog.layerSql(),
|
|
298
298
|
* SingleRunner.layer({ runnerStorage: "sql" }).pipe(Layer.provide(NodeCrypto.layer))
|
|
299
299
|
* )
|
|
300
300
|
* ),
|
package/src/ChatLog.ts
CHANGED
|
@@ -142,7 +142,31 @@ export const layerMemory: Layer.Layer<ChatLog> = Layer.effect(
|
|
|
142
142
|
})
|
|
143
143
|
)
|
|
144
144
|
|
|
145
|
-
|
|
145
|
+
/**
|
|
146
|
+
* Schema for an event as the log stores it: JSON text.
|
|
147
|
+
*
|
|
148
|
+
* **Details**
|
|
149
|
+
*
|
|
150
|
+
* `layerSql` stores events with it. A storage of your own, for `make`, can use it to store the
|
|
151
|
+
* same text.
|
|
152
|
+
*
|
|
153
|
+
* @category schemas
|
|
154
|
+
* @since 0.8.0
|
|
155
|
+
*/
|
|
156
|
+
export const EventJson = Schema.fromJsonString(
|
|
157
|
+
Schema.Union([AssistantEvent.AssistantEvent, AssistantEvent.StoredAppEvent])
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Represents the options accepted by `layerSql`.
|
|
162
|
+
*
|
|
163
|
+
* @category models
|
|
164
|
+
* @since 0.8.0
|
|
165
|
+
*/
|
|
166
|
+
export interface SqlOptions {
|
|
167
|
+
/** The table's name. Defaults to `chat_log`. */
|
|
168
|
+
readonly table?: string | undefined
|
|
169
|
+
}
|
|
146
170
|
|
|
147
171
|
// Postgres returns BIGINT and DOUBLE PRECISION columns as strings; other databases as numbers.
|
|
148
172
|
const SqlNumber = Schema.Union([Schema.Finite, Schema.FiniteFromString])
|
|
@@ -154,12 +178,13 @@ const decodeLastSequence = Schema.decodeUnknownEffect(
|
|
|
154
178
|
)
|
|
155
179
|
|
|
156
180
|
/**
|
|
157
|
-
* Layer that stores chats in a SQL table
|
|
181
|
+
* Layer that stores chats in a SQL table, `chat_log` unless `table` names another.
|
|
158
182
|
*
|
|
159
183
|
* **Details**
|
|
160
184
|
*
|
|
161
185
|
* The table must already exist. It has the columns `chat_id`, `sequence`, `at`, and `event`, with the
|
|
162
|
-
* primary key `(chat_id, sequence)`. The layer requires a `SqlClient`.
|
|
186
|
+
* primary key `(chat_id, sequence)`. The layer requires a `SqlClient`. In MySQL, `event` needs
|
|
187
|
+
* `LONGTEXT`, since a tool's result can be larger than `TEXT` holds.
|
|
163
188
|
*
|
|
164
189
|
* **Example** (Storing chats in Postgres)
|
|
165
190
|
*
|
|
@@ -169,7 +194,7 @@ const decodeLastSequence = Schema.decodeUnknownEffect(
|
|
|
169
194
|
* import * as Config from "effect/Config"
|
|
170
195
|
* import * as Layer from "effect/Layer"
|
|
171
196
|
*
|
|
172
|
-
* const ChatLogLive = ChatLog.layerSql.pipe(
|
|
197
|
+
* const ChatLogLive = ChatLog.layerSql().pipe(
|
|
173
198
|
* Layer.provide(PgClient.layerConfig({ url: Config.Redacted("DATABASE_URL") }))
|
|
174
199
|
* )
|
|
175
200
|
* ```
|
|
@@ -189,64 +214,66 @@ const decodeLastSequence = Schema.decodeUnknownEffect(
|
|
|
189
214
|
* @category layers
|
|
190
215
|
* @since 0.1.0
|
|
191
216
|
*/
|
|
192
|
-
export const layerSql: Layer.Layer<ChatLog, never, SqlClient.SqlClient>
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
const decoded = yield* Schema.decodeUnknownEffect(Schema.Array(Row))(rows)
|
|
199
|
-
|
|
200
|
-
return decoded.map((row) =>
|
|
201
|
-
AssistantEvent.Entry.make({
|
|
202
|
-
chatId: row.chat_id,
|
|
203
|
-
sequence: row.sequence,
|
|
204
|
-
at: row.at,
|
|
205
|
-
event: row.event
|
|
206
|
-
})
|
|
207
|
-
)
|
|
208
|
-
})
|
|
217
|
+
export const layerSql = (options?: SqlOptions): Layer.Layer<ChatLog, never, SqlClient.SqlClient> =>
|
|
218
|
+
Layer.effect(
|
|
219
|
+
ChatLog,
|
|
220
|
+
Effect.gen(function* () {
|
|
221
|
+
const sql = (yield* SqlClient.SqlClient).withoutTransforms()
|
|
222
|
+
const table = sql(options?.table ?? "chat_log")
|
|
209
223
|
|
|
210
|
-
|
|
224
|
+
const toEntries = Effect.fnUntraced(function* (rows: ReadonlyArray<unknown>) {
|
|
225
|
+
const decoded = yield* Schema.decodeUnknownEffect(Schema.Array(Row))(rows)
|
|
211
226
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
227
|
+
return decoded.map((row) =>
|
|
228
|
+
AssistantEvent.Entry.make({
|
|
229
|
+
chatId: row.chat_id,
|
|
230
|
+
sequence: row.sequence,
|
|
231
|
+
at: row.at,
|
|
232
|
+
event: row.event
|
|
233
|
+
})
|
|
234
|
+
)
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
const encodeEvent = Schema.encodeEffect(EventJson)
|
|
238
|
+
|
|
239
|
+
return make({
|
|
240
|
+
read: Effect.fnUntraced(function* (chatId: ChatId, options?: ReadOptions) {
|
|
241
|
+
const rows = yield* sql`
|
|
242
|
+
SELECT chat_id, sequence, at, event FROM ${table} WHERE chat_id = ${chatId} AND sequence > ${options?.after ?? 0} ORDER BY sequence
|
|
216
243
|
`
|
|
217
244
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
SELECT MAX(sequence) AS sequence FROM
|
|
245
|
+
return yield* toEntries(rows)
|
|
246
|
+
}, Effect.orDie),
|
|
247
|
+
lastSequence: Effect.fnUntraced(function* (chatId: ChatId) {
|
|
248
|
+
const result = yield* sql`
|
|
249
|
+
SELECT MAX(sequence) AS sequence FROM ${table} WHERE chat_id = ${chatId}
|
|
223
250
|
`
|
|
224
251
|
|
|
225
|
-
|
|
252
|
+
const rows = yield* decodeLastSequence(result)
|
|
226
253
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
254
|
+
return rows[0]?.sequence ?? 0
|
|
255
|
+
}, Effect.orDie),
|
|
256
|
+
write: Effect.fnUntraced(function* (chatId: ChatId, entries: ReadonlyArray<AssistantEvent.Entry>) {
|
|
257
|
+
const rows = yield* Effect.forEach(
|
|
258
|
+
entries,
|
|
259
|
+
Effect.fnUntraced(function* (entry: AssistantEvent.Entry) {
|
|
260
|
+
const event = yield* encodeEvent(entry.event)
|
|
234
261
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
262
|
+
return { chat_id: chatId, sequence: entry.sequence, at: entry.at, event }
|
|
263
|
+
})
|
|
264
|
+
)
|
|
238
265
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
SELECT e.chat_id, e.sequence, e.at, e.event FROM
|
|
244
|
-
JOIN (SELECT chat_id, MAX(sequence) AS sequence FROM
|
|
266
|
+
yield* sql`INSERT INTO ${table} ${sql.insert(rows)}`
|
|
267
|
+
}, Effect.orDie),
|
|
268
|
+
lastEntries: Effect.gen(function* () {
|
|
269
|
+
const rows = yield* sql`
|
|
270
|
+
SELECT e.chat_id, e.sequence, e.at, e.event FROM ${table} e
|
|
271
|
+
JOIN (SELECT chat_id, MAX(sequence) AS sequence FROM ${table} GROUP BY chat_id) last
|
|
245
272
|
ON last.chat_id = e.chat_id AND last.sequence = e.sequence
|
|
246
273
|
`
|
|
247
274
|
|
|
248
|
-
|
|
249
|
-
|
|
275
|
+
return yield* toEntries(rows)
|
|
276
|
+
}).pipe(Effect.orDie)
|
|
277
|
+
})
|
|
250
278
|
})
|
|
251
|
-
|
|
252
|
-
)
|
|
279
|
+
)
|
package/src/internal/history.ts
CHANGED
|
@@ -321,8 +321,9 @@ const progressMessages = (entries: ReadonlyArray<AssistantEvent.AnyEntry>): Read
|
|
|
321
321
|
)
|
|
322
322
|
)
|
|
323
323
|
|
|
324
|
-
// The model said it was on it and stopped without the result:
|
|
325
|
-
//
|
|
324
|
+
// The model said it was on it and stopped without the result: its last message since the user's latest input was
|
|
325
|
+
// progress. An earlier message may have answered something else, such as an input that came before. A specialist still
|
|
326
|
+
// working doesn't count, since its report is what comes next.
|
|
326
327
|
/** @internal */
|
|
327
328
|
export const owesDelivery = (entries: ReadonlyArray<AssistantEvent.AnyEntry>, throughSequence: number): boolean => {
|
|
328
329
|
const latest = entries.findLast(
|
|
@@ -336,7 +337,7 @@ export const owesDelivery = (entries: ReadonlyArray<AssistantEvent.AnyEntry>, th
|
|
|
336
337
|
sequence > latest.sequence && AssistantEvent.isLibraryEvent(event) ? [event] : []
|
|
337
338
|
)
|
|
338
339
|
|
|
339
|
-
const
|
|
340
|
+
const last = after.findLast((event) => Predicate.isTagged(event, "AssistantMessage"))
|
|
340
341
|
const progress = progressMessages(entries)
|
|
341
342
|
const started = after.flatMap((event) => (Predicate.isTagged(event, "SpecialistStarted") ? [event.runId] : []))
|
|
342
343
|
|
|
@@ -348,8 +349,9 @@ export const owesDelivery = (entries: ReadonlyArray<AssistantEvent.AnyEntry>, th
|
|
|
348
349
|
|
|
349
350
|
// A call waiting for the user's approval is what comes next, like a specialist's report.
|
|
350
351
|
return (
|
|
351
|
-
|
|
352
|
-
|
|
352
|
+
last !== undefined &&
|
|
353
|
+
last.block === undefined &&
|
|
354
|
+
progress.has(last.id) &&
|
|
353
355
|
!after.some((event) => Predicate.isTagged(event, "TurnFailed") || Predicate.isTagged(event, "ApprovalRequested")) &&
|
|
354
356
|
started.every((runId) => finished.has(runId))
|
|
355
357
|
)
|