effect-assistant 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +22 -2
- package/dist/AppEvent.d.ts +111 -0
- package/dist/AppEvent.d.ts.map +1 -0
- package/dist/AppEvent.js +71 -0
- package/dist/AppEvent.js.map +1 -0
- package/dist/Assistant.d.ts +403 -0
- package/dist/Assistant.d.ts.map +1 -0
- package/dist/Assistant.js +126 -0
- package/dist/Assistant.js.map +1 -0
- package/dist/AssistantActivity.d.ts +85 -0
- package/dist/AssistantActivity.d.ts.map +1 -0
- package/dist/AssistantActivity.js +105 -0
- package/dist/AssistantActivity.js.map +1 -0
- package/dist/AssistantEngine.d.ts +116 -0
- package/dist/AssistantEngine.d.ts.map +1 -0
- package/dist/AssistantEngine.js +255 -0
- package/dist/AssistantEngine.js.map +1 -0
- package/dist/AssistantEvent.d.ts +551 -0
- package/dist/AssistantEvent.d.ts.map +1 -0
- package/dist/AssistantEvent.js +473 -0
- package/dist/AssistantEvent.js.map +1 -0
- package/dist/AssistantLoop.d.ts +173 -0
- package/dist/AssistantLoop.d.ts.map +1 -0
- package/dist/AssistantLoop.js +111 -0
- package/dist/AssistantLoop.js.map +1 -0
- package/dist/AssistantToolCall.d.ts +32 -0
- package/dist/AssistantToolCall.d.ts.map +1 -0
- package/dist/AssistantToolCall.js +37 -0
- package/dist/AssistantToolCall.js.map +1 -0
- package/dist/Block.d.ts +69 -0
- package/dist/Block.d.ts.map +1 -0
- package/dist/Block.js +39 -0
- package/dist/Block.js.map +1 -0
- package/dist/BlockKit.d.ts +180 -0
- package/dist/BlockKit.d.ts.map +1 -0
- package/dist/BlockKit.js +148 -0
- package/dist/BlockKit.js.map +1 -0
- package/dist/ChatId.d.ts +31 -0
- package/dist/ChatId.d.ts.map +1 -0
- package/dist/ChatId.js +36 -0
- package/dist/ChatId.js.map +1 -0
- package/dist/ChatLog.d.ts +109 -0
- package/dist/ChatLog.d.ts.map +1 -0
- package/dist/ChatLog.js +177 -0
- package/dist/ChatLog.js.map +1 -0
- package/dist/ChatView.d.ts +408 -0
- package/dist/ChatView.d.ts.map +1 -0
- package/dist/ChatView.js +443 -0
- package/dist/ChatView.js.map +1 -0
- package/dist/Chats.d.ts +110 -0
- package/dist/Chats.d.ts.map +1 -0
- package/dist/Chats.js +98 -0
- package/dist/Chats.js.map +1 -0
- package/dist/Compaction.d.ts +105 -0
- package/dist/Compaction.d.ts.map +1 -0
- package/dist/Compaction.js +126 -0
- package/dist/Compaction.js.map +1 -0
- package/dist/CurrentChat.d.ts +40 -0
- package/dist/CurrentChat.d.ts.map +1 -0
- package/dist/CurrentChat.js +33 -0
- package/dist/CurrentChat.js.map +1 -0
- package/dist/InputId.d.ts +38 -0
- package/dist/InputId.d.ts.map +1 -0
- package/dist/InputId.js +43 -0
- package/dist/InputId.js.map +1 -0
- package/dist/Instructions.d.ts +56 -0
- package/dist/Instructions.d.ts.map +1 -0
- package/dist/Instructions.js +50 -0
- package/dist/Instructions.js.map +1 -0
- package/dist/Memory.d.ts +190 -0
- package/dist/Memory.d.ts.map +1 -0
- package/dist/Memory.js +216 -0
- package/dist/Memory.js.map +1 -0
- package/dist/RunId.d.ts +41 -0
- package/dist/RunId.d.ts.map +1 -0
- package/dist/RunId.js +46 -0
- package/dist/RunId.js.map +1 -0
- package/dist/Skill.d.ts +72 -0
- package/dist/Skill.d.ts.map +1 -0
- package/dist/Skill.js +58 -0
- package/dist/Skill.js.map +1 -0
- package/dist/Specialist.d.ts +267 -0
- package/dist/Specialist.d.ts.map +1 -0
- package/dist/Specialist.js +119 -0
- package/dist/Specialist.js.map +1 -0
- package/dist/ToolDiscovery.d.ts +107 -0
- package/dist/ToolDiscovery.d.ts.map +1 -0
- package/dist/ToolDiscovery.js +140 -0
- package/dist/ToolDiscovery.js.map +1 -0
- package/dist/ToolFailure.d.ts +66 -0
- package/dist/ToolFailure.d.ts.map +1 -0
- package/dist/ToolFailure.js +79 -0
- package/dist/ToolFailure.js.map +1 -0
- package/dist/_virtual/_rolldown/runtime.js +13 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +24 -0
- package/dist/internal/chat.js +177 -0
- package/dist/internal/chat.js.map +1 -0
- package/dist/internal/events.d.ts +46 -0
- package/dist/internal/events.d.ts.map +1 -0
- package/dist/internal/events.js +65 -0
- package/dist/internal/events.js.map +1 -0
- package/dist/internal/history.js +147 -0
- package/dist/internal/history.js.map +1 -0
- package/dist/internal/instructions.js +11 -0
- package/dist/internal/instructions.js.map +1 -0
- package/dist/internal/loop.d.ts +73 -0
- package/dist/internal/loop.d.ts.map +1 -0
- package/dist/internal/loop.js +131 -0
- package/dist/internal/loop.js.map +1 -0
- package/dist/internal/schema.js +14 -0
- package/dist/internal/schema.js.map +1 -0
- package/dist/internal/skills.js +28 -0
- package/dist/internal/skills.js.map +1 -0
- package/dist/internal/specialists.js +272 -0
- package/dist/internal/specialists.js.map +1 -0
- package/dist/internal/telemetry.js +85 -0
- package/dist/internal/telemetry.js.map +1 -0
- package/dist/internal/toolkit.d.ts +62 -0
- package/dist/internal/toolkit.d.ts.map +1 -0
- package/dist/internal/toolkit.js +108 -0
- package/dist/internal/toolkit.js.map +1 -0
- package/dist/internal/tools.js +113 -0
- package/dist/internal/tools.js.map +1 -0
- package/dist/internal/turns.d.ts +64 -0
- package/dist/internal/turns.d.ts.map +1 -0
- package/dist/internal/turns.js +214 -0
- package/dist/internal/turns.js.map +1 -0
- package/dist/internal/uuid.js +45 -0
- package/dist/internal/uuid.js.map +1 -0
- package/dist/internal/writer.d.ts +12 -0
- package/dist/internal/writer.d.ts.map +1 -0
- package/package.json +52 -3
- package/src/AppEvent.ts +134 -0
- package/src/Assistant.ts +589 -0
- package/src/AssistantActivity.ts +150 -0
- package/src/AssistantEngine.ts +343 -0
- package/src/AssistantEvent.ts +569 -0
- package/src/AssistantLoop.ts +209 -0
- package/src/AssistantToolCall.ts +40 -0
- package/src/Block.ts +84 -0
- package/src/BlockKit.ts +298 -0
- package/src/ChatId.ts +36 -0
- package/src/ChatLog.ts +247 -0
- package/src/ChatView.ts +587 -0
- package/src/Chats.ts +122 -0
- package/src/Compaction.ts +207 -0
- package/src/CurrentChat.ts +47 -0
- package/src/InputId.ts +43 -0
- package/src/Instructions.ts +72 -0
- package/src/Memory.ts +276 -0
- package/src/RunId.ts +46 -0
- package/src/Skill.ts +91 -0
- package/src/Specialist.ts +355 -0
- package/src/ToolDiscovery.ts +179 -0
- package/src/ToolFailure.ts +74 -0
- package/src/index.ts +120 -0
- package/src/internal/chat.ts +323 -0
- package/src/internal/events.ts +100 -0
- package/src/internal/history.ts +294 -0
- package/src/internal/instructions.ts +9 -0
- package/src/internal/loop.ts +274 -0
- package/src/internal/schema.ts +25 -0
- package/src/internal/skills.ts +37 -0
- package/src/internal/specialists.ts +410 -0
- package/src/internal/telemetry.ts +125 -0
- package/src/internal/toolkit.ts +188 -0
- package/src/internal/tools.ts +159 -0
- package/src/internal/turns.ts +410 -0
- package/src/internal/uuid.ts +50 -0
- package/src/internal/writer.ts +12 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BlockKit.js","names":["AssistantEvent.isLibraryEvent","InputId.fromKey"],"sources":["../src/BlockKit.ts"],"sourcesContent":["/**\n * Groups the blocks an assistant may put in its messages, and renders them on the client.\n *\n * A kit is created with {@link make} and passed to `Assistant.make`. {@link fromEntries} computes\n * the state of every message's block from a chat's log, {@link closeStale} closes the blocks the\n * user moved on from, and {@link toClient} creates a client-side renderer with one function per\n * block kind.\n *\n * @since 0.1.0\n */\nimport * as Effect from \"effect/Effect\"\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 type * as Block from \"./Block.ts\"\nimport type * as ChatView from \"./ChatView.ts\"\nimport { CurrentChat } from \"./CurrentChat.ts\"\nimport * as InputId from \"./InputId.ts\"\n\n/**\n * Represents a catalog of blocks, with a lookup by kind.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface BlockKit<Blocks extends Block.Any = Block.Any> {\n readonly blocks: ReadonlyArray<Blocks>\n readonly byKind: ReadonlyMap<string, Blocks>\n}\n\n/**\n * Creates a kit from blocks.\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const make = <const Blocks extends ReadonlyArray<Block.Any>>(blocks: Blocks): BlockKit<Blocks[number]> => ({\n blocks,\n byKind: new Map(blocks.map((block) => [block.kind, block]))\n})\n\ntype KindOf<B> = B extends { readonly kind: infer K extends string } ? K : never\n\ntype BlockOf<Kit extends BlockKit, K> = Extract<Kit[\"blocks\"][number], { readonly kind: K }>\n\ntype PropsOf<B> = B extends { readonly props: Schema.Top } ? B[\"props\"][\"Type\"] : never\n\ntype AnswerOf<B> = B extends { readonly answer: infer A } ? (A extends Schema.Top ? A[\"Type\"] : never) : never\n\n/**\n * Represents the state of a message's block.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface State {\n readonly open: boolean\n /** The user's answer, if they used the block. */\n readonly answer: AssistantEvent.BlockAnswer | undefined\n}\n\n/**\n * Returns the block states after one more event.\n *\n * @category utils\n * @since 0.1.0\n */\nexport const apply = (\n states: ReadonlyMap<string, State>,\n event: { readonly _tag: string }\n): ReadonlyMap<string, State> => {\n if (!AssistantEvent.isLibraryEvent(event)) return states\n\n const close = (ids: ReadonlyArray<string>) => {\n const open = ids.filter((id) => states.get(id)?.open === true)\n\n if (open.length === 0) return states\n\n const next = new Map(states)\n\n for (const id of open) next.set(id, { open: false, answer: undefined })\n\n return next\n }\n\n if (Predicate.isTagged(event, \"BlocksClosed\")) return close(event.messageIds)\n\n if (Predicate.isTagged(event, \"AssistantMessage\")) {\n const closed = close(event.closes ?? [])\n\n return event.block === undefined ? closed : new Map(closed).set(event.id, { open: true, answer: undefined })\n }\n\n if (!Predicate.isTagged(event, \"BlockAnswer\") || states.get(event.messageId)?.open !== true) return states\n\n return new Map(states).set(event.messageId, { open: false, answer: event })\n}\n\n/**\n * Returns the state of every message's block, by message id.\n *\n * @category utils\n * @since 0.1.0\n */\nexport const fromEntries = (entries: ReadonlyArray<AssistantEvent.AnyEntry>): ReadonlyMap<string, State> => {\n let states: ReadonlyMap<string, State> = new Map()\n\n for (const entry of entries) states = apply(states, entry.event)\n\n return states\n}\n\n/**\n * Returns the ids of the messages whose blocks are still open after the user typed the given\n * number of messages without using them.\n *\n * **Details**\n *\n * These are the blocks {@link closeStale} closes.\n *\n * @category utils\n * @since 0.1.0\n */\nexport const staleBlocks = (\n entries: ReadonlyArray<AssistantEvent.AnyEntry>,\n options: { readonly messages: number }\n): ReadonlyArray<string> => {\n const open = [...fromEntries(entries)].flatMap(([id, state]) => (state.open ? [id] : []))\n\n return open.filter((id) => {\n const shown = entries.findIndex(\n ({ event }) =>\n AssistantEvent.isLibraryEvent(event) && Predicate.isTagged(event, \"AssistantMessage\") && event.id === id\n )\n\n if (shown === -1) return false\n\n const typed = entries.slice(shown + 1).filter(({ event }) => Predicate.isTagged(event, \"UserMessage\")).length\n\n return typed >= options.messages\n })\n}\n\n/**\n * Closes the blocks the user moved on from, which are still open after the given number of typed\n * messages.\n *\n * **Details**\n *\n * Run it from a turn, before or after its loop. It records a `BlocksClosed` event, so every\n * client folds the blocks closed.\n *\n * @see {@link staleBlocks} for the blocks it closes\n *\n * @category utils\n * @since 0.1.0\n */\nexport const closeStale = Effect.fn(function* (options: { readonly messages: number }) {\n const chat = yield* CurrentChat\n const stale = staleBlocks(yield* chat.entries, options)\n\n if (stale.length > 0) yield* chat.append(AssistantEvent.BlocksClosed.make({ messageIds: stale }))\n})\n\n/**\n * Represents what a renderer receives for one block.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface RenderArgs<B> {\n /**\n * The id of the message the block is on. A block whose button calls your API can key the work\n * on it, with the chat's id, so the work runs once per block.\n */\n readonly messageId: string\n readonly text: string\n readonly props: PropsOf<B>\n /**\n * Whether the block is open. It is `false` once the block is answered, with the block or in\n * words; show it read-only then.\n */\n readonly open: boolean\n /** The user's answer, if they used the block. */\n readonly answer: AnswerOf<B> | undefined\n /** Sends an answer. Only call it while `open`. */\n readonly respond: (answer: AnswerOf<B>) => void\n}\n\n/**\n * Represents a renderer for every kind in a kit.\n *\n * @category models\n * @since 0.1.0\n */\nexport type Renderers<Kit extends BlockKit, Out> = {\n readonly [K in KindOf<Kit[\"blocks\"][number]>]: (args: RenderArgs<BlockOf<Kit, K>>) => Out\n}\n\n/**\n * Represents the options for rendering one message's block.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface RenderOptions {\n /** Submits an answer, for example over RPC. */\n readonly respond: (answer: AssistantEvent.BlockAnswer) => void\n}\n\n/**\n * Creates a client-side renderer for the blocks on a chat view's messages\n * (`ChatView.AssistantMessage`).\n *\n * **Details**\n *\n * The renderers are type-checked, so every kind in the kit needs one, and each receives typed\n * `props` and `answer`. A renderer's `respond` builds a `BlockAnswer` input, with the answer\n * encoded by the block's answer schema, and passes it to the `respond` option to submit.\n *\n * The returned function returns `undefined` for a message without a block, or with a block it\n * can't decode or has no renderer for.\n *\n * **Example** (Rendering blocks with React)\n *\n * ```tsx\n * import { BlockKit } from \"effect-assistant\"\n * import type { ReactNode } from \"react\"\n *\n * const Blocks = BlockKit.make([PickOne, OrderDraft])\n *\n * const renderBlock = BlockKit.toClient<typeof Blocks, ReactNode>(Blocks, {\n * pick_one: ({ props, open, respond }) =>\n * props.options.map((option) => (\n * <button key={option} disabled={!open} onClick={() => respond({ picked: option })}>\n * {option}\n * </button>\n * )),\n * // No answer schema: the button calls the app's own API with the props the user sees.\n * order_draft: ({ props }) => <button onClick={() => placeOrder(props)}>Place order</button>\n * })\n *\n * // For each AssistantMessage in the chat's view:\n * renderBlock(message, { respond: (answer) => submit(answer) })\n * ```\n *\n * @category constructors\n * @since 0.1.0\n */\nexport function toClient<Kit extends BlockKit, Out>(\n kit: Kit,\n renderers: Renderers<Kit, Out>\n): (message: ChatView.AssistantMessage, options: RenderOptions) => Out | undefined\n// The overload above checks that every kind has a renderer; here each one takes any block's\n// arguments.\nexport function toClient<Out>(\n kit: BlockKit,\n renderers: Readonly<Record<string, (args: RenderArgs<Block.Any>) => Out>>\n): (message: ChatView.AssistantMessage, options: RenderOptions) => Out | undefined {\n return (message, options) => {\n if (message.block === undefined) return undefined\n\n const { kind } = message.block\n const block = kit.byKind.get(kind)\n const render = renderers[kind]\n\n if (block === undefined || render === undefined) return undefined\n\n const props = Schema.decodeUnknownOption(block.props)(message.block.props)\n\n if (Option.isNone(props)) return undefined\n\n const given = message.block.answer\n const answerCodec = block.answer === undefined ? undefined : Schema.toCodecJson(block.answer)\n\n const answer =\n given === undefined || answerCodec === undefined ? Option.none() : Schema.decodeUnknownOption(answerCodec)(given)\n\n return render({\n messageId: message.id,\n text: message.text,\n props: props.value,\n open: message.block.status === \"open\",\n answer: Option.getOrUndefined(answer),\n respond: (value) =>\n options.respond(\n AssistantEvent.BlockAnswer.make({\n // A block takes one answer: the same id every time, so a second click is stored once.\n id: InputId.fromKey(`${message.id}:answer`),\n messageId: message.id,\n kind,\n value: answerCodec === undefined ? value : Schema.encodeUnknownSync(answerCodec)(value)\n })\n )\n })\n }\n}\n"],"mappings":"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"}
|
package/dist/ChatId.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as Schema from "effect/Schema";
|
|
3
|
+
declare namespace ChatId_d_exports {
|
|
4
|
+
export { ChatId, make };
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Schema for chat ids of the form `` `chat:${uuid}` ``. The schema is branded, so a run id or a
|
|
8
|
+
* plain string can't be passed where a chat id is required.
|
|
9
|
+
*
|
|
10
|
+
* @category schemas
|
|
11
|
+
* @since 0.1.0
|
|
12
|
+
*/
|
|
13
|
+
export declare const ChatId: Schema.brand<Schema.TemplateLiteral<readonly ["chat:", Schema.String]>, "~effect-assistant/ChatId">;
|
|
14
|
+
/**
|
|
15
|
+
* Represents a chat id.
|
|
16
|
+
*
|
|
17
|
+
* @category models
|
|
18
|
+
* @since 0.1.0
|
|
19
|
+
*/
|
|
20
|
+
export type ChatId = typeof ChatId.Type;
|
|
21
|
+
/**
|
|
22
|
+
* Creates a new random chat id using Effect's `Random`. Seed `Random` in tests to get the same ids
|
|
23
|
+
* on every run.
|
|
24
|
+
*
|
|
25
|
+
* @category constructors
|
|
26
|
+
* @since 0.1.0
|
|
27
|
+
*/
|
|
28
|
+
export declare const make: () => Effect.Effect<ChatId>;
|
|
29
|
+
//#endregion
|
|
30
|
+
export { ChatId_d_exports };
|
|
31
|
+
//# sourceMappingURL=ChatId.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ChatId.d.ts","names":[],"sources":["../src/ChatId.ts"],"mappings":";;;;;;;;;;;;qBAkBa,QAAM,OAAA,MAAA,OAAA,mCAAA,OAAA;;;;;;;YAQP,gBAAgB,OAAO;;;;;;;;qBAStB,YAAW,OAAO,OAAO"}
|
package/dist/ChatId.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { __exportAll } from "./_virtual/_rolldown/runtime.js";
|
|
2
|
+
import { Uuid, make as make$1 } from "./internal/uuid.js";
|
|
3
|
+
import * as Effect from "effect/Effect";
|
|
4
|
+
import * as Schema from "effect/Schema";
|
|
5
|
+
//#region src/ChatId.ts
|
|
6
|
+
/**
|
|
7
|
+
* This module provides the id of a chat, which is `chat:` followed by a UUID.
|
|
8
|
+
*
|
|
9
|
+
* A chat id keys everything about a chat: its log, its cluster entity, and its trace session.
|
|
10
|
+
*
|
|
11
|
+
* @since 0.1.0
|
|
12
|
+
*/
|
|
13
|
+
var ChatId_exports = /* @__PURE__ */ __exportAll({
|
|
14
|
+
ChatId: () => ChatId,
|
|
15
|
+
make: () => make
|
|
16
|
+
});
|
|
17
|
+
/**
|
|
18
|
+
* Schema for chat ids of the form `` `chat:${uuid}` ``. The schema is branded, so a run id or a
|
|
19
|
+
* plain string can't be passed where a chat id is required.
|
|
20
|
+
*
|
|
21
|
+
* @category schemas
|
|
22
|
+
* @since 0.1.0
|
|
23
|
+
*/
|
|
24
|
+
const ChatId = Schema.TemplateLiteral(["chat:", Uuid]).pipe(Schema.brand("~effect-assistant/ChatId"));
|
|
25
|
+
/**
|
|
26
|
+
* Creates a new random chat id using Effect's `Random`. Seed `Random` in tests to get the same ids
|
|
27
|
+
* on every run.
|
|
28
|
+
*
|
|
29
|
+
* @category constructors
|
|
30
|
+
* @since 0.1.0
|
|
31
|
+
*/
|
|
32
|
+
const make = () => Effect.map(make$1, (uuid) => ChatId.make(`chat:${uuid}`));
|
|
33
|
+
//#endregion
|
|
34
|
+
export { ChatId, ChatId_exports, make };
|
|
35
|
+
|
|
36
|
+
//# sourceMappingURL=ChatId.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ChatId.js","names":["Uuid.Uuid","Uuid.make"],"sources":["../src/ChatId.ts"],"sourcesContent":["/**\n * This module provides the id of a chat, which is `chat:` followed by a UUID.\n *\n * A chat id keys everything about a chat: its log, its cluster entity, and its trace session.\n *\n * @since 0.1.0\n */\nimport * as Effect from \"effect/Effect\"\nimport * as Schema from \"effect/Schema\"\nimport * as Uuid from \"./internal/uuid.ts\"\n\n/**\n * Schema for chat ids of the form `` `chat:${uuid}` ``. The schema is branded, so a run id or a\n * plain string can't be passed where a chat id is required.\n *\n * @category schemas\n * @since 0.1.0\n */\nexport const ChatId = Schema.TemplateLiteral([\"chat:\", Uuid.Uuid]).pipe(Schema.brand(\"~effect-assistant/ChatId\"))\n\n/**\n * Represents a chat id.\n *\n * @category models\n * @since 0.1.0\n */\nexport type ChatId = typeof ChatId.Type\n\n/**\n * Creates a new random chat id using Effect's `Random`. Seed `Random` in tests to get the same ids\n * on every run.\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const make = (): Effect.Effect<ChatId> => Effect.map(Uuid.make, (uuid) => ChatId.make(`chat:${uuid}`))\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAkBA,MAAa,SAAS,OAAO,gBAAgB,CAAC,SAASA,IAAS,CAAC,CAAC,CAAC,KAAK,OAAO,MAAM,0BAA0B,CAAC;;;;;;;;AAiBhH,MAAa,aAAoC,OAAO,IAAIC,SAAY,SAAS,OAAO,KAAK,QAAQ,MAAM,CAAC"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { ChatId } from "./ChatId.js";
|
|
2
|
+
import { AssistantEvent, AssistantEvent_d_exports, Entry, StoredAppEvent } from "./AssistantEvent.js";
|
|
3
|
+
import * as Context from "effect/Context";
|
|
4
|
+
import * as Effect from "effect/Effect";
|
|
5
|
+
import * as Layer from "effect/Layer";
|
|
6
|
+
import * as SqlClient from "effect/sql/SqlClient";
|
|
7
|
+
declare namespace ChatLog_d_exports {
|
|
8
|
+
export { ChatLog, ReadOptions, Storage, layerMemory, layerSql, make };
|
|
9
|
+
}
|
|
10
|
+
declare const ChatLog_base: Context.ServiceClass<ChatLog, "effect-assistant/ChatLog", {
|
|
11
|
+
/** Appends events to a chat and returns them as stored entries. */
|
|
12
|
+
readonly append: (chatId: ChatId, events: ReadonlyArray<AssistantEvent | StoredAppEvent>) => Effect.Effect<ReadonlyArray<Entry>>;
|
|
13
|
+
/** Reads a chat's stored entries in order, either all of them or only those after a `seq`. */
|
|
14
|
+
readonly read: (chatId: ChatId, options?: ReadOptions) => Effect.Effect<ReadonlyArray<Entry>>;
|
|
15
|
+
/**
|
|
16
|
+
* Returns the chats whose last entry ends neither a turn nor the chat. After a restart, these
|
|
17
|
+
* are the chats that may still owe a turn.
|
|
18
|
+
*/
|
|
19
|
+
readonly unfinished: Effect.Effect<ReadonlyArray<ChatId>>;
|
|
20
|
+
}>;
|
|
21
|
+
/**
|
|
22
|
+
* Service that stores each chat's events as an ordered list of entries.
|
|
23
|
+
*
|
|
24
|
+
* @category services
|
|
25
|
+
* @since 0.1.0
|
|
26
|
+
*/
|
|
27
|
+
export declare class ChatLog extends ChatLog_base {}
|
|
28
|
+
/**
|
|
29
|
+
* Represents the options that select which of a chat's entries to read.
|
|
30
|
+
*
|
|
31
|
+
* @category models
|
|
32
|
+
* @since 0.1.0
|
|
33
|
+
*/
|
|
34
|
+
export interface ReadOptions {
|
|
35
|
+
/** Reads only the entries after this `seq`. */
|
|
36
|
+
readonly after?: number | undefined;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Represents the storage that a `ChatLog` is built on.
|
|
40
|
+
*
|
|
41
|
+
* A storage reads a chat's entries, finds a chat's last `seq`, writes new entries, and finds every
|
|
42
|
+
* chat's last entry. Entries are never removed, so a deleted chat keeps its log, which ends in
|
|
43
|
+
* `ChatDeleted`.
|
|
44
|
+
*
|
|
45
|
+
* @category models
|
|
46
|
+
* @since 0.1.0
|
|
47
|
+
*/
|
|
48
|
+
export interface Storage {
|
|
49
|
+
readonly read: (chatId: ChatId, options?: ReadOptions) => Effect.Effect<ReadonlyArray<Entry>>;
|
|
50
|
+
readonly lastSeq: (chatId: ChatId) => Effect.Effect<number>;
|
|
51
|
+
readonly write: (chatId: ChatId, entries: ReadonlyArray<Entry>) => Effect.Effect<void>;
|
|
52
|
+
/** Returns the last entry of every chat. */
|
|
53
|
+
readonly lastEntries: Effect.Effect<ReadonlyArray<Entry>>;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Creates a `ChatLog` from a {@link Storage}. `append` numbers new entries after the chat's last
|
|
57
|
+
* `seq` and stamps them with the current time.
|
|
58
|
+
*
|
|
59
|
+
* @category constructors
|
|
60
|
+
* @since 0.1.0
|
|
61
|
+
*/
|
|
62
|
+
export declare const make: (storage: Storage) => ChatLog["Service"];
|
|
63
|
+
/**
|
|
64
|
+
* Layer that keeps every chat in memory. The log is lost when the process stops.
|
|
65
|
+
*
|
|
66
|
+
* @category layers
|
|
67
|
+
* @since 0.1.0
|
|
68
|
+
*/
|
|
69
|
+
export declare const layerMemory: Layer.Layer<ChatLog>;
|
|
70
|
+
/**
|
|
71
|
+
* Layer that stores chats in a SQL table named `chat_log`.
|
|
72
|
+
*
|
|
73
|
+
* **Details**
|
|
74
|
+
*
|
|
75
|
+
* The table must already exist. It has the columns `chat_id`, `seq`, `at`, and `event`, with the
|
|
76
|
+
* primary key `(chat_id, seq)`. The layer requires a `SqlClient`.
|
|
77
|
+
*
|
|
78
|
+
* **Example** (Storing chats in Postgres)
|
|
79
|
+
*
|
|
80
|
+
* ```ts
|
|
81
|
+
* import * as PgClient from "@effect/sql-pg/PgClient"
|
|
82
|
+
* import { ChatLog } from "effect-assistant"
|
|
83
|
+
* import * as Config from "effect/Config"
|
|
84
|
+
* import * as Layer from "effect/Layer"
|
|
85
|
+
*
|
|
86
|
+
* const ChatLogLive = ChatLog.layerSql.pipe(
|
|
87
|
+
* Layer.provide(PgClient.layerConfig({ url: Config.Redacted("DATABASE_URL") }))
|
|
88
|
+
* )
|
|
89
|
+
* ```
|
|
90
|
+
*
|
|
91
|
+
* The table, in Postgres:
|
|
92
|
+
*
|
|
93
|
+
* ```sql
|
|
94
|
+
* CREATE TABLE chat_log (
|
|
95
|
+
* chat_id TEXT NOT NULL,
|
|
96
|
+
* seq INTEGER NOT NULL,
|
|
97
|
+
* at DOUBLE PRECISION NOT NULL,
|
|
98
|
+
* event TEXT NOT NULL,
|
|
99
|
+
* PRIMARY KEY (chat_id, seq)
|
|
100
|
+
* );
|
|
101
|
+
* ```
|
|
102
|
+
*
|
|
103
|
+
* @category layers
|
|
104
|
+
* @since 0.1.0
|
|
105
|
+
*/
|
|
106
|
+
export declare const layerSql: Layer.Layer<ChatLog, never, SqlClient.SqlClient>;
|
|
107
|
+
//#endregion
|
|
108
|
+
export { ChatLog_d_exports };
|
|
109
|
+
//# sourceMappingURL=ChatLog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
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,UAAU,QAAQ,WAAW,OAAO;WACpC,QAAQ,QAAQ,QAAQ,SAAS,cAAc,WAA0B,OAAO;;WAEhF,aAAa,OAAO,OAAO,cAAc;;;;;;;;;qBAUvC,OAAI,SAAa,YAAU;;;;;;;qBAiC3B,aAAa,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAwEzB,UAAU,MAAM,MAAM,gBAAgB,UAAU"}
|
package/dist/ChatLog.js
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { __exportAll } from "./_virtual/_rolldown/runtime.js";
|
|
2
|
+
import { ChatId } from "./ChatId.js";
|
|
3
|
+
import { AssistantEvent, Entry, StoredAppEvent, isTurnEnd } from "./AssistantEvent.js";
|
|
4
|
+
import * as Predicate from "effect/Predicate";
|
|
5
|
+
import * as Context from "effect/Context";
|
|
6
|
+
import * as Effect from "effect/Effect";
|
|
7
|
+
import * as Layer from "effect/Layer";
|
|
8
|
+
import * as Schema from "effect/Schema";
|
|
9
|
+
import * as Clock from "effect/Clock";
|
|
10
|
+
import * as Ref from "effect/Ref";
|
|
11
|
+
import * as SqlClient from "effect/sql/SqlClient";
|
|
12
|
+
//#region src/ChatLog.ts
|
|
13
|
+
/**
|
|
14
|
+
* This module provides the `ChatLog` service, which stores each chat as an append-only list of
|
|
15
|
+
* `AssistantEvent`s.
|
|
16
|
+
*
|
|
17
|
+
* A log is built on a {@link Storage}. {@link layerMemory} keeps every chat in memory, for tests
|
|
18
|
+
* and local runs, and {@link layerSql} stores chats in a SQL table so they survive restarts. To
|
|
19
|
+
* use another store, implement a `Storage` and pass it to {@link make}.
|
|
20
|
+
*
|
|
21
|
+
* @since 0.1.0
|
|
22
|
+
*/
|
|
23
|
+
var ChatLog_exports = /* @__PURE__ */ __exportAll({
|
|
24
|
+
ChatLog: () => ChatLog,
|
|
25
|
+
layerMemory: () => layerMemory,
|
|
26
|
+
layerSql: () => layerSql,
|
|
27
|
+
make: () => make
|
|
28
|
+
});
|
|
29
|
+
/**
|
|
30
|
+
* Service that stores each chat's events as an ordered list of entries.
|
|
31
|
+
*
|
|
32
|
+
* @category services
|
|
33
|
+
* @since 0.1.0
|
|
34
|
+
*/
|
|
35
|
+
var ChatLog = class extends Context.Service()("effect-assistant/ChatLog") {};
|
|
36
|
+
/**
|
|
37
|
+
* Creates a `ChatLog` from a {@link Storage}. `append` numbers new entries after the chat's last
|
|
38
|
+
* `seq` and stamps them with the current time.
|
|
39
|
+
*
|
|
40
|
+
* @category constructors
|
|
41
|
+
* @since 0.1.0
|
|
42
|
+
*/
|
|
43
|
+
const make = (storage) => ChatLog.of({
|
|
44
|
+
read: storage.read,
|
|
45
|
+
append: Effect.fn("ChatLog.append")(function* (chatId, events) {
|
|
46
|
+
if (events.length === 0) return [];
|
|
47
|
+
const seq = yield* storage.lastSeq(chatId);
|
|
48
|
+
const at = yield* Clock.currentTimeMillis;
|
|
49
|
+
const entries = events.map((event, n) => Entry.make({
|
|
50
|
+
chatId,
|
|
51
|
+
seq: seq + n + 1,
|
|
52
|
+
at,
|
|
53
|
+
event
|
|
54
|
+
}));
|
|
55
|
+
yield* storage.write(chatId, entries);
|
|
56
|
+
return entries;
|
|
57
|
+
}),
|
|
58
|
+
unfinished: Effect.gen(function* () {
|
|
59
|
+
return (yield* storage.lastEntries).flatMap((entry) => isTurnEnd(entry.event) || Predicate.isTagged(entry.event, "ChatDeleted") ? [] : [entry.chatId]);
|
|
60
|
+
})
|
|
61
|
+
});
|
|
62
|
+
/**
|
|
63
|
+
* Layer that keeps every chat in memory. The log is lost when the process stops.
|
|
64
|
+
*
|
|
65
|
+
* @category layers
|
|
66
|
+
* @since 0.1.0
|
|
67
|
+
*/
|
|
68
|
+
const layerMemory = Layer.effect(ChatLog, Effect.gen(function* () {
|
|
69
|
+
const chats = yield* Ref.make(/* @__PURE__ */ new Map());
|
|
70
|
+
return make({
|
|
71
|
+
read: Effect.fnUntraced(function* (chatId, options) {
|
|
72
|
+
return ((yield* Ref.get(chats)).get(chatId) ?? []).filter((entry) => entry.seq > (options?.after ?? 0));
|
|
73
|
+
}),
|
|
74
|
+
lastSeq: Effect.fnUntraced(function* (chatId) {
|
|
75
|
+
return (yield* Ref.get(chats)).get(chatId)?.at(-1)?.seq ?? 0;
|
|
76
|
+
}),
|
|
77
|
+
write: (chatId, entries) => Ref.update(chats, (all) => new Map(all).set(chatId, [...all.get(chatId) ?? [], ...entries])),
|
|
78
|
+
lastEntries: Effect.gen(function* () {
|
|
79
|
+
return [...(yield* Ref.get(chats)).values()].flatMap((entries) => entries.slice(-1));
|
|
80
|
+
})
|
|
81
|
+
});
|
|
82
|
+
}));
|
|
83
|
+
const EventJson = Schema.fromJsonString(Schema.Union([AssistantEvent, StoredAppEvent]));
|
|
84
|
+
const SqlNumber = Schema.Union([Schema.Finite, Schema.FiniteFromString]);
|
|
85
|
+
const Row = Schema.Struct({
|
|
86
|
+
chat_id: ChatId,
|
|
87
|
+
seq: SqlNumber,
|
|
88
|
+
at: SqlNumber,
|
|
89
|
+
event: EventJson
|
|
90
|
+
});
|
|
91
|
+
const decodeLastSeq = Schema.decodeUnknownEffect(Schema.Array(Schema.Struct({ seq: Schema.NullOr(SqlNumber) })));
|
|
92
|
+
/**
|
|
93
|
+
* Layer that stores chats in a SQL table named `chat_log`.
|
|
94
|
+
*
|
|
95
|
+
* **Details**
|
|
96
|
+
*
|
|
97
|
+
* The table must already exist. It has the columns `chat_id`, `seq`, `at`, and `event`, with the
|
|
98
|
+
* primary key `(chat_id, seq)`. The layer requires a `SqlClient`.
|
|
99
|
+
*
|
|
100
|
+
* **Example** (Storing chats in Postgres)
|
|
101
|
+
*
|
|
102
|
+
* ```ts
|
|
103
|
+
* import * as PgClient from "@effect/sql-pg/PgClient"
|
|
104
|
+
* import { ChatLog } from "effect-assistant"
|
|
105
|
+
* import * as Config from "effect/Config"
|
|
106
|
+
* import * as Layer from "effect/Layer"
|
|
107
|
+
*
|
|
108
|
+
* const ChatLogLive = ChatLog.layerSql.pipe(
|
|
109
|
+
* Layer.provide(PgClient.layerConfig({ url: Config.Redacted("DATABASE_URL") }))
|
|
110
|
+
* )
|
|
111
|
+
* ```
|
|
112
|
+
*
|
|
113
|
+
* The table, in Postgres:
|
|
114
|
+
*
|
|
115
|
+
* ```sql
|
|
116
|
+
* CREATE TABLE chat_log (
|
|
117
|
+
* chat_id TEXT NOT NULL,
|
|
118
|
+
* seq INTEGER NOT NULL,
|
|
119
|
+
* at DOUBLE PRECISION NOT NULL,
|
|
120
|
+
* event TEXT NOT NULL,
|
|
121
|
+
* PRIMARY KEY (chat_id, seq)
|
|
122
|
+
* );
|
|
123
|
+
* ```
|
|
124
|
+
*
|
|
125
|
+
* @category layers
|
|
126
|
+
* @since 0.1.0
|
|
127
|
+
*/
|
|
128
|
+
const layerSql = Layer.effect(ChatLog, Effect.gen(function* () {
|
|
129
|
+
const sql = (yield* SqlClient.SqlClient).withoutTransforms();
|
|
130
|
+
const toEntries = Effect.fnUntraced(function* (rows) {
|
|
131
|
+
return (yield* Schema.decodeUnknownEffect(Schema.Array(Row))(rows)).map((row) => Entry.make({
|
|
132
|
+
chatId: row.chat_id,
|
|
133
|
+
seq: row.seq,
|
|
134
|
+
at: row.at,
|
|
135
|
+
event: row.event
|
|
136
|
+
}));
|
|
137
|
+
});
|
|
138
|
+
const encodeEvent = Schema.encodeEffect(EventJson);
|
|
139
|
+
return make({
|
|
140
|
+
read: Effect.fnUntraced(function* (chatId, options) {
|
|
141
|
+
const rows = yield* sql`
|
|
142
|
+
SELECT chat_id, seq, at, event FROM chat_log WHERE chat_id = ${chatId} AND seq > ${options?.after ?? 0} ORDER BY seq
|
|
143
|
+
`;
|
|
144
|
+
return yield* toEntries(rows);
|
|
145
|
+
}, Effect.orDie),
|
|
146
|
+
lastSeq: Effect.fnUntraced(function* (chatId) {
|
|
147
|
+
const result = yield* sql`
|
|
148
|
+
SELECT MAX(seq) AS seq FROM chat_log WHERE chat_id = ${chatId}
|
|
149
|
+
`;
|
|
150
|
+
return (yield* decodeLastSeq(result))[0]?.seq ?? 0;
|
|
151
|
+
}, Effect.orDie),
|
|
152
|
+
write: Effect.fnUntraced(function* (chatId, entries) {
|
|
153
|
+
const rows = yield* Effect.forEach(entries, Effect.fnUntraced(function* (entry) {
|
|
154
|
+
const event = yield* encodeEvent(entry.event);
|
|
155
|
+
return {
|
|
156
|
+
chat_id: chatId,
|
|
157
|
+
seq: entry.seq,
|
|
158
|
+
at: entry.at,
|
|
159
|
+
event
|
|
160
|
+
};
|
|
161
|
+
}));
|
|
162
|
+
yield* sql`INSERT INTO chat_log ${sql.insert(rows)}`;
|
|
163
|
+
}, Effect.orDie),
|
|
164
|
+
lastEntries: Effect.gen(function* () {
|
|
165
|
+
const rows = yield* sql`
|
|
166
|
+
SELECT e.chat_id, e.seq, e.at, e.event FROM chat_log e
|
|
167
|
+
JOIN (SELECT chat_id, MAX(seq) AS seq FROM chat_log GROUP BY chat_id) last
|
|
168
|
+
ON last.chat_id = e.chat_id AND last.seq = e.seq
|
|
169
|
+
`;
|
|
170
|
+
return yield* toEntries(rows);
|
|
171
|
+
}).pipe(Effect.orDie)
|
|
172
|
+
});
|
|
173
|
+
}));
|
|
174
|
+
//#endregion
|
|
175
|
+
export { ChatLog, ChatLog_exports, layerMemory, layerSql, make };
|
|
176
|
+
|
|
177
|
+
//# sourceMappingURL=ChatLog.js.map
|
|
@@ -0,0 +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 `seq`. */\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 `seq`. */\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 `seq`, 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 lastSeq: (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 * `seq` 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 seq = yield* storage.lastSeq(chatId)\n const at = yield* Clock.currentTimeMillis\n const entries = events.map((event, n) => AssistantEvent.Entry.make({ chatId, seq: seq + n + 1, at, event }))\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.seq > (options?.after ?? 0))\n }),\n lastSeq: Effect.fnUntraced(function* (chatId: ChatId) {\n const all = yield* Ref.get(chats)\n\n return all.get(chatId)?.at(-1)?.seq ?? 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, seq: SqlNumber, at: SqlNumber, event: EventJson })\n\nconst decodeLastSeq = Schema.decodeUnknownEffect(Schema.Array(Schema.Struct({ seq: Schema.NullOr(SqlNumber) })))\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`, `seq`, `at`, and `event`, with the\n * primary key `(chat_id, seq)`. 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 * seq INTEGER NOT NULL,\n * at DOUBLE PRECISION NOT NULL,\n * event TEXT NOT NULL,\n * PRIMARY KEY (chat_id, seq)\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 seq: row.seq,\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, seq, at, event FROM chat_log WHERE chat_id = ${chatId} AND seq > ${options?.after ?? 0} ORDER BY seq\n `\n\n return yield* toEntries(rows)\n }, Effect.orDie),\n lastSeq: Effect.fnUntraced(function* (chatId: ChatId) {\n const result = yield* sql`\n SELECT MAX(seq) AS seq FROM chat_log WHERE chat_id = ${chatId}\n `\n\n const rows = yield* decodeLastSeq(result)\n\n return rows[0]?.seq ?? 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, seq: entry.seq, 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.seq, e.at, e.event FROM chat_log e\n JOIN (SELECT chat_id, MAX(seq) AS seq FROM chat_log GROUP BY chat_id) last\n ON last.chat_id = e.chat_id AND last.seq = e.seq\n `\n\n return yield* toEntries(rows)\n }).pipe(Effect.orDie)\n })\n })\n)\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BA,IAAa,UAAb,cAA6B,QAAQ,QAgBnC,CAAC,CAAC,0BAA0B,CAAC,CAAC,CAAC;;;;;;;;AAsCjC,MAAa,QAAQ,YACnB,QAAQ,GAAG;CACT,MAAM,QAAQ;CACd,QAAQ,OAAO,GAAG,gBAAgB,CAAC,CAAC,WAClC,QACA,QACA;EACA,IAAI,OAAO,WAAW,GAAG,OAAO,CAAC;EAEjC,MAAM,MAAM,OAAO,QAAQ,QAAQ,MAAM;EACzC,MAAM,KAAK,OAAO,MAAM;EACxB,MAAM,UAAU,OAAO,KAAK,OAAO,MAAA,MAA2B,KAAK;GAAE;GAAQ,KAAK,MAAM,IAAI;GAAG;GAAI;EAAM,CAAC,CAAC;EAE3G,OAAO,QAAQ,MAAM,QAAQ,OAAO;EAEpC,OAAO;CACT,CAAC;CACD,YAAY,OAAO,IAAI,aAAa;EAIlC,QAAO,OAHa,QAAQ,YAAA,CAGhB,SAAS,UACnBA,UAAyB,MAAM,KAAK,KAAK,UAAU,SAAS,MAAM,OAAO,aAAa,IAAI,CAAC,IAAI,CAAC,MAAM,MAAM,CAC9G;CACF,CAAC;AACH,CAAC;;;;;;;AAQH,MAAa,cAAoC,MAAM,OACrD,SACA,OAAO,IAAI,aAAa;CACtB,MAAM,QAAQ,OAAO,IAAI,qBAAK,IAAI,IAAiD,CAAC;CAEpF,OAAO,KAAK;EACV,MAAM,OAAO,WAAW,WAAW,QAAgB,SAAuB;GAGxE,SAAQ,OAFW,IAAI,IAAI,KAAK,EAAA,CAEpB,IAAI,MAAM,KAAK,CAAC,EAAA,CAAG,QAAQ,UAAU,MAAM,OAAO,SAAS,SAAS,EAAE;EACpF,CAAC;EACD,SAAS,OAAO,WAAW,WAAW,QAAgB;GAGpD,QAAO,OAFY,IAAI,IAAI,KAAK,EAAA,CAErB,IAAI,MAAM,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,OAAO;EACzC,CAAC;EACD,QAAQ,QAAQ,YACd,IAAI,OAAO,QAAQ,QAAQ,IAAI,IAAI,GAAG,CAAC,CAAC,IAAI,QAAQ,CAAC,GAAI,IAAI,IAAI,MAAM,KAAK,CAAC,GAAI,GAAG,OAAO,CAAC,CAAC;EAC/F,aAAa,OAAO,IAAI,aAAa;GAGnC,OAAO,CAAC,IAAG,OAFQ,IAAI,IAAI,KAAK,EAAA,CAEjB,OAAO,CAAC,CAAC,CAAC,SAAS,YAAY,QAAQ,MAAM,EAAE,CAAC;EACjE,CAAC;CACH,CAAC;AACH,CAAC,CACH;AAEA,MAAM,YAAY,OAAO,eAAe,OAAO,MAAM,CAACC,gBAA+BC,cAA6B,CAAC,CAAC;AAGpH,MAAM,YAAY,OAAO,MAAM,CAAC,OAAO,QAAQ,OAAO,gBAAgB,CAAC;AAEvE,MAAM,MAAM,OAAO,OAAO;CAAE,SAAS;CAAQ,KAAK;CAAW,IAAI;CAAW,OAAO;AAAU,CAAC;AAE9F,MAAM,gBAAgB,OAAO,oBAAoB,OAAO,MAAM,OAAO,OAAO,EAAE,KAAK,OAAO,OAAO,SAAS,EAAE,CAAC,CAAC,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsC/G,MAAa,WAA6D,MAAM,OAC9E,SACA,OAAO,IAAI,aAAa;CACtB,MAAM,OAAO,OAAO,UAAU,UAAA,CAAW,kBAAkB;CAE3D,MAAM,YAAY,OAAO,WAAW,WAAW,MAA8B;EAG3E,QAAO,OAFgB,OAAO,oBAAoB,OAAO,MAAM,GAAG,CAAC,CAAC,CAAC,IAAI,EAAA,CAE1D,KAAK,QAAA,MACG,KAAK;GACxB,QAAQ,IAAI;GACZ,KAAK,IAAI;GACT,IAAI,IAAI;GACR,OAAO,IAAI;EACb,CAAC,CACH;CACF,CAAC;CAED,MAAM,cAAc,OAAO,aAAa,SAAS;CAEjD,OAAO,KAAK;EACV,MAAM,OAAO,WAAW,WAAW,QAAgB,SAAuB;GACxE,MAAM,OAAO,OAAO,GAAG;yEAC0C,OAAO,aAAa,SAAS,SAAS,EAAE;;GAGzG,OAAO,OAAO,UAAU,IAAI;EAC9B,GAAG,OAAO,KAAK;EACf,SAAS,OAAO,WAAW,WAAW,QAAgB;GACpD,MAAM,SAAS,OAAO,GAAG;iEACgC,OAAO;;GAKhE,QAAO,OAFa,cAAc,MAAM,EAAA,CAE5B,EAAE,EAAE,OAAO;EACzB,GAAG,OAAO,KAAK;EACf,OAAO,OAAO,WAAW,WAAW,QAAgB,SAA8C;GAChG,MAAM,OAAO,OAAO,OAAO,QACzB,SACA,OAAO,WAAW,WAAW,OAA6B;IACxD,MAAM,QAAQ,OAAO,YAAY,MAAM,KAAK;IAE5C,OAAO;KAAE,SAAS;KAAQ,KAAK,MAAM;KAAK,IAAI,MAAM;KAAI;IAAM;GAChE,CAAC,CACH;GAEA,OAAO,GAAG,wBAAwB,IAAI,OAAO,IAAI;EACnD,GAAG,OAAO,KAAK;EACf,aAAa,OAAO,IAAI,aAAa;GACnC,MAAM,OAAO,OAAO,GAAG;;;;;GAMvB,OAAO,OAAO,UAAU,IAAI;EAC9B,CAAC,CAAC,CAAC,KAAK,OAAO,KAAK;CACtB,CAAC;AACH,CAAC,CACH"}
|