effect-assistant 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Assistant.d.ts +10 -10
- package/dist/Assistant.d.ts.map +1 -1
- package/dist/Assistant.js +1 -1
- package/dist/Assistant.js.map +1 -1
- package/dist/AssistantActivity.d.ts +2 -2
- package/dist/AssistantActivity.d.ts.map +1 -1
- package/dist/AssistantActivity.js +5 -5
- package/dist/AssistantActivity.js.map +1 -1
- package/dist/AssistantEngine.d.ts +3 -3
- package/dist/AssistantEngine.js +2 -2
- package/dist/AssistantEngine.js.map +1 -1
- package/dist/AssistantEvent.d.ts +19 -8
- package/dist/AssistantEvent.d.ts.map +1 -1
- package/dist/AssistantEvent.js +20 -9
- package/dist/AssistantEvent.js.map +1 -1
- package/dist/AssistantLoop.d.ts +2 -2
- package/dist/AssistantLoop.js.map +1 -1
- package/dist/Block.d.ts +63 -1
- package/dist/Block.d.ts.map +1 -1
- package/dist/Block.js +63 -2
- package/dist/Block.js.map +1 -1
- package/dist/BlockKit.d.ts +25 -3
- package/dist/BlockKit.d.ts.map +1 -1
- package/dist/BlockKit.js +14 -12
- package/dist/BlockKit.js.map +1 -1
- package/dist/ChatLog.d.ts +9 -9
- package/dist/ChatLog.d.ts.map +1 -1
- package/dist/ChatLog.js +21 -21
- package/dist/ChatLog.js.map +1 -1
- package/dist/Compaction.d.ts +10 -10
- package/dist/Compaction.d.ts.map +1 -1
- package/dist/Compaction.js +19 -19
- package/dist/Compaction.js.map +1 -1
- package/dist/Memory.d.ts +1 -1
- package/dist/Memory.js +1 -1
- package/dist/Memory.js.map +1 -1
- package/dist/Skill.d.ts +1 -1
- package/dist/Skill.js +1 -1
- package/dist/Skill.js.map +1 -1
- package/dist/Specialist.d.ts +1 -1
- package/dist/Specialist.js +1 -1
- package/dist/Specialist.js.map +1 -1
- package/dist/Timeline.d.ts +13 -9
- package/dist/Timeline.d.ts.map +1 -1
- package/dist/Timeline.js +35 -23
- package/dist/Timeline.js.map +1 -1
- package/dist/ToolApproval.d.ts +1 -1
- package/dist/ToolApproval.js +1 -1
- package/dist/ToolApproval.js.map +1 -1
- package/dist/ToolDiscovery.d.ts +1 -1
- package/dist/ToolDiscovery.js +1 -1
- package/dist/ToolDiscovery.js.map +1 -1
- package/dist/ToolResult.d.ts +56 -0
- package/dist/ToolResult.d.ts.map +1 -0
- package/dist/ToolResult.js +49 -0
- package/dist/ToolResult.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +4 -3
- package/dist/internal/chat.js +4 -4
- package/dist/internal/chat.js.map +1 -1
- package/dist/internal/events.js +1 -1
- package/dist/internal/events.js.map +1 -1
- package/dist/internal/history.js +23 -23
- package/dist/internal/history.js.map +1 -1
- package/dist/internal/loop.d.ts +1 -1
- package/dist/internal/loop.d.ts.map +1 -1
- package/dist/internal/loop.js +12 -6
- package/dist/internal/loop.js.map +1 -1
- package/dist/internal/toolkit.d.ts +7 -1
- package/dist/internal/toolkit.d.ts.map +1 -1
- package/dist/internal/toolkit.js +47 -4
- package/dist/internal/toolkit.js.map +1 -1
- package/dist/internal/tools.js +39 -6
- package/dist/internal/tools.js.map +1 -1
- package/dist/internal/turns.d.ts.map +1 -1
- package/dist/internal/turns.js +24 -21
- package/dist/internal/turns.js.map +1 -1
- package/package.json +1 -1
- package/src/Assistant.ts +11 -11
- package/src/AssistantActivity.ts +9 -7
- package/src/AssistantEngine.ts +3 -3
- package/src/AssistantEvent.ts +21 -10
- package/src/AssistantLoop.ts +3 -3
- package/src/Block.ts +82 -1
- package/src/BlockKit.ts +37 -4
- package/src/ChatLog.ts +31 -26
- package/src/Compaction.ts +21 -21
- package/src/Memory.ts +1 -1
- package/src/Skill.ts +1 -1
- package/src/Specialist.ts +1 -1
- package/src/Timeline.ts +38 -25
- package/src/ToolApproval.ts +1 -1
- package/src/ToolDiscovery.ts +1 -1
- package/src/ToolResult.ts +80 -0
- package/src/index.ts +5 -0
- package/src/internal/chat.ts +4 -4
- package/src/internal/events.ts +1 -1
- package/src/internal/history.ts +32 -28
- package/src/internal/loop.ts +24 -7
- package/src/internal/toolkit.ts +55 -5
- package/src/internal/tools.ts +81 -5
- package/src/internal/turns.ts +35 -29
package/dist/BlockKit.js.map
CHANGED
|
@@ -1 +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 Timeline from \"./Timeline.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 the assistant's messages in a chat's timeline\n * (`Timeline.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 timeline:\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: Timeline.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: Timeline.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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"}
|
|
1
|
+
{"version":3,"file":"BlockKit.js","names":["Block.isToolResult","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 type * as Tool from \"effect/ai/Tool\"\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 * as Block from \"./Block.ts\"\nimport type * as Timeline from \"./Timeline.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\n/**\n * Returns the names of the tools whose results the kit's blocks show (`Block.fromToolResult`).\n *\n * @category utils\n * @since 0.5.0\n */\nexport const toolNames = (kit: BlockKit | undefined): ReadonlySet<string> =>\n new Set((kit?.blocks ?? []).flatMap((block) => (Block.isToolResult(block) ? [block.tool.name] : [])))\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\ntype ResultArgs<B> = B extends { readonly tool: infer T }\n ? {\n /** The tool's result the block shows, decoded by its success schema. */\n readonly result: Tool.Success<T>\n }\n : unknown\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. A block that shows a tool's result also gets\n * the `result`.\n *\n * @category models\n * @since 0.1.0\n */\nexport type RenderArgs<B> = BlockArgs<B> & ResultArgs<B>\n\n/**\n * Represents what every block's renderer receives.\n *\n * @category models\n * @since 0.5.0\n */\nexport interface BlockArgs<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 the assistant's messages in a chat's timeline\n * (`Timeline.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 timeline:\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: Timeline.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: BlockArgs<Block.Any> & { readonly result?: unknown }) => Out>>\n): (message: Timeline.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 result = Block.isToolResult(block)\n ? Schema.decodeUnknownOption(Schema.toCodecJson(block.result))(message.block.result)\n : Option.some(undefined)\n\n if (Option.isNone(result)) 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 result: result.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":";;;;;;;;;;;;;;;;;;;;;;;;;AAsCA,MAAa,QAAuD,YAA8C;CAChH;CACA,QAAQ,IAAI,IAAI,OAAO,KAAK,UAAU,CAAC,MAAM,MAAM,KAAK,CAAC,CAAC;AAC5D;;;;;;;AAQA,MAAa,aAAa,QACxB,IAAI,KAAK,KAAK,UAAU,CAAC,EAAA,CAAG,SAAS,UAAWA,aAAmB,KAAK,IAAI,CAAC,MAAM,KAAK,IAAI,IAAI,CAAC,CAAE,CAAC;;;;;;;AAmCtG,MAAa,SACX,QACA,UAC+B;CAC/B,IAAI,CAACC,eAA8B,KAAK,GAAG,OAAO;CAElD,MAAM,SAAS,QAA+B;EAC5C,MAAM,OAAO,IAAI,QAAQ,OAAO,OAAO,IAAI,EAAE,CAAC,EAAE,SAAS,IAAI;EAE7D,IAAI,KAAK,WAAW,GAAG,OAAO;EAE9B,MAAM,OAAO,IAAI,IAAI,MAAM;EAE3B,KAAK,MAAM,MAAM,MAAM,KAAK,IAAI,IAAI;GAAE,MAAM;GAAO,QAAQ,KAAA;EAAU,CAAC;EAEtE,OAAO;CACT;CAEA,IAAI,UAAU,SAAS,OAAO,cAAc,GAAG,OAAO,MAAM,MAAM,UAAU;CAE5E,IAAI,UAAU,SAAS,OAAO,kBAAkB,GAAG;EACjD,MAAM,SAAS,MAAM,MAAM,UAAU,CAAC,CAAC;EAEvC,OAAO,MAAM,UAAU,KAAA,IAAY,SAAS,IAAI,IAAI,MAAM,CAAC,CAAC,IAAI,MAAM,IAAI;GAAE,MAAM;GAAM,QAAQ,KAAA;EAAU,CAAC;CAC7G;CAEA,IAAI,CAAC,UAAU,SAAS,OAAO,aAAa,KAAK,OAAO,IAAI,MAAM,SAAS,CAAC,EAAE,SAAS,MAAM,OAAO;CAEpG,OAAO,IAAI,IAAI,MAAM,CAAC,CAAC,IAAI,MAAM,WAAW;EAAE,MAAM;EAAO,QAAQ;CAAM,CAAC;AAC5E;;;;;;;AAQA,MAAa,eAAe,YAAgF;CAC1G,IAAI,yBAAqC,IAAI,IAAI;CAEjD,KAAK,MAAM,SAAS,SAAS,SAAS,MAAM,QAAQ,MAAM,KAAK;CAE/D,OAAO;AACT;;;;;;;;;;;;AAaA,MAAa,eACX,SACA,YAC0B;CAG1B,OAFa,CAAC,GAAG,YAAY,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,WAAY,MAAM,OAAO,CAAC,EAAE,IAAI,CAAC,CAE5E,CAAC,CAAC,QAAQ,OAAO;EACzB,MAAM,QAAQ,QAAQ,WACnB,EAAE,YACDA,eAA8B,KAAK,KAAK,UAAU,SAAS,OAAO,kBAAkB,KAAK,MAAM,OAAO,EAC1G;EAEA,IAAI,UAAU,IAAI,OAAO;EAIzB,OAFc,QAAQ,MAAM,QAAQ,CAAC,CAAC,CAAC,QAAQ,EAAE,YAAY,UAAU,SAAS,OAAO,aAAa,CAAC,CAAC,CAAC,UAEvF,QAAQ;CAC1B,CAAC;AACH;;;;;;;;;;;;;;;AAgBA,MAAa,aAAa,OAAO,GAAG,WAAW,SAAwC;CACrF,MAAM,OAAO,OAAO;CACpB,MAAM,QAAQ,YAAY,OAAO,KAAK,SAAS,OAAO;CAEtD,IAAI,MAAM,SAAS,GAAG,OAAO,KAAK,OAAA,aAAmC,KAAK,EAAE,YAAY,MAAM,CAAC,CAAC;AAClG,CAAC;AAsGD,SAAgB,SACd,KACA,WACiF;CACjF,QAAQ,SAAS,YAAY;EAC3B,IAAI,QAAQ,UAAU,KAAA,GAAW,OAAO,KAAA;EAExC,MAAM,EAAE,SAAS,QAAQ;EACzB,MAAM,QAAQ,IAAI,OAAO,IAAI,IAAI;EACjC,MAAM,SAAS,UAAU;EAEzB,IAAI,UAAU,KAAA,KAAa,WAAW,KAAA,GAAW,OAAO,KAAA;EAExD,MAAM,QAAQ,OAAO,oBAAoB,MAAM,KAAK,CAAC,CAAC,QAAQ,MAAM,KAAK;EAEzE,IAAI,OAAO,OAAO,KAAK,GAAG,OAAO,KAAA;EAEjC,MAAM,SAASD,aAAmB,KAAK,IACnC,OAAO,oBAAoB,OAAO,YAAY,MAAM,MAAM,CAAC,CAAC,CAAC,QAAQ,MAAM,MAAM,IACjF,OAAO,KAAK,KAAA,CAAS;EAEzB,IAAI,OAAO,OAAO,MAAM,GAAG,OAAO,KAAA;EAElC,MAAM,QAAQ,QAAQ,MAAM;EAC5B,MAAM,cAAc,MAAM,WAAW,KAAA,IAAY,KAAA,IAAY,OAAO,YAAY,MAAM,MAAM;EAE5F,MAAM,SACJ,UAAU,KAAA,KAAa,gBAAgB,KAAA,IAAY,OAAO,KAAK,IAAI,OAAO,oBAAoB,WAAW,CAAC,CAAC,KAAK;EAElH,OAAO,OAAO;GACZ,WAAW,QAAQ;GACnB,MAAM,QAAQ;GACd,OAAO,MAAM;GACb,QAAQ,OAAO;GACf,MAAM,QAAQ,MAAM,WAAW;GAC/B,QAAQ,OAAO,eAAe,MAAM;GACpC,UAAU,UACR,QAAQ,QAAA,YACqB,KAAK;IAE9B,IAAIE,QAAgB,GAAG,QAAQ,GAAG,QAAQ;IAC1C,WAAW,QAAQ;IACnB;IACA,OAAO,gBAAgB,KAAA,IAAY,QAAQ,OAAO,kBAAkB,WAAW,CAAC,CAAC,KAAK;GACxF,CAAC,CACH;EACJ,CAAC;CACH;AACF"}
|
package/dist/ChatLog.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ declare namespace ChatLog_d_exports {
|
|
|
10
10
|
declare const ChatLog_base: Context.ServiceClass<ChatLog, "effect-assistant/ChatLog", {
|
|
11
11
|
/** Appends events to a chat and returns them as stored entries. */
|
|
12
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 `
|
|
13
|
+
/** Reads a chat's stored entries in order, either all of them or only those after a `sequence`. */
|
|
14
14
|
readonly read: (chatId: ChatId, options?: ReadOptions) => Effect.Effect<ReadonlyArray<Entry>>;
|
|
15
15
|
/**
|
|
16
16
|
* Returns the chats whose last entry ends neither a turn nor the chat. After a restart, these
|
|
@@ -32,13 +32,13 @@ export declare class ChatLog extends ChatLog_base {}
|
|
|
32
32
|
* @since 0.1.0
|
|
33
33
|
*/
|
|
34
34
|
export interface ReadOptions {
|
|
35
|
-
/** Reads only the entries after this `
|
|
35
|
+
/** Reads only the entries after this `sequence`. */
|
|
36
36
|
readonly after?: number | undefined;
|
|
37
37
|
}
|
|
38
38
|
/**
|
|
39
39
|
* Represents the storage that a `ChatLog` is built on.
|
|
40
40
|
*
|
|
41
|
-
* A storage reads a chat's entries, finds a chat's last `
|
|
41
|
+
* A storage reads a chat's entries, finds a chat's last `sequence`, writes new entries, and finds every
|
|
42
42
|
* chat's last entry. Entries are never removed, so a deleted chat keeps its log, which ends in
|
|
43
43
|
* `ChatDeleted`.
|
|
44
44
|
*
|
|
@@ -47,14 +47,14 @@ export interface ReadOptions {
|
|
|
47
47
|
*/
|
|
48
48
|
export interface Storage {
|
|
49
49
|
readonly read: (chatId: ChatId, options?: ReadOptions) => Effect.Effect<ReadonlyArray<Entry>>;
|
|
50
|
-
readonly
|
|
50
|
+
readonly lastSequence: (chatId: ChatId) => Effect.Effect<number>;
|
|
51
51
|
readonly write: (chatId: ChatId, entries: ReadonlyArray<Entry>) => Effect.Effect<void>;
|
|
52
52
|
/** Returns the last entry of every chat. */
|
|
53
53
|
readonly lastEntries: Effect.Effect<ReadonlyArray<Entry>>;
|
|
54
54
|
}
|
|
55
55
|
/**
|
|
56
56
|
* Creates a `ChatLog` from a {@link Storage}. `append` numbers new entries after the chat's last
|
|
57
|
-
* `
|
|
57
|
+
* `sequence` and stamps them with the current time.
|
|
58
58
|
*
|
|
59
59
|
* @category constructors
|
|
60
60
|
* @since 0.1.0
|
|
@@ -72,8 +72,8 @@ export declare const layerMemory: Layer.Layer<ChatLog>;
|
|
|
72
72
|
*
|
|
73
73
|
* **Details**
|
|
74
74
|
*
|
|
75
|
-
* The table must already exist. It has the columns `chat_id`, `
|
|
76
|
-
* primary key `(chat_id,
|
|
75
|
+
* 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`.
|
|
77
77
|
*
|
|
78
78
|
* **Example** (Storing chats in Postgres)
|
|
79
79
|
*
|
|
@@ -93,10 +93,10 @@ export declare const layerMemory: Layer.Layer<ChatLog>;
|
|
|
93
93
|
* ```sql
|
|
94
94
|
* CREATE TABLE chat_log (
|
|
95
95
|
* chat_id TEXT NOT NULL,
|
|
96
|
-
*
|
|
96
|
+
* sequence INTEGER NOT NULL,
|
|
97
97
|
* at DOUBLE PRECISION NOT NULL,
|
|
98
98
|
* event TEXT NOT NULL,
|
|
99
|
-
* PRIMARY KEY (chat_id,
|
|
99
|
+
* PRIMARY KEY (chat_id, sequence)
|
|
100
100
|
* );
|
|
101
101
|
* ```
|
|
102
102
|
*
|
package/dist/ChatLog.d.ts.map
CHANGED
|
@@ -1 +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,
|
|
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBA0EzB,UAAU,MAAM,MAAM,gBAAgB,UAAU"}
|
package/dist/ChatLog.js
CHANGED
|
@@ -35,7 +35,7 @@ var ChatLog_exports = /* @__PURE__ */ __exportAll({
|
|
|
35
35
|
var ChatLog = class extends Context.Service()("effect-assistant/ChatLog") {};
|
|
36
36
|
/**
|
|
37
37
|
* Creates a `ChatLog` from a {@link Storage}. `append` numbers new entries after the chat's last
|
|
38
|
-
* `
|
|
38
|
+
* `sequence` and stamps them with the current time.
|
|
39
39
|
*
|
|
40
40
|
* @category constructors
|
|
41
41
|
* @since 0.1.0
|
|
@@ -44,11 +44,11 @@ const make = (storage) => ChatLog.of({
|
|
|
44
44
|
read: storage.read,
|
|
45
45
|
append: Effect.fn("ChatLog.append")(function* (chatId, events) {
|
|
46
46
|
if (events.length === 0) return [];
|
|
47
|
-
const
|
|
47
|
+
const sequence = yield* storage.lastSequence(chatId);
|
|
48
48
|
const at = yield* Clock.currentTimeMillis;
|
|
49
49
|
const entries = events.map((event, n) => Entry.make({
|
|
50
50
|
chatId,
|
|
51
|
-
|
|
51
|
+
sequence: sequence + n + 1,
|
|
52
52
|
at,
|
|
53
53
|
event
|
|
54
54
|
}));
|
|
@@ -69,10 +69,10 @@ const layerMemory = Layer.effect(ChatLog, Effect.gen(function* () {
|
|
|
69
69
|
const chats = yield* Ref.make(/* @__PURE__ */ new Map());
|
|
70
70
|
return make({
|
|
71
71
|
read: Effect.fnUntraced(function* (chatId, options) {
|
|
72
|
-
return ((yield* Ref.get(chats)).get(chatId) ?? []).filter((entry) => entry.
|
|
72
|
+
return ((yield* Ref.get(chats)).get(chatId) ?? []).filter((entry) => entry.sequence > (options?.after ?? 0));
|
|
73
73
|
}),
|
|
74
|
-
|
|
75
|
-
return (yield* Ref.get(chats)).get(chatId)?.at(-1)?.
|
|
74
|
+
lastSequence: Effect.fnUntraced(function* (chatId) {
|
|
75
|
+
return (yield* Ref.get(chats)).get(chatId)?.at(-1)?.sequence ?? 0;
|
|
76
76
|
}),
|
|
77
77
|
write: (chatId, entries) => Ref.update(chats, (all) => new Map(all).set(chatId, [...all.get(chatId) ?? [], ...entries])),
|
|
78
78
|
lastEntries: Effect.gen(function* () {
|
|
@@ -84,18 +84,18 @@ const EventJson = Schema.fromJsonString(Schema.Union([AssistantEvent, StoredAppE
|
|
|
84
84
|
const SqlNumber = Schema.Union([Schema.Finite, Schema.FiniteFromString]);
|
|
85
85
|
const Row = Schema.Struct({
|
|
86
86
|
chat_id: ChatId,
|
|
87
|
-
|
|
87
|
+
sequence: SqlNumber,
|
|
88
88
|
at: SqlNumber,
|
|
89
89
|
event: EventJson
|
|
90
90
|
});
|
|
91
|
-
const
|
|
91
|
+
const decodeLastSequence = Schema.decodeUnknownEffect(Schema.Array(Schema.Struct({ sequence: Schema.NullOr(SqlNumber) })));
|
|
92
92
|
/**
|
|
93
93
|
* Layer that stores chats in a SQL table named `chat_log`.
|
|
94
94
|
*
|
|
95
95
|
* **Details**
|
|
96
96
|
*
|
|
97
|
-
* The table must already exist. It has the columns `chat_id`, `
|
|
98
|
-
* primary key `(chat_id,
|
|
97
|
+
* 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`.
|
|
99
99
|
*
|
|
100
100
|
* **Example** (Storing chats in Postgres)
|
|
101
101
|
*
|
|
@@ -115,10 +115,10 @@ const decodeLastSeq = Schema.decodeUnknownEffect(Schema.Array(Schema.Struct({ se
|
|
|
115
115
|
* ```sql
|
|
116
116
|
* CREATE TABLE chat_log (
|
|
117
117
|
* chat_id TEXT NOT NULL,
|
|
118
|
-
*
|
|
118
|
+
* sequence INTEGER NOT NULL,
|
|
119
119
|
* at DOUBLE PRECISION NOT NULL,
|
|
120
120
|
* event TEXT NOT NULL,
|
|
121
|
-
* PRIMARY KEY (chat_id,
|
|
121
|
+
* PRIMARY KEY (chat_id, sequence)
|
|
122
122
|
* );
|
|
123
123
|
* ```
|
|
124
124
|
*
|
|
@@ -130,7 +130,7 @@ const layerSql = Layer.effect(ChatLog, Effect.gen(function* () {
|
|
|
130
130
|
const toEntries = Effect.fnUntraced(function* (rows) {
|
|
131
131
|
return (yield* Schema.decodeUnknownEffect(Schema.Array(Row))(rows)).map((row) => Entry.make({
|
|
132
132
|
chatId: row.chat_id,
|
|
133
|
-
|
|
133
|
+
sequence: row.sequence,
|
|
134
134
|
at: row.at,
|
|
135
135
|
event: row.event
|
|
136
136
|
}));
|
|
@@ -139,22 +139,22 @@ const layerSql = Layer.effect(ChatLog, Effect.gen(function* () {
|
|
|
139
139
|
return make({
|
|
140
140
|
read: Effect.fnUntraced(function* (chatId, options) {
|
|
141
141
|
const rows = yield* sql`
|
|
142
|
-
SELECT chat_id,
|
|
142
|
+
SELECT chat_id, sequence, at, event FROM chat_log WHERE chat_id = ${chatId} AND sequence > ${options?.after ?? 0} ORDER BY sequence
|
|
143
143
|
`;
|
|
144
144
|
return yield* toEntries(rows);
|
|
145
145
|
}, Effect.orDie),
|
|
146
|
-
|
|
146
|
+
lastSequence: Effect.fnUntraced(function* (chatId) {
|
|
147
147
|
const result = yield* sql`
|
|
148
|
-
SELECT MAX(
|
|
148
|
+
SELECT MAX(sequence) AS sequence FROM chat_log WHERE chat_id = ${chatId}
|
|
149
149
|
`;
|
|
150
|
-
return (yield*
|
|
150
|
+
return (yield* decodeLastSequence(result))[0]?.sequence ?? 0;
|
|
151
151
|
}, Effect.orDie),
|
|
152
152
|
write: Effect.fnUntraced(function* (chatId, entries) {
|
|
153
153
|
const rows = yield* Effect.forEach(entries, Effect.fnUntraced(function* (entry) {
|
|
154
154
|
const event = yield* encodeEvent(entry.event);
|
|
155
155
|
return {
|
|
156
156
|
chat_id: chatId,
|
|
157
|
-
|
|
157
|
+
sequence: entry.sequence,
|
|
158
158
|
at: entry.at,
|
|
159
159
|
event
|
|
160
160
|
};
|
|
@@ -163,9 +163,9 @@ const layerSql = Layer.effect(ChatLog, Effect.gen(function* () {
|
|
|
163
163
|
}, Effect.orDie),
|
|
164
164
|
lastEntries: Effect.gen(function* () {
|
|
165
165
|
const rows = yield* sql`
|
|
166
|
-
SELECT e.chat_id, e.
|
|
167
|
-
JOIN (SELECT chat_id, MAX(
|
|
168
|
-
ON last.chat_id = e.chat_id AND last.
|
|
166
|
+
SELECT e.chat_id, e.sequence, e.at, e.event FROM chat_log e
|
|
167
|
+
JOIN (SELECT chat_id, MAX(sequence) AS sequence FROM chat_log GROUP BY chat_id) last
|
|
168
|
+
ON last.chat_id = e.chat_id AND last.sequence = e.sequence
|
|
169
169
|
`;
|
|
170
170
|
return yield* toEntries(rows);
|
|
171
171
|
}).pipe(Effect.orDie)
|
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 `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":"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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\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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"}
|
package/dist/Compaction.d.ts
CHANGED
|
@@ -6,9 +6,9 @@ declare namespace Compaction_d_exports {
|
|
|
6
6
|
export { Summary, attachedFiles, inputTokens, openItems, since };
|
|
7
7
|
}
|
|
8
8
|
/**
|
|
9
|
-
* Represents the fields a summary event must have: its text and the last `
|
|
9
|
+
* Represents the fields a summary event must have: its text and the last `sequence` it covers.
|
|
10
10
|
*
|
|
11
|
-
* Entries after `
|
|
11
|
+
* Entries after `throughSequence` are kept as they are, so an input that arrived while the summary was
|
|
12
12
|
* being written isn't lost.
|
|
13
13
|
*
|
|
14
14
|
* @category models
|
|
@@ -17,7 +17,7 @@ declare namespace Compaction_d_exports {
|
|
|
17
17
|
export interface Summary {
|
|
18
18
|
readonly _tag: string;
|
|
19
19
|
readonly text: string;
|
|
20
|
-
readonly
|
|
20
|
+
readonly throughSequence: number;
|
|
21
21
|
}
|
|
22
22
|
/**
|
|
23
23
|
* Returns the number of input tokens used by the chat's last model call, which shows how large
|
|
@@ -28,22 +28,22 @@ export interface Summary {
|
|
|
28
28
|
*/
|
|
29
29
|
export declare const inputTokens: (entries: ReadonlyArray<AnyEntry>) => number;
|
|
30
30
|
/**
|
|
31
|
-
* Returns the items that were open at `
|
|
31
|
+
* Returns the items that were open at `throughSequence` and are still open: blocks the user hasn't
|
|
32
32
|
* answered, specialists that are working, and tool calls waiting for the user's approval. The result is text for
|
|
33
33
|
* the model, or `None` when nothing is open.
|
|
34
34
|
*
|
|
35
35
|
* @category utils
|
|
36
36
|
* @since 0.1.0
|
|
37
37
|
*/
|
|
38
|
-
export declare const openItems: (entries: ReadonlyArray<AnyEntry>,
|
|
38
|
+
export declare const openItems: (entries: ReadonlyArray<AnyEntry>, throughSequence: number) => Option.Option<string>;
|
|
39
39
|
/**
|
|
40
|
-
* Returns the files the user attached up to `
|
|
40
|
+
* Returns the files the user attached up to `throughSequence`, as lines of text for the model, or
|
|
41
41
|
* `None` when there are none. A summary may leave out a file's id, which tools need.
|
|
42
42
|
*
|
|
43
43
|
* @category utils
|
|
44
44
|
* @since 0.1.0
|
|
45
45
|
*/
|
|
46
|
-
export declare const attachedFiles: (entries: ReadonlyArray<AnyEntry>,
|
|
46
|
+
export declare const attachedFiles: (entries: ReadonlyArray<AnyEntry>, throughSequence: number) => Option.Option<string>;
|
|
47
47
|
/**
|
|
48
48
|
* Creates a history view that starts at the last summary.
|
|
49
49
|
*
|
|
@@ -69,7 +69,7 @@ export declare const attachedFiles: (entries: ReadonlyArray<AnyEntry>, throughSe
|
|
|
69
69
|
*
|
|
70
70
|
* class Summary extends Schema.TaggedClass<Summary>()("Summary", {
|
|
71
71
|
* text: Schema.String,
|
|
72
|
-
*
|
|
72
|
+
* throughSequence: Schema.Int
|
|
73
73
|
* }) {}
|
|
74
74
|
*
|
|
75
75
|
* declare const transcript: (entries: ReadonlyArray<AssistantEvent.AnyEntry>) => string
|
|
@@ -87,12 +87,12 @@ export declare const attachedFiles: (entries: ReadonlyArray<AnyEntry>, throughSe
|
|
|
87
87
|
* prompt: `Summarize this chat:\n\n${transcript(entries)}`
|
|
88
88
|
* })
|
|
89
89
|
*
|
|
90
|
-
* yield* chat.append(Summary.make({ text,
|
|
90
|
+
* yield* chat.append(Summary.make({ text, throughSequence: entries.at(-1)?.sequence ?? 0 }))
|
|
91
91
|
* })
|
|
92
92
|
*
|
|
93
93
|
* // After the reply, so the user doesn't wait for the summary.
|
|
94
94
|
* return Stream.concat(
|
|
95
|
-
* turn.
|
|
95
|
+
* turn.run({ instructions: "Be brief.", history: Compaction.since(Schema.is(Summary)) }),
|
|
96
96
|
* Stream.drain(Stream.fromEffect(compact))
|
|
97
97
|
* )
|
|
98
98
|
* })
|
package/dist/Compaction.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Compaction.d.ts","names":[],"sources":["../src/Compaction.ts"],"mappings":";;;;;;;;;;;;;;;;iBAgCiB;WACN;WACA;WACA;;;;;;;;;qBAUE,cAAW,SAAa,cAAc;;;;;;;;;qBAmBtC,YAAS,SACX,cAAc,WAAwB,
|
|
1
|
+
{"version":3,"file":"Compaction.d.ts","names":[],"sources":["../src/Compaction.ts"],"mappings":";;;;;;;;;;;;;;;;iBAgCiB;WACN;WACA;WACA;;;;;;;;;qBAUE,cAAW,SAAa,cAAc;;;;;;;;;qBAmBtC,YAAS,SACX,cAAc,WAAwB,4BAE9C,OAAO;;;;;;;;qBAmEG,gBAAa,SACf,cAAc,WAAwB,4BAE9C,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAmEG,QAAK,YACH,mBAAmB,SAAS,aACxC;WAA0B;GAAc,SAC9B,cAAc,MAAgB,aACtC,cAAc,MAAgB,UAAU,OAAO"}
|
package/dist/Compaction.js
CHANGED
|
@@ -3,8 +3,8 @@ import { attachmentLine, isLibraryEvent } from "./AssistantEvent.js";
|
|
|
3
3
|
import { fromEntries } from "./ToolApproval.js";
|
|
4
4
|
import { toJsonText } from "./internal/schema.js";
|
|
5
5
|
import { userText } from "./internal/history.js";
|
|
6
|
-
import { fromEntries as fromEntries$1 } from "./
|
|
7
|
-
import { fromEntries as fromEntries$2 } from "./
|
|
6
|
+
import { fromEntries as fromEntries$1 } from "./BlockKit.js";
|
|
7
|
+
import { fromEntries as fromEntries$2 } from "./AssistantActivity.js";
|
|
8
8
|
import * as Predicate from "effect/Predicate";
|
|
9
9
|
import * as Option from "effect/Option";
|
|
10
10
|
//#region src/Compaction.ts
|
|
@@ -26,23 +26,23 @@ const inputTokens = (entries) => {
|
|
|
26
26
|
return last !== void 0 && isLibraryEvent(last) && Predicate.isTagged(last, "ModelOutput") ? last.usage?.inputTokens.total ?? 0 : 0;
|
|
27
27
|
};
|
|
28
28
|
/**
|
|
29
|
-
* Returns the items that were open at `
|
|
29
|
+
* Returns the items that were open at `throughSequence` and are still open: blocks the user hasn't
|
|
30
30
|
* answered, specialists that are working, and tool calls waiting for the user's approval. The result is text for
|
|
31
31
|
* the model, or `None` when nothing is open.
|
|
32
32
|
*
|
|
33
33
|
* @category utils
|
|
34
34
|
* @since 0.1.0
|
|
35
35
|
*/
|
|
36
|
-
const openItems = (entries,
|
|
37
|
-
const
|
|
38
|
-
const opened = (
|
|
39
|
-
const blocks = [...fromEntries$
|
|
36
|
+
const openItems = (entries, throughSequence) => {
|
|
37
|
+
const sequenceOf = (found) => entries.find(({ event }) => found(event))?.sequence ?? Number.POSITIVE_INFINITY;
|
|
38
|
+
const opened = (sequence) => sequence <= throughSequence;
|
|
39
|
+
const blocks = [...fromEntries$1(entries)].flatMap(([messageId, state]) => {
|
|
40
40
|
const shown = entries.find(({ event }) => isLibraryEvent(event) && Predicate.isTagged(event, "AssistantMessage") && event.id === messageId);
|
|
41
41
|
const message = shown?.event;
|
|
42
|
-
return state.open && shown !== void 0 && opened(shown.
|
|
42
|
+
return state.open && shown !== void 0 && opened(shown.sequence) && message !== void 0 && isLibraryEvent(message) && Predicate.isTagged(message, "AssistantMessage") ? [`- message ${messageId}, a ${message.block?.kind} block: ${message.text}`] : [];
|
|
43
43
|
});
|
|
44
|
-
const specialists = [...fromEntries$
|
|
45
|
-
const approvals = [...fromEntries(entries).values()].flatMap((approval) => approval.status === "pending" && opened(
|
|
44
|
+
const specialists = [...fromEntries$2(entries).specialists.values()].flatMap((run) => opened(sequenceOf((event) => isLibraryEvent(event) && Predicate.isTagged(event, "SpecialistStarted") && event.runId === run.runId)) ? [`- your ${run.specialist} specialist (${run.runId}), working on: ${run.goal}`] : []);
|
|
45
|
+
const approvals = [...fromEntries(entries).values()].flatMap((approval) => approval.status === "pending" && opened(sequenceOf((event) => isLibraryEvent(event) && Predicate.isTagged(event, "ApprovalRequested") && event.callId === approval.request.callId)) ? [`- your call to ${approval.request.tool} (${approval.request.callId}): ${toJsonText(approval.request.params)}`] : []);
|
|
46
46
|
const sections = [
|
|
47
47
|
blocks.length > 0 ? `Blocks the user hasn't answered:\n${blocks.join("\n")}` : "",
|
|
48
48
|
specialists.length > 0 ? `Specialists still working:\n${specialists.join("\n")}` : "",
|
|
@@ -51,14 +51,14 @@ const openItems = (entries, throughSeq) => {
|
|
|
51
51
|
return sections.length === 0 ? Option.none() : Option.some(sections.join("\n\n"));
|
|
52
52
|
};
|
|
53
53
|
/**
|
|
54
|
-
* Returns the files the user attached up to `
|
|
54
|
+
* Returns the files the user attached up to `throughSequence`, as lines of text for the model, or
|
|
55
55
|
* `None` when there are none. A summary may leave out a file's id, which tools need.
|
|
56
56
|
*
|
|
57
57
|
* @category utils
|
|
58
58
|
* @since 0.1.0
|
|
59
59
|
*/
|
|
60
|
-
const attachedFiles = (entries,
|
|
61
|
-
const lines = entries.flatMap(({
|
|
60
|
+
const attachedFiles = (entries, throughSequence) => {
|
|
61
|
+
const lines = entries.flatMap(({ sequence, event }) => sequence <= throughSequence && isLibraryEvent(event) && Predicate.isTagged(event, "UserMessage") ? (event.attachments ?? []).map(attachmentLine) : []);
|
|
62
62
|
return lines.length === 0 ? Option.none() : Option.some(lines.join("\n"));
|
|
63
63
|
};
|
|
64
64
|
/**
|
|
@@ -86,7 +86,7 @@ const attachedFiles = (entries, throughSeq) => {
|
|
|
86
86
|
*
|
|
87
87
|
* class Summary extends Schema.TaggedClass<Summary>()("Summary", {
|
|
88
88
|
* text: Schema.String,
|
|
89
|
-
*
|
|
89
|
+
* throughSequence: Schema.Int
|
|
90
90
|
* }) {}
|
|
91
91
|
*
|
|
92
92
|
* declare const transcript: (entries: ReadonlyArray<AssistantEvent.AnyEntry>) => string
|
|
@@ -104,12 +104,12 @@ const attachedFiles = (entries, throughSeq) => {
|
|
|
104
104
|
* prompt: `Summarize this chat:\n\n${transcript(entries)}`
|
|
105
105
|
* })
|
|
106
106
|
*
|
|
107
|
-
* yield* chat.append(Summary.make({ text,
|
|
107
|
+
* yield* chat.append(Summary.make({ text, throughSequence: entries.at(-1)?.sequence ?? 0 }))
|
|
108
108
|
* })
|
|
109
109
|
*
|
|
110
110
|
* // After the reply, so the user doesn't wait for the summary.
|
|
111
111
|
* return Stream.concat(
|
|
112
|
-
* turn.
|
|
112
|
+
* turn.run({ instructions: "Be brief.", history: Compaction.since(Schema.is(Summary)) }),
|
|
113
113
|
* Stream.drain(Stream.fromEffect(compact))
|
|
114
114
|
* )
|
|
115
115
|
* })
|
|
@@ -121,15 +121,15 @@ const attachedFiles = (entries, throughSeq) => {
|
|
|
121
121
|
const since = (isSummary) => (entries) => {
|
|
122
122
|
const last = entries.findLast(({ event }) => isSummary(event))?.event;
|
|
123
123
|
if (last === void 0 || !isSummary(last)) return entries;
|
|
124
|
-
const open = Option.match(openItems(entries, last.
|
|
124
|
+
const open = Option.match(openItems(entries, last.throughSequence), {
|
|
125
125
|
onNone: () => "",
|
|
126
126
|
onSome: (items) => `\n\n<still_open>\n${items}\n</still_open>`
|
|
127
127
|
});
|
|
128
|
-
const files = Option.match(attachedFiles(entries, last.
|
|
128
|
+
const files = Option.match(attachedFiles(entries, last.throughSequence), {
|
|
129
129
|
onNone: () => "",
|
|
130
130
|
onSome: (lines) => `\n\n<files_attached_earlier>\n${lines}\n</files_attached_earlier>`
|
|
131
131
|
});
|
|
132
|
-
return [userText(`<earlier_in_this_chat>\n${last.text}\n</earlier_in_this_chat>${files}${open}`), ...entries.filter(({
|
|
132
|
+
return [userText(`<earlier_in_this_chat>\n${last.text}\n</earlier_in_this_chat>${files}${open}`), ...entries.filter(({ sequence, event }) => sequence > last.throughSequence && !isSummary(event))];
|
|
133
133
|
};
|
|
134
134
|
//#endregion
|
|
135
135
|
export { Compaction_exports, attachedFiles, inputTokens, openItems, since };
|
package/dist/Compaction.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Compaction.js","names":["AssistantEvent.isLibraryEvent","BlockKit.fromEntries","AssistantActivity.fromEntries","ToolApproval.fromEntries","AssistantEvent.attachmentLine"],"sources":["../src/Compaction.ts"],"sourcesContent":["/**\n * This module provides what a long chat needs so that the model can read a summary in place of\n * everything before it.\n *\n * The app decides when to compact, which model writes the summary, and what the summary says.\n * Its handler reads the chat's entries, has a model write a summary, and stores the summary as one\n * of the assistant's own events (see {@link Summary}). Its loop then reads the chat through\n * {@link since}. {@link inputTokens} helps decide when the prompt has grown large enough to\n * compact. The log keeps every entry, so the user still sees the whole chat.\n *\n * @since 0.1.0\n */\nimport type * as Prompt from \"effect/ai/Prompt\"\nimport * as Option from \"effect/Option\"\nimport * as Predicate from \"effect/Predicate\"\nimport type * as Assistant from \"./Assistant.ts\"\nimport * as AssistantActivity from \"./AssistantActivity.ts\"\nimport * as AssistantEvent from \"./AssistantEvent.ts\"\nimport * as BlockKit from \"./BlockKit.ts\"\nimport { userText } from \"./internal/history.ts\"\nimport { toJsonText } from \"./internal/schema.ts\"\nimport * as ToolApproval from \"./ToolApproval.ts\"\n\n/**\n * Represents the fields a summary event must have: its text and the last `seq` it covers.\n *\n * Entries after `throughSeq` are kept as they are, so an input that arrived while the summary was\n * being written isn't lost.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface Summary {\n readonly _tag: string\n readonly text: string\n readonly throughSeq: number\n}\n\n/**\n * Returns the number of input tokens used by the chat's last model call, which shows how large\n * the prompt has grown. Returns 0 before the first call.\n *\n * @category utils\n * @since 0.1.0\n */\nexport const inputTokens = (entries: ReadonlyArray<AssistantEvent.AnyEntry>): number => {\n const last = entries.findLast(\n ({ event }) =>\n AssistantEvent.isLibraryEvent(event) && Predicate.isTagged(event, \"ModelOutput\") && event.usage !== undefined\n )?.event\n\n return last !== undefined && AssistantEvent.isLibraryEvent(last) && Predicate.isTagged(last, \"ModelOutput\")\n ? (last.usage?.inputTokens.total ?? 0)\n : 0\n}\n\n/**\n * Returns the items that were open at `throughSeq` and are still open: blocks the user hasn't\n * answered, specialists that are working, and tool calls waiting for the user's approval. The result is text for\n * the model, or `None` when nothing is open.\n *\n * @category utils\n * @since 0.1.0\n */\nexport const openItems = (\n entries: ReadonlyArray<AssistantEvent.AnyEntry>,\n throughSeq: number\n): Option.Option<string> => {\n const seqOf = (found: (event: AssistantEvent.AnyEntry[\"event\"]) => boolean) =>\n entries.find(({ event }) => found(event))?.seq ?? Number.POSITIVE_INFINITY\n\n const opened = (seq: number) => seq <= throughSeq\n\n const blocks = [...BlockKit.fromEntries(entries)].flatMap(([messageId, state]) => {\n const shown = entries.find(\n ({ event }) =>\n AssistantEvent.isLibraryEvent(event) && Predicate.isTagged(event, \"AssistantMessage\") && event.id === messageId\n )\n\n const message = shown?.event\n\n return state.open &&\n shown !== undefined &&\n opened(shown.seq) &&\n message !== undefined &&\n AssistantEvent.isLibraryEvent(message) &&\n Predicate.isTagged(message, \"AssistantMessage\")\n ? [`- message ${messageId}, a ${message.block?.kind} block: ${message.text}`]\n : []\n })\n\n const specialists = [...AssistantActivity.fromEntries(entries).specialists.values()].flatMap((run) =>\n opened(\n seqOf(\n (event) =>\n AssistantEvent.isLibraryEvent(event) &&\n Predicate.isTagged(event, \"SpecialistStarted\") &&\n event.runId === run.runId\n )\n )\n ? [`- your ${run.specialist} specialist (${run.runId}), working on: ${run.goal}`]\n : []\n )\n\n const approvals = [...ToolApproval.fromEntries(entries).values()].flatMap((approval) =>\n approval.status === \"pending\" &&\n opened(\n seqOf(\n (event) =>\n AssistantEvent.isLibraryEvent(event) &&\n Predicate.isTagged(event, \"ApprovalRequested\") &&\n event.callId === approval.request.callId\n )\n )\n ? [`- your call to ${approval.request.tool} (${approval.request.callId}): ${toJsonText(approval.request.params)}`]\n : []\n )\n\n const sections = [\n blocks.length > 0 ? `Blocks the user hasn't answered:\\n${blocks.join(\"\\n\")}` : \"\",\n specialists.length > 0 ? `Specialists still working:\\n${specialists.join(\"\\n\")}` : \"\",\n approvals.length > 0 ? `Calls waiting for the user's approval:\\n${approvals.join(\"\\n\")}` : \"\"\n ].filter((section) => section.length > 0)\n\n return sections.length === 0 ? Option.none() : Option.some(sections.join(\"\\n\\n\"))\n}\n\n/**\n * Returns the files the user attached up to `throughSeq`, as lines of text for the model, or\n * `None` when there are none. A summary may leave out a file's id, which tools need.\n *\n * @category utils\n * @since 0.1.0\n */\nexport const attachedFiles = (\n entries: ReadonlyArray<AssistantEvent.AnyEntry>,\n throughSeq: number\n): Option.Option<string> => {\n const lines = entries.flatMap(({ seq, event }) =>\n seq <= throughSeq && AssistantEvent.isLibraryEvent(event) && Predicate.isTagged(event, \"UserMessage\")\n ? (event.attachments ?? []).map(AssistantEvent.attachmentLine)\n : []\n )\n\n return lines.length === 0 ? Option.none() : Option.some(lines.join(\"\\n\"))\n}\n\n/**\n * Creates a history view that starts at the last summary.\n *\n * **Details**\n *\n * `isSummary` identifies the app's summary events. The model reads the last summary, the files\n * attached earlier ({@link attachedFiles}), and the items that are still open\n * ({@link openItems}), followed by the entries after the summary. When the chat has no summary,\n * the view returns every entry.\n *\n * The app's other events after the summary stay entries, which the model doesn't read: map those\n * it should read to messages after this view, as with any history view (see\n * `Assistant.HistoryView`). A summary the handler appends wakes no turn.\n *\n * **Example** (Compacting a long chat after each turn)\n *\n * ```ts\n * import { Assistant, type AssistantEvent, Compaction } from \"effect-assistant\"\n * import * as LanguageModel from \"effect/ai/LanguageModel\"\n * import * as Effect from \"effect/Effect\"\n * import * as Schema from \"effect/Schema\"\n * import * as Stream from \"effect/Stream\"\n *\n * class Summary extends Schema.TaggedClass<Summary>()(\"Summary\", {\n * text: Schema.String,\n * throughSeq: Schema.Int\n * }) {}\n *\n * declare const transcript: (entries: ReadonlyArray<AssistantEvent.AnyEntry>) => string\n *\n * export const MyAssistant = Assistant.make(\"my-assistant\", { events: [Summary] })\n *\n * const MyAssistantLive = MyAssistant.toLayer((turn) => {\n * const compact = Effect.gen(function* () {\n * const chat = yield* MyAssistant.Chat\n * const entries = yield* chat.entries\n *\n * if (Compaction.inputTokens(entries) < 100_000) return\n *\n * const { text } = yield* LanguageModel.generateText({\n * prompt: `Summarize this chat:\\n\\n${transcript(entries)}`\n * })\n *\n * yield* chat.append(Summary.make({ text, throughSeq: entries.at(-1)?.seq ?? 0 }))\n * })\n *\n * // After the reply, so the user doesn't wait for the summary.\n * return Stream.concat(\n * turn.loop({ instructions: \"Be brief.\", history: Compaction.since(Schema.is(Summary)) }),\n * Stream.drain(Stream.fromEffect(compact))\n * )\n * })\n * ```\n *\n * @category utils\n * @since 0.1.0\n */\nexport const since =\n (isSummary: (event: unknown) => event is Summary) =>\n <Events extends { readonly _tag: string }>(\n entries: ReadonlyArray<Assistant.Entry<Events>>\n ): ReadonlyArray<Assistant.Entry<Events> | Prompt.Message> => {\n const last = entries.findLast(({ event }) => isSummary(event))?.event\n\n if (last === undefined || !isSummary(last)) return entries\n\n const open = Option.match(openItems(entries, last.throughSeq), {\n onNone: () => \"\",\n onSome: (items) => `\\n\\n<still_open>\\n${items}\\n</still_open>`\n })\n\n const files = Option.match(attachedFiles(entries, last.throughSeq), {\n onNone: () => \"\",\n onSome: (lines) => `\\n\\n<files_attached_earlier>\\n${lines}\\n</files_attached_earlier>`\n })\n\n return [\n userText(`<earlier_in_this_chat>\\n${last.text}\\n</earlier_in_this_chat>${files}${open}`),\n ...entries.filter(({ seq, event }) => seq > last.throughSeq && !isSummary(event))\n ]\n }\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Compaction.js","names":["AssistantEvent.isLibraryEvent","BlockKit.fromEntries","AssistantActivity.fromEntries","ToolApproval.fromEntries","AssistantEvent.attachmentLine"],"sources":["../src/Compaction.ts"],"sourcesContent":["/**\n * This module provides what a long chat needs so that the model can read a summary in place of\n * everything before it.\n *\n * The app decides when to compact, which model writes the summary, and what the summary says.\n * Its handler reads the chat's entries, has a model write a summary, and stores the summary as one\n * of the assistant's own events (see {@link Summary}). Its loop then reads the chat through\n * {@link since}. {@link inputTokens} helps decide when the prompt has grown large enough to\n * compact. The log keeps every entry, so the user still sees the whole chat.\n *\n * @since 0.1.0\n */\nimport type * as Prompt from \"effect/ai/Prompt\"\nimport * as Option from \"effect/Option\"\nimport * as Predicate from \"effect/Predicate\"\nimport type * as Assistant from \"./Assistant.ts\"\nimport * as AssistantActivity from \"./AssistantActivity.ts\"\nimport * as AssistantEvent from \"./AssistantEvent.ts\"\nimport * as BlockKit from \"./BlockKit.ts\"\nimport { userText } from \"./internal/history.ts\"\nimport { toJsonText } from \"./internal/schema.ts\"\nimport * as ToolApproval from \"./ToolApproval.ts\"\n\n/**\n * Represents the fields a summary event must have: its text and the last `sequence` it covers.\n *\n * Entries after `throughSequence` are kept as they are, so an input that arrived while the summary was\n * being written isn't lost.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface Summary {\n readonly _tag: string\n readonly text: string\n readonly throughSequence: number\n}\n\n/**\n * Returns the number of input tokens used by the chat's last model call, which shows how large\n * the prompt has grown. Returns 0 before the first call.\n *\n * @category utils\n * @since 0.1.0\n */\nexport const inputTokens = (entries: ReadonlyArray<AssistantEvent.AnyEntry>): number => {\n const last = entries.findLast(\n ({ event }) =>\n AssistantEvent.isLibraryEvent(event) && Predicate.isTagged(event, \"ModelOutput\") && event.usage !== undefined\n )?.event\n\n return last !== undefined && AssistantEvent.isLibraryEvent(last) && Predicate.isTagged(last, \"ModelOutput\")\n ? (last.usage?.inputTokens.total ?? 0)\n : 0\n}\n\n/**\n * Returns the items that were open at `throughSequence` and are still open: blocks the user hasn't\n * answered, specialists that are working, and tool calls waiting for the user's approval. The result is text for\n * the model, or `None` when nothing is open.\n *\n * @category utils\n * @since 0.1.0\n */\nexport const openItems = (\n entries: ReadonlyArray<AssistantEvent.AnyEntry>,\n throughSequence: number\n): Option.Option<string> => {\n const sequenceOf = (found: (event: AssistantEvent.AnyEntry[\"event\"]) => boolean) =>\n entries.find(({ event }) => found(event))?.sequence ?? Number.POSITIVE_INFINITY\n\n const opened = (sequence: number) => sequence <= throughSequence\n\n const blocks = [...BlockKit.fromEntries(entries)].flatMap(([messageId, state]) => {\n const shown = entries.find(\n ({ event }) =>\n AssistantEvent.isLibraryEvent(event) && Predicate.isTagged(event, \"AssistantMessage\") && event.id === messageId\n )\n\n const message = shown?.event\n\n return state.open &&\n shown !== undefined &&\n opened(shown.sequence) &&\n message !== undefined &&\n AssistantEvent.isLibraryEvent(message) &&\n Predicate.isTagged(message, \"AssistantMessage\")\n ? [`- message ${messageId}, a ${message.block?.kind} block: ${message.text}`]\n : []\n })\n\n const specialists = [...AssistantActivity.fromEntries(entries).specialists.values()].flatMap((run) =>\n opened(\n sequenceOf(\n (event) =>\n AssistantEvent.isLibraryEvent(event) &&\n Predicate.isTagged(event, \"SpecialistStarted\") &&\n event.runId === run.runId\n )\n )\n ? [`- your ${run.specialist} specialist (${run.runId}), working on: ${run.goal}`]\n : []\n )\n\n const approvals = [...ToolApproval.fromEntries(entries).values()].flatMap((approval) =>\n approval.status === \"pending\" &&\n opened(\n sequenceOf(\n (event) =>\n AssistantEvent.isLibraryEvent(event) &&\n Predicate.isTagged(event, \"ApprovalRequested\") &&\n event.callId === approval.request.callId\n )\n )\n ? [`- your call to ${approval.request.tool} (${approval.request.callId}): ${toJsonText(approval.request.params)}`]\n : []\n )\n\n const sections = [\n blocks.length > 0 ? `Blocks the user hasn't answered:\\n${blocks.join(\"\\n\")}` : \"\",\n specialists.length > 0 ? `Specialists still working:\\n${specialists.join(\"\\n\")}` : \"\",\n approvals.length > 0 ? `Calls waiting for the user's approval:\\n${approvals.join(\"\\n\")}` : \"\"\n ].filter((section) => section.length > 0)\n\n return sections.length === 0 ? Option.none() : Option.some(sections.join(\"\\n\\n\"))\n}\n\n/**\n * Returns the files the user attached up to `throughSequence`, as lines of text for the model, or\n * `None` when there are none. A summary may leave out a file's id, which tools need.\n *\n * @category utils\n * @since 0.1.0\n */\nexport const attachedFiles = (\n entries: ReadonlyArray<AssistantEvent.AnyEntry>,\n throughSequence: number\n): Option.Option<string> => {\n const lines = entries.flatMap(({ sequence, event }) =>\n sequence <= throughSequence && AssistantEvent.isLibraryEvent(event) && Predicate.isTagged(event, \"UserMessage\")\n ? (event.attachments ?? []).map(AssistantEvent.attachmentLine)\n : []\n )\n\n return lines.length === 0 ? Option.none() : Option.some(lines.join(\"\\n\"))\n}\n\n/**\n * Creates a history view that starts at the last summary.\n *\n * **Details**\n *\n * `isSummary` identifies the app's summary events. The model reads the last summary, the files\n * attached earlier ({@link attachedFiles}), and the items that are still open\n * ({@link openItems}), followed by the entries after the summary. When the chat has no summary,\n * the view returns every entry.\n *\n * The app's other events after the summary stay entries, which the model doesn't read: map those\n * it should read to messages after this view, as with any history view (see\n * `Assistant.HistoryView`). A summary the handler appends wakes no turn.\n *\n * **Example** (Compacting a long chat after each turn)\n *\n * ```ts\n * import { Assistant, type AssistantEvent, Compaction } from \"effect-assistant\"\n * import * as LanguageModel from \"effect/ai/LanguageModel\"\n * import * as Effect from \"effect/Effect\"\n * import * as Schema from \"effect/Schema\"\n * import * as Stream from \"effect/Stream\"\n *\n * class Summary extends Schema.TaggedClass<Summary>()(\"Summary\", {\n * text: Schema.String,\n * throughSequence: Schema.Int\n * }) {}\n *\n * declare const transcript: (entries: ReadonlyArray<AssistantEvent.AnyEntry>) => string\n *\n * export const MyAssistant = Assistant.make(\"my-assistant\", { events: [Summary] })\n *\n * const MyAssistantLive = MyAssistant.toLayer((turn) => {\n * const compact = Effect.gen(function* () {\n * const chat = yield* MyAssistant.Chat\n * const entries = yield* chat.entries\n *\n * if (Compaction.inputTokens(entries) < 100_000) return\n *\n * const { text } = yield* LanguageModel.generateText({\n * prompt: `Summarize this chat:\\n\\n${transcript(entries)}`\n * })\n *\n * yield* chat.append(Summary.make({ text, throughSequence: entries.at(-1)?.sequence ?? 0 }))\n * })\n *\n * // After the reply, so the user doesn't wait for the summary.\n * return Stream.concat(\n * turn.run({ instructions: \"Be brief.\", history: Compaction.since(Schema.is(Summary)) }),\n * Stream.drain(Stream.fromEffect(compact))\n * )\n * })\n * ```\n *\n * @category utils\n * @since 0.1.0\n */\nexport const since =\n (isSummary: (event: unknown) => event is Summary) =>\n <Events extends { readonly _tag: string }>(\n entries: ReadonlyArray<Assistant.Entry<Events>>\n ): ReadonlyArray<Assistant.Entry<Events> | Prompt.Message> => {\n const last = entries.findLast(({ event }) => isSummary(event))?.event\n\n if (last === undefined || !isSummary(last)) return entries\n\n const open = Option.match(openItems(entries, last.throughSequence), {\n onNone: () => \"\",\n onSome: (items) => `\\n\\n<still_open>\\n${items}\\n</still_open>`\n })\n\n const files = Option.match(attachedFiles(entries, last.throughSequence), {\n onNone: () => \"\",\n onSome: (lines) => `\\n\\n<files_attached_earlier>\\n${lines}\\n</files_attached_earlier>`\n })\n\n return [\n userText(`<earlier_in_this_chat>\\n${last.text}\\n</earlier_in_this_chat>${files}${open}`),\n ...entries.filter(({ sequence, event }) => sequence > last.throughSequence && !isSummary(event))\n ]\n }\n"],"mappings":"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"}
|
package/dist/Memory.d.ts
CHANGED
|
@@ -106,7 +106,7 @@ export declare const layerMemory: Layer.Layer<MemoryStore>;
|
|
|
106
106
|
* return (turn) =>
|
|
107
107
|
* Stream.unwrap(
|
|
108
108
|
* Effect.map(memory.facts(turn.chatId), (facts) =>
|
|
109
|
-
* turn.
|
|
109
|
+
* turn.run({
|
|
110
110
|
* instructions: `You help a small team.\n\n${Memory.render(facts)}`,
|
|
111
111
|
* toolkit: Memory.Tools
|
|
112
112
|
* })
|
package/dist/Memory.js
CHANGED
|
@@ -163,7 +163,7 @@ const Recall = Tool.make("recall", {
|
|
|
163
163
|
* return (turn) =>
|
|
164
164
|
* Stream.unwrap(
|
|
165
165
|
* Effect.map(memory.facts(turn.chatId), (facts) =>
|
|
166
|
-
* turn.
|
|
166
|
+
* turn.run({
|
|
167
167
|
* instructions: `You help a small team.\n\n${Memory.render(facts)}`,
|
|
168
168
|
* toolkit: Memory.Tools
|
|
169
169
|
* })
|