effect-assistant 0.5.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 +8 -8
- package/dist/AssistantEvent.js +7 -7
- package/dist/AssistantEvent.js.map +1 -1
- package/dist/AssistantLoop.d.ts +2 -2
- package/dist/AssistantLoop.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 +16 -16
- 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 +9 -9
- package/dist/Timeline.js +16 -16
- 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/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.js +4 -4
- package/dist/internal/loop.js.map +1 -1
- package/dist/internal/turns.d.ts.map +1 -1
- package/dist/internal/turns.js +18 -18
- 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 +8 -8
- package/src/AssistantLoop.ts +3 -3
- 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 +22 -22
- package/src/ToolApproval.ts +1 -1
- package/src/ToolDiscovery.ts +1 -1
- 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 +5 -5
- package/src/internal/turns.ts +28 -26
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
|
* })
|
package/dist/Memory.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Memory.js","names":["Uuid.make","ToolFailure.fail","ToolFailure.ToolFailure"],"sources":["../src/Memory.ts"],"sourcesContent":["/**\n * Keeps facts an assistant remembers across chats.\n *\n * The model keeps facts with the `remember` tool, finds them with `recall`, and drops them with\n * `forget` ({@link Tools}). Where facts are stored and who shares them is up to the application,\n * which implements {@link MemoryStore}; every method receives the chat it's for. {@link render}\n * shows the facts in a turn's instructions.\n *\n * @since 0.1.0\n */\nimport * as Tool from \"effect/ai/Tool\"\nimport * as Toolkit from \"effect/ai/Toolkit\"\nimport * as Clock from \"effect/Clock\"\nimport * as Context from \"effect/Context\"\nimport * as DateTime from \"effect/DateTime\"\nimport * as Effect from \"effect/Effect\"\nimport * as Layer from \"effect/Layer\"\nimport * as Option from \"effect/Option\"\nimport * as Ref from \"effect/Ref\"\nimport * as Schema from \"effect/Schema\"\nimport type { ChatId } from \"./ChatId.ts\"\nimport { CurrentChat } from \"./CurrentChat.ts\"\nimport * as Uuid from \"./internal/uuid.ts\"\nimport * as ToolFailure from \"./ToolFailure.ts\"\n\n/**\n * Represents a fact the assistant remembers.\n *\n * It holds the fact's id, one short sentence, and when the fact stops mattering, if it does.\n *\n * @category schemas\n * @since 0.1.0\n */\nexport class Fact extends Schema.Class<Fact>(\"effect-assistant/Memory/Fact\")({\n id: Schema.String,\n text: Schema.String,\n expiresAt: Schema.optional(Schema.DateTimeUtc)\n}) {}\n\n/**\n * Represents a fact to keep, as the `remember` tool hands it to the store.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface NewFact {\n readonly text: string\n readonly expiresAt?: DateTime.Utc | undefined\n /** The id of a fact this one replaces. */\n readonly replaces?: string | undefined\n}\n\n/**\n * Service that keeps facts, implemented by the application.\n *\n * Every method receives the chat it's for, and the application decides whose facts that means\n * (the chat's user, their team, ...).\n *\n * @category services\n * @since 0.1.0\n */\nexport class MemoryStore extends Context.Service<\n MemoryStore,\n {\n /** Returns the unexpired facts a chat should know. */\n readonly facts: (chatId: ChatId) => Effect.Effect<ReadonlyArray<Fact>>\n /** Keeps a fact for the chat's owner, and returns its id. */\n readonly remember: (chatId: ChatId, fact: NewFact) => Effect.Effect<string>\n /**\n * Drops a fact. Fails with a message for the model when the chat's owner has no fact with\n * that id.\n */\n readonly forget: (chatId: ChatId, id: string) => Effect.Effect<void, ToolFailure.ToolFailure>\n /** Returns the chat owner's facts that match a query, best first. */\n readonly recall: (chatId: ChatId, query: string) => Effect.Effect<ReadonlyArray<Fact>>\n }\n>()(\"effect-assistant/Memory/MemoryStore\") {}\n\nconst day = (at: DateTime.Utc): string => DateTime.formatIsoDateUtc(at)\n\nconst line = (fact: Fact): string =>\n `- [${fact.id}] ${fact.text}${fact.expiresAt === undefined ? \"\" : ` (until ${day(fact.expiresAt)})`}`\n\n/**\n * Renders facts as the model reads them, for a turn's instructions.\n *\n * Each fact is listed with its id, so the model can replace or forget it. The result is an empty\n * string when there are no facts.\n *\n * @see {@link Tools} for an example\n *\n * @category utils\n * @since 0.1.0\n */\nexport const render = (facts: ReadonlyArray<Fact>): string =>\n facts.length === 0\n ? \"\"\n : `<memory>\\nWhat you remember from earlier chats. Use it without saying where it came from; when their data or what they say now disagrees, that wins.\\n${facts.map(line).join(\"\\n\")}\\n</memory>`\n\nconst words = (text: string): ReadonlyArray<string> =>\n text\n .toLowerCase()\n .split(/[^\\p{L}\\p{N}]+/u)\n .filter((word) => word.length > 2)\n\nconst matches = (fact: Fact, query: ReadonlyArray<string>): number => {\n const own = words(fact.text)\n\n return query.filter((word) => own.some((w) => w.startsWith(word) || word.startsWith(w))).length\n}\n\nconst live = (now: number) => (fact: Fact) =>\n fact.expiresAt === undefined || DateTime.toEpochMillis(fact.expiresAt) > now\n\n/**\n * Layer that provides a {@link MemoryStore} in memory, for tests and local runs.\n *\n * Every chat shares one set of facts.\n *\n * @category layers\n * @since 0.1.0\n */\nexport const layerMemory: Layer.Layer<MemoryStore> = Layer.effect(\n MemoryStore,\n Effect.gen(function* () {\n // Newest first.\n const all = yield* Ref.make<ReadonlyArray<Fact>>([])\n\n const current = Effect.gen(function* () {\n const now = yield* Clock.currentTimeMillis\n\n return (yield* Ref.get(all)).filter(live(now))\n })\n\n return MemoryStore.of({\n facts: () => current,\n remember: Effect.fnUntraced(function* (_chatId: ChatId, fact: NewFact) {\n const same = (yield* current).find((kept) => kept.text === fact.text.trim())\n const id = same?.id ?? `f-${(yield* Uuid.make).slice(0, 8)}`\n const kept = new Fact({ id, text: fact.text.trim(), expiresAt: fact.expiresAt })\n\n yield* Ref.update(all, (facts) => [kept, ...facts.filter((old) => old.id !== id && old.id !== fact.replaces)])\n\n return id\n }),\n forget: Effect.fnUntraced(function* (_chatId: ChatId, id: string) {\n if (!(yield* current).some((fact) => fact.id === id))\n return yield* ToolFailure.fail(`Nothing remembered as ${id}.`)\n\n yield* Ref.update(all, (facts) => facts.filter((fact) => fact.id !== id))\n }),\n recall: Effect.fnUntraced(function* (_chatId: ChatId, query: string) {\n const wanted = words(query)\n\n return (yield* current)\n .map((fact) => ({ fact, score: matches(fact, wanted) }))\n .filter(({ score }) => score > 0)\n .sort((a, b) => b.score - a.score)\n .slice(0, 10)\n .map(({ fact }) => fact)\n })\n })\n })\n)\n\nconst Remember = Tool.make(\"remember\", {\n description: `Remember something for later chats. Only what you can't look up again and that will still matter: a preference, a standing decision, how they like to work (\"Rotterdam's copy is always Dutch\"). For something short-lived (\"out of office until Friday\"), pass until. Not records your tools can read (orders, locations, budgets, people), not the state of a task in progress, and nothing you'd only need in this chat. One fact per call, in one short sentence that stands on its own. When it changes one you already have, pass that one's id as replaces.`,\n parameters: Schema.Struct({\n text: Schema.String,\n until: Schema.optional(Schema.String).annotate({\n description: \"For something short-lived: the last day it matters, as YYYY-MM-DD.\"\n }),\n replaces: Schema.optional(Schema.String).annotate({ description: \"The id of a fact this one replaces.\" })\n }),\n success: Schema.String,\n failure: ToolFailure.ToolFailure,\n dependencies: [CurrentChat, MemoryStore]\n})\n\nconst Forget = Tool.make(\"forget\", {\n description: \"Forget a fact you remember, by its id, when they ask or it's no longer true.\",\n parameters: Schema.Struct({ id: Schema.String }),\n success: Schema.String,\n failure: ToolFailure.ToolFailure,\n dependencies: [CurrentChat, MemoryStore]\n})\n\nconst Recall = Tool.make(\"recall\", {\n description: \"Search what you remember, by words that would be in it.\",\n parameters: Schema.Struct({ query: Schema.String }),\n success: Schema.Array(Schema.String),\n failure: ToolFailure.ToolFailure,\n dependencies: [CurrentChat, MemoryStore]\n})\n\n/**\n * Toolkit with the `remember`, `forget`, and `recall` tools.\n *\n * **Details**\n *\n * Merge it into the loop's toolkit, and provide {@link layerTools} and a {@link MemoryStore}. Show\n * the facts in the turn's instructions with {@link render}.\n *\n * **Example** (Giving an assistant memory)\n *\n * ```ts\n * import { Assistant, Memory } from \"effect-assistant\"\n * import * as Effect from \"effect/Effect\"\n * import * as Layer from \"effect/Layer\"\n * import * as Stream from \"effect/Stream\"\n *\n * export const MyAssistant = Assistant.make(\"my-assistant\")\n *\n * const MyAssistantLive = MyAssistant.toLayer(\n * Effect.gen(function* () {\n * const memory = yield* Memory.MemoryStore\n *\n * return (turn) =>\n * Stream.unwrap(\n * Effect.map(memory.facts(turn.chatId), (facts) =>\n * turn.loop({\n * instructions: `You help a small team.\\n\\n${Memory.render(facts)}`,\n * toolkit: Memory.Tools\n * })\n * )\n * ).pipe(Stream.provideService(Memory.MemoryStore, memory))\n * })\n * ).pipe(Layer.provide(Memory.layerTools), Layer.provide(Memory.layerMemory))\n * ```\n *\n * @category tools\n * @since 0.1.0\n */\nexport const Tools = Toolkit.make(Remember, Forget, Recall)\n\nconst endOf = (date: string): Option.Option<DateTime.Utc> =>\n /^\\d{4}-\\d{2}-\\d{2}$/.test(date) ? DateTime.make(`${date}T23:59:59Z`) : Option.none()\n\n/**\n * Layer that provides the handlers of {@link Tools}.\n *\n * The handlers call the {@link MemoryStore} with the chat they run in.\n *\n * @see {@link Tools} for an example\n *\n * @category layers\n * @since 0.1.0\n */\nexport const layerTools = Tools.toLayer({\n remember: Effect.fnUntraced(function* ({ text, until, replaces }) {\n const chat = yield* CurrentChat\n const store = yield* MemoryStore\n const expiresAt = until === undefined ? Option.none() : endOf(until)\n\n if (until !== undefined && Option.isNone(expiresAt)) {\n return yield* ToolFailure.fail(`until must be a date as YYYY-MM-DD, not ${until}.`)\n }\n\n const id = yield* store.remember(chat.chatId, { text, expiresAt: Option.getOrUndefined(expiresAt), replaces })\n\n return `Remembered as ${id}.`\n }),\n forget: Effect.fnUntraced(function* ({ id }) {\n const chat = yield* CurrentChat\n\n yield* (yield* MemoryStore).forget(chat.chatId, id)\n\n return `Forgot ${id}.`\n }),\n recall: Effect.fnUntraced(function* ({ query }) {\n const chat = yield* CurrentChat\n const found = yield* (yield* MemoryStore).recall(chat.chatId, query)\n\n return found.map(line)\n })\n})\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Memory.js","names":["Uuid.make","ToolFailure.fail","ToolFailure.ToolFailure"],"sources":["../src/Memory.ts"],"sourcesContent":["/**\n * Keeps facts an assistant remembers across chats.\n *\n * The model keeps facts with the `remember` tool, finds them with `recall`, and drops them with\n * `forget` ({@link Tools}). Where facts are stored and who shares them is up to the application,\n * which implements {@link MemoryStore}; every method receives the chat it's for. {@link render}\n * shows the facts in a turn's instructions.\n *\n * @since 0.1.0\n */\nimport * as Tool from \"effect/ai/Tool\"\nimport * as Toolkit from \"effect/ai/Toolkit\"\nimport * as Clock from \"effect/Clock\"\nimport * as Context from \"effect/Context\"\nimport * as DateTime from \"effect/DateTime\"\nimport * as Effect from \"effect/Effect\"\nimport * as Layer from \"effect/Layer\"\nimport * as Option from \"effect/Option\"\nimport * as Ref from \"effect/Ref\"\nimport * as Schema from \"effect/Schema\"\nimport type { ChatId } from \"./ChatId.ts\"\nimport { CurrentChat } from \"./CurrentChat.ts\"\nimport * as Uuid from \"./internal/uuid.ts\"\nimport * as ToolFailure from \"./ToolFailure.ts\"\n\n/**\n * Represents a fact the assistant remembers.\n *\n * It holds the fact's id, one short sentence, and when the fact stops mattering, if it does.\n *\n * @category schemas\n * @since 0.1.0\n */\nexport class Fact extends Schema.Class<Fact>(\"effect-assistant/Memory/Fact\")({\n id: Schema.String,\n text: Schema.String,\n expiresAt: Schema.optional(Schema.DateTimeUtc)\n}) {}\n\n/**\n * Represents a fact to keep, as the `remember` tool hands it to the store.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface NewFact {\n readonly text: string\n readonly expiresAt?: DateTime.Utc | undefined\n /** The id of a fact this one replaces. */\n readonly replaces?: string | undefined\n}\n\n/**\n * Service that keeps facts, implemented by the application.\n *\n * Every method receives the chat it's for, and the application decides whose facts that means\n * (the chat's user, their team, ...).\n *\n * @category services\n * @since 0.1.0\n */\nexport class MemoryStore extends Context.Service<\n MemoryStore,\n {\n /** Returns the unexpired facts a chat should know. */\n readonly facts: (chatId: ChatId) => Effect.Effect<ReadonlyArray<Fact>>\n /** Keeps a fact for the chat's owner, and returns its id. */\n readonly remember: (chatId: ChatId, fact: NewFact) => Effect.Effect<string>\n /**\n * Drops a fact. Fails with a message for the model when the chat's owner has no fact with\n * that id.\n */\n readonly forget: (chatId: ChatId, id: string) => Effect.Effect<void, ToolFailure.ToolFailure>\n /** Returns the chat owner's facts that match a query, best first. */\n readonly recall: (chatId: ChatId, query: string) => Effect.Effect<ReadonlyArray<Fact>>\n }\n>()(\"effect-assistant/Memory/MemoryStore\") {}\n\nconst day = (at: DateTime.Utc): string => DateTime.formatIsoDateUtc(at)\n\nconst line = (fact: Fact): string =>\n `- [${fact.id}] ${fact.text}${fact.expiresAt === undefined ? \"\" : ` (until ${day(fact.expiresAt)})`}`\n\n/**\n * Renders facts as the model reads them, for a turn's instructions.\n *\n * Each fact is listed with its id, so the model can replace or forget it. The result is an empty\n * string when there are no facts.\n *\n * @see {@link Tools} for an example\n *\n * @category utils\n * @since 0.1.0\n */\nexport const render = (facts: ReadonlyArray<Fact>): string =>\n facts.length === 0\n ? \"\"\n : `<memory>\\nWhat you remember from earlier chats. Use it without saying where it came from; when their data or what they say now disagrees, that wins.\\n${facts.map(line).join(\"\\n\")}\\n</memory>`\n\nconst words = (text: string): ReadonlyArray<string> =>\n text\n .toLowerCase()\n .split(/[^\\p{L}\\p{N}]+/u)\n .filter((word) => word.length > 2)\n\nconst matches = (fact: Fact, query: ReadonlyArray<string>): number => {\n const own = words(fact.text)\n\n return query.filter((word) => own.some((w) => w.startsWith(word) || word.startsWith(w))).length\n}\n\nconst live = (now: number) => (fact: Fact) =>\n fact.expiresAt === undefined || DateTime.toEpochMillis(fact.expiresAt) > now\n\n/**\n * Layer that provides a {@link MemoryStore} in memory, for tests and local runs.\n *\n * Every chat shares one set of facts.\n *\n * @category layers\n * @since 0.1.0\n */\nexport const layerMemory: Layer.Layer<MemoryStore> = Layer.effect(\n MemoryStore,\n Effect.gen(function* () {\n // Newest first.\n const all = yield* Ref.make<ReadonlyArray<Fact>>([])\n\n const current = Effect.gen(function* () {\n const now = yield* Clock.currentTimeMillis\n\n return (yield* Ref.get(all)).filter(live(now))\n })\n\n return MemoryStore.of({\n facts: () => current,\n remember: Effect.fnUntraced(function* (_chatId: ChatId, fact: NewFact) {\n const same = (yield* current).find((kept) => kept.text === fact.text.trim())\n const id = same?.id ?? `f-${(yield* Uuid.make).slice(0, 8)}`\n const kept = new Fact({ id, text: fact.text.trim(), expiresAt: fact.expiresAt })\n\n yield* Ref.update(all, (facts) => [kept, ...facts.filter((old) => old.id !== id && old.id !== fact.replaces)])\n\n return id\n }),\n forget: Effect.fnUntraced(function* (_chatId: ChatId, id: string) {\n if (!(yield* current).some((fact) => fact.id === id))\n return yield* ToolFailure.fail(`Nothing remembered as ${id}.`)\n\n yield* Ref.update(all, (facts) => facts.filter((fact) => fact.id !== id))\n }),\n recall: Effect.fnUntraced(function* (_chatId: ChatId, query: string) {\n const wanted = words(query)\n\n return (yield* current)\n .map((fact) => ({ fact, score: matches(fact, wanted) }))\n .filter(({ score }) => score > 0)\n .sort((a, b) => b.score - a.score)\n .slice(0, 10)\n .map(({ fact }) => fact)\n })\n })\n })\n)\n\nconst Remember = Tool.make(\"remember\", {\n description: `Remember something for later chats. Only what you can't look up again and that will still matter: a preference, a standing decision, how they like to work (\"Rotterdam's copy is always Dutch\"). For something short-lived (\"out of office until Friday\"), pass until. Not records your tools can read (orders, locations, budgets, people), not the state of a task in progress, and nothing you'd only need in this chat. One fact per call, in one short sentence that stands on its own. When it changes one you already have, pass that one's id as replaces.`,\n parameters: Schema.Struct({\n text: Schema.String,\n until: Schema.optional(Schema.String).annotate({\n description: \"For something short-lived: the last day it matters, as YYYY-MM-DD.\"\n }),\n replaces: Schema.optional(Schema.String).annotate({ description: \"The id of a fact this one replaces.\" })\n }),\n success: Schema.String,\n failure: ToolFailure.ToolFailure,\n dependencies: [CurrentChat, MemoryStore]\n})\n\nconst Forget = Tool.make(\"forget\", {\n description: \"Forget a fact you remember, by its id, when they ask or it's no longer true.\",\n parameters: Schema.Struct({ id: Schema.String }),\n success: Schema.String,\n failure: ToolFailure.ToolFailure,\n dependencies: [CurrentChat, MemoryStore]\n})\n\nconst Recall = Tool.make(\"recall\", {\n description: \"Search what you remember, by words that would be in it.\",\n parameters: Schema.Struct({ query: Schema.String }),\n success: Schema.Array(Schema.String),\n failure: ToolFailure.ToolFailure,\n dependencies: [CurrentChat, MemoryStore]\n})\n\n/**\n * Toolkit with the `remember`, `forget`, and `recall` tools.\n *\n * **Details**\n *\n * Merge it into the loop's toolkit, and provide {@link layerTools} and a {@link MemoryStore}. Show\n * the facts in the turn's instructions with {@link render}.\n *\n * **Example** (Giving an assistant memory)\n *\n * ```ts\n * import { Assistant, Memory } from \"effect-assistant\"\n * import * as Effect from \"effect/Effect\"\n * import * as Layer from \"effect/Layer\"\n * import * as Stream from \"effect/Stream\"\n *\n * export const MyAssistant = Assistant.make(\"my-assistant\")\n *\n * const MyAssistantLive = MyAssistant.toLayer(\n * Effect.gen(function* () {\n * const memory = yield* Memory.MemoryStore\n *\n * return (turn) =>\n * Stream.unwrap(\n * Effect.map(memory.facts(turn.chatId), (facts) =>\n * turn.run({\n * instructions: `You help a small team.\\n\\n${Memory.render(facts)}`,\n * toolkit: Memory.Tools\n * })\n * )\n * ).pipe(Stream.provideService(Memory.MemoryStore, memory))\n * })\n * ).pipe(Layer.provide(Memory.layerTools), Layer.provide(Memory.layerMemory))\n * ```\n *\n * @category tools\n * @since 0.1.0\n */\nexport const Tools = Toolkit.make(Remember, Forget, Recall)\n\nconst endOf = (date: string): Option.Option<DateTime.Utc> =>\n /^\\d{4}-\\d{2}-\\d{2}$/.test(date) ? DateTime.make(`${date}T23:59:59Z`) : Option.none()\n\n/**\n * Layer that provides the handlers of {@link Tools}.\n *\n * The handlers call the {@link MemoryStore} with the chat they run in.\n *\n * @see {@link Tools} for an example\n *\n * @category layers\n * @since 0.1.0\n */\nexport const layerTools = Tools.toLayer({\n remember: Effect.fnUntraced(function* ({ text, until, replaces }) {\n const chat = yield* CurrentChat\n const store = yield* MemoryStore\n const expiresAt = until === undefined ? Option.none() : endOf(until)\n\n if (until !== undefined && Option.isNone(expiresAt)) {\n return yield* ToolFailure.fail(`until must be a date as YYYY-MM-DD, not ${until}.`)\n }\n\n const id = yield* store.remember(chat.chatId, { text, expiresAt: Option.getOrUndefined(expiresAt), replaces })\n\n return `Remembered as ${id}.`\n }),\n forget: Effect.fnUntraced(function* ({ id }) {\n const chat = yield* CurrentChat\n\n yield* (yield* MemoryStore).forget(chat.chatId, id)\n\n return `Forgot ${id}.`\n }),\n recall: Effect.fnUntraced(function* ({ query }) {\n const chat = yield* CurrentChat\n const found = yield* (yield* MemoryStore).recall(chat.chatId, query)\n\n return found.map(line)\n })\n})\n"],"mappings":"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"}
|
package/dist/Skill.d.ts
CHANGED
|
@@ -113,7 +113,7 @@ export interface Toolkit<R = never> extends AiToolkit.Toolkit<{
|
|
|
113
113
|
* const AssistantSkills = Skill.toolkit([orderingCampaigns, sharingAssets])
|
|
114
114
|
*
|
|
115
115
|
* const MyAssistantLive = MyAssistant.toLayer((turn) =>
|
|
116
|
-
* turn.
|
|
116
|
+
* turn.run({ instructions, toolkit: Toolkit.merge(MyTools, AssistantSkills) })
|
|
117
117
|
* ).pipe(Layer.provide([MyToolsLive, AssistantSkills.layer]))
|
|
118
118
|
* ```
|
|
119
119
|
*
|
package/dist/Skill.js
CHANGED
|
@@ -98,7 +98,7 @@ function toolkitOf(skills) {
|
|
|
98
98
|
* const AssistantSkills = Skill.toolkit([orderingCampaigns, sharingAssets])
|
|
99
99
|
*
|
|
100
100
|
* const MyAssistantLive = MyAssistant.toLayer((turn) =>
|
|
101
|
-
* turn.
|
|
101
|
+
* turn.run({ instructions, toolkit: Toolkit.merge(MyTools, AssistantSkills) })
|
|
102
102
|
* ).pipe(Layer.provide([MyToolsLive, AssistantSkills.layer]))
|
|
103
103
|
* ```
|
|
104
104
|
*
|
package/dist/Skill.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Skill.js","names":["ToolFailure.ToolFailure","ToolFailure.fail","Instructions.resolve"],"sources":["../src/Skill.ts"],"sourcesContent":["/**\n * Defines skills, which are playbooks with step-by-step instructions for a kind of task.\n *\n * A list of skills becomes a toolkit with one tool, `load_skill` ({@link toolkit}). Its\n * description is the catalog, each skill's name and when to use it, and the model calls it to read\n * a skill's instructions when a task matches. Merge it into a loop's toolkit like any other.\n *\n * @since 0.1.0\n */\nimport * as Tool from \"effect/ai/Tool\"\nimport * as AiToolkit from \"effect/ai/Toolkit\"\nimport * as Effect from \"effect/Effect\"\nimport type * as Layer from \"effect/Layer\"\nimport * as Schema from \"effect/Schema\"\nimport * as Instructions from \"./Instructions.ts\"\nimport * as ToolFailure from \"./ToolFailure.ts\"\n\n/**\n * Represents a skill.\n *\n * `R` is the services its instructions need when they're loaded.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface Skill<R = never> {\n readonly name: string\n /** When to use it, in one line. This is all the model sees until it loads the skill. */\n readonly description: string\n /** Text, or an Effect resolved each time the skill is loaded. */\n readonly instructions: Instructions.Instructions<R>\n}\n\n/**\n * Creates a skill from its name, a description of when to use it, and its instructions.\n *\n * **Details**\n *\n * Write the description as a trigger (\"Use when…\"), since it's all the model sees until it loads\n * the skill. The instructions are text, or an Effect that is resolved each time the skill is\n * loaded. Offer skills to a loop with {@link toolkit}.\n *\n * **Example** (Defining a playbook for ordering campaigns)\n *\n * ```ts\n * import { Skill } from \"effect-assistant\"\n *\n * const orderingCampaigns = Skill.make(\"Ordering a campaign\", {\n * description: \"Use when the user wants to order or start a campaign.\",\n * instructions: `1. Pick the product with list_campaign_products.\n * 2. You need who it should reach and a budget. Ask for what's missing, one question at a time.\n * 3. When you have both, call start_campaign.`\n * })\n * ```\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const make = <R = never>(\n name: string,\n options: {\n /** When to use it, in one line. This is all the model sees until it loads the skill. */\n readonly description: string\n /** Text, or an Effect resolved each time the skill is loaded. */\n readonly instructions: Instructions.Instructions<R>\n }\n): Skill<R> => ({ name, description: options.description, instructions: options.instructions })\n\n/**\n * Extracts the services that skills need when they're loaded.\n *\n * @category utility types\n * @since 0.1.0\n */\nexport type Services<S> = S extends Skill<infer R> ? R : never\n\nconst loadSkill = (description: string) =>\n Tool.make(\"load_skill\", {\n description,\n parameters: Schema.Struct({ name: Schema.String }),\n success: Schema.String,\n failure: ToolFailure.ToolFailure\n })\n\n/**\n * Represents the `load_skill` tool, which reads a skill's instructions by name.\n *\n * @category models\n * @since 0.4.0\n */\nexport type LoadSkill = ReturnType<typeof loadSkill>\n\n/**\n * Represents a list of skills as a toolkit: the `load_skill` tool, and the layer with its handler.\n *\n * `R` is the services the skills' instructions need, which the layer requires.\n *\n * @category models\n * @since 0.4.0\n */\nexport interface Toolkit<R = never> extends AiToolkit.Toolkit<{ readonly load_skill: LoadSkill }> {\n /** The handler of `load_skill`, which resolves the instructions of these skills. */\n readonly layer: Layer.Layer<Tool.Handler<\"load_skill\">, never, R>\n}\n\nconst catalog = (skills: ReadonlyArray<Skill<unknown>>): string =>\n [\n \"Load a skill, a playbook for a kind of task, by name, and follow it for the rest of the task. When one matches what you're doing, load it in the same step as your first lookups, not in a step of its own.\",\n \"\",\n \"Skills:\",\n ...skills.map((skill) => `- ${skill.name}: ${skill.description}`)\n ].join(\"\\n\")\n\nfunction toolkitOf<const Skills extends ReadonlyArray<Skill<unknown>>>(\n skills: Skills\n): Toolkit<Services<Skills[number]>>\n// The overload states what the layer requires: the services of these skills' instructions.\nfunction toolkitOf(skills: ReadonlyArray<Skill<unknown>>): Toolkit<unknown> {\n const byName = new Map(skills.map((skill) => [skill.name, skill]))\n const tools = AiToolkit.make(loadSkill(catalog(skills)))\n\n const layer = tools.toLayer(\n Effect.gen(function* () {\n const context = yield* Effect.context<unknown>()\n\n return {\n load_skill: Effect.fnUntraced(function* ({ name }) {\n const skill = byName.get(name)\n\n if (skill === undefined) {\n return yield* ToolFailure.fail(\n `No skill named ${name}. The skills are: ${skills.map((known) => known.name).join(\", \")}.`\n )\n }\n\n const resolved = yield* Instructions.resolve(skill.instructions).pipe(Effect.provideContext(context))\n\n return resolved.text\n })\n }\n })\n )\n\n return Object.assign(tools, { layer })\n}\n\n/**\n * Creates a toolkit from a list of skills: the `load_skill` tool, whose description lists each\n * skill's name and when to use it, and its handler's `layer`.\n *\n * **Details**\n *\n * Merge the toolkit into a loop's own with `Toolkit.merge`, and provide its `layer`, which requires\n * the services the skills' instructions need. The model loads a skill by name; a name that isn't\n * in the list is a failure the model reads, with the names there are.\n *\n * The handler resolves only the skills of its own list. To offer each user the skills they may\n * use, build the toolkit in the turn from that user's list, and provide its layer to the loop's\n * stream (`Stream.provide(skills.layer)`): building one does no work.\n *\n * **Example** (Giving an assistant its playbooks)\n *\n * ```ts\n * import { Skill } from \"effect-assistant\"\n * import * as Layer from \"effect/Layer\"\n * import * as Toolkit from \"effect/ai/Toolkit\"\n *\n * const AssistantSkills = Skill.toolkit([orderingCampaigns, sharingAssets])\n *\n * const MyAssistantLive = MyAssistant.toLayer((turn) =>\n * turn.
|
|
1
|
+
{"version":3,"file":"Skill.js","names":["ToolFailure.ToolFailure","ToolFailure.fail","Instructions.resolve"],"sources":["../src/Skill.ts"],"sourcesContent":["/**\n * Defines skills, which are playbooks with step-by-step instructions for a kind of task.\n *\n * A list of skills becomes a toolkit with one tool, `load_skill` ({@link toolkit}). Its\n * description is the catalog, each skill's name and when to use it, and the model calls it to read\n * a skill's instructions when a task matches. Merge it into a loop's toolkit like any other.\n *\n * @since 0.1.0\n */\nimport * as Tool from \"effect/ai/Tool\"\nimport * as AiToolkit from \"effect/ai/Toolkit\"\nimport * as Effect from \"effect/Effect\"\nimport type * as Layer from \"effect/Layer\"\nimport * as Schema from \"effect/Schema\"\nimport * as Instructions from \"./Instructions.ts\"\nimport * as ToolFailure from \"./ToolFailure.ts\"\n\n/**\n * Represents a skill.\n *\n * `R` is the services its instructions need when they're loaded.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface Skill<R = never> {\n readonly name: string\n /** When to use it, in one line. This is all the model sees until it loads the skill. */\n readonly description: string\n /** Text, or an Effect resolved each time the skill is loaded. */\n readonly instructions: Instructions.Instructions<R>\n}\n\n/**\n * Creates a skill from its name, a description of when to use it, and its instructions.\n *\n * **Details**\n *\n * Write the description as a trigger (\"Use when…\"), since it's all the model sees until it loads\n * the skill. The instructions are text, or an Effect that is resolved each time the skill is\n * loaded. Offer skills to a loop with {@link toolkit}.\n *\n * **Example** (Defining a playbook for ordering campaigns)\n *\n * ```ts\n * import { Skill } from \"effect-assistant\"\n *\n * const orderingCampaigns = Skill.make(\"Ordering a campaign\", {\n * description: \"Use when the user wants to order or start a campaign.\",\n * instructions: `1. Pick the product with list_campaign_products.\n * 2. You need who it should reach and a budget. Ask for what's missing, one question at a time.\n * 3. When you have both, call start_campaign.`\n * })\n * ```\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const make = <R = never>(\n name: string,\n options: {\n /** When to use it, in one line. This is all the model sees until it loads the skill. */\n readonly description: string\n /** Text, or an Effect resolved each time the skill is loaded. */\n readonly instructions: Instructions.Instructions<R>\n }\n): Skill<R> => ({ name, description: options.description, instructions: options.instructions })\n\n/**\n * Extracts the services that skills need when they're loaded.\n *\n * @category utility types\n * @since 0.1.0\n */\nexport type Services<S> = S extends Skill<infer R> ? R : never\n\nconst loadSkill = (description: string) =>\n Tool.make(\"load_skill\", {\n description,\n parameters: Schema.Struct({ name: Schema.String }),\n success: Schema.String,\n failure: ToolFailure.ToolFailure\n })\n\n/**\n * Represents the `load_skill` tool, which reads a skill's instructions by name.\n *\n * @category models\n * @since 0.4.0\n */\nexport type LoadSkill = ReturnType<typeof loadSkill>\n\n/**\n * Represents a list of skills as a toolkit: the `load_skill` tool, and the layer with its handler.\n *\n * `R` is the services the skills' instructions need, which the layer requires.\n *\n * @category models\n * @since 0.4.0\n */\nexport interface Toolkit<R = never> extends AiToolkit.Toolkit<{ readonly load_skill: LoadSkill }> {\n /** The handler of `load_skill`, which resolves the instructions of these skills. */\n readonly layer: Layer.Layer<Tool.Handler<\"load_skill\">, never, R>\n}\n\nconst catalog = (skills: ReadonlyArray<Skill<unknown>>): string =>\n [\n \"Load a skill, a playbook for a kind of task, by name, and follow it for the rest of the task. When one matches what you're doing, load it in the same step as your first lookups, not in a step of its own.\",\n \"\",\n \"Skills:\",\n ...skills.map((skill) => `- ${skill.name}: ${skill.description}`)\n ].join(\"\\n\")\n\nfunction toolkitOf<const Skills extends ReadonlyArray<Skill<unknown>>>(\n skills: Skills\n): Toolkit<Services<Skills[number]>>\n// The overload states what the layer requires: the services of these skills' instructions.\nfunction toolkitOf(skills: ReadonlyArray<Skill<unknown>>): Toolkit<unknown> {\n const byName = new Map(skills.map((skill) => [skill.name, skill]))\n const tools = AiToolkit.make(loadSkill(catalog(skills)))\n\n const layer = tools.toLayer(\n Effect.gen(function* () {\n const context = yield* Effect.context<unknown>()\n\n return {\n load_skill: Effect.fnUntraced(function* ({ name }) {\n const skill = byName.get(name)\n\n if (skill === undefined) {\n return yield* ToolFailure.fail(\n `No skill named ${name}. The skills are: ${skills.map((known) => known.name).join(\", \")}.`\n )\n }\n\n const resolved = yield* Instructions.resolve(skill.instructions).pipe(Effect.provideContext(context))\n\n return resolved.text\n })\n }\n })\n )\n\n return Object.assign(tools, { layer })\n}\n\n/**\n * Creates a toolkit from a list of skills: the `load_skill` tool, whose description lists each\n * skill's name and when to use it, and its handler's `layer`.\n *\n * **Details**\n *\n * Merge the toolkit into a loop's own with `Toolkit.merge`, and provide its `layer`, which requires\n * the services the skills' instructions need. The model loads a skill by name; a name that isn't\n * in the list is a failure the model reads, with the names there are.\n *\n * The handler resolves only the skills of its own list. To offer each user the skills they may\n * use, build the toolkit in the turn from that user's list, and provide its layer to the loop's\n * stream (`Stream.provide(skills.layer)`): building one does no work.\n *\n * **Example** (Giving an assistant its playbooks)\n *\n * ```ts\n * import { Skill } from \"effect-assistant\"\n * import * as Layer from \"effect/Layer\"\n * import * as Toolkit from \"effect/ai/Toolkit\"\n *\n * const AssistantSkills = Skill.toolkit([orderingCampaigns, sharingAssets])\n *\n * const MyAssistantLive = MyAssistant.toLayer((turn) =>\n * turn.run({ instructions, toolkit: Toolkit.merge(MyTools, AssistantSkills) })\n * ).pipe(Layer.provide([MyToolsLive, AssistantSkills.layer]))\n * ```\n *\n * **Example** (Offering each user the skills their role allows)\n *\n * ```ts\n * import { Skill } from \"effect-assistant\"\n * import * as Effect from \"effect/Effect\"\n * import * as Stream from \"effect/Stream\"\n * import * as Toolkit from \"effect/ai/Toolkit\"\n *\n * const MyAssistantLive = MyAssistant.toLayer((turn) =>\n * Stream.unwrap(\n * Effect.map(roleOf(turn.chatId), (role) => {\n * const skills = Skill.toolkit(allSkills.filter((skill) => allowed(role, skill)))\n *\n * return turn\n * .loop({ instructions, toolkit: Toolkit.merge(MyTools, skills) })\n * .pipe(Stream.provide(skills.layer))\n * })\n * )\n * )\n * ```\n *\n * @category constructors\n * @since 0.4.0\n */\nexport const toolkit: <const Skills extends ReadonlyArray<Skill<unknown>>>(\n skills: Skills\n) => Toolkit<Services<Skills[number]>> = toolkitOf\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0DA,MAAa,QACX,MACA,aAMc;CAAE;CAAM,aAAa,QAAQ;CAAa,cAAc,QAAQ;AAAa;AAU7F,MAAM,aAAa,gBACjB,KAAK,KAAK,cAAc;CACtB;CACA,YAAY,OAAO,OAAO,EAAE,MAAM,OAAO,OAAO,CAAC;CACjD,SAAS,OAAO;CAChB,SAASA;AACX,CAAC;AAuBH,MAAM,WAAW,WACf;CACE;CACA;CACA;CACA,GAAG,OAAO,KAAK,UAAU,KAAK,MAAM,KAAK,IAAI,MAAM,aAAa;AAClE,CAAC,CAAC,KAAK,IAAI;AAMb,SAAS,UAAU,QAAyD;CAC1E,MAAM,SAAS,IAAI,IAAI,OAAO,KAAK,UAAU,CAAC,MAAM,MAAM,KAAK,CAAC,CAAC;CACjE,MAAM,QAAQ,UAAU,KAAK,UAAU,QAAQ,MAAM,CAAC,CAAC;CAEvD,MAAM,QAAQ,MAAM,QAClB,OAAO,IAAI,aAAa;EACtB,MAAM,UAAU,OAAO,OAAO,QAAiB;EAE/C,OAAO,EACL,YAAY,OAAO,WAAW,WAAW,EAAE,QAAQ;GACjD,MAAM,QAAQ,OAAO,IAAI,IAAI;GAE7B,IAAI,UAAU,KAAA,GACZ,OAAO,OAAOC,KACZ,kBAAkB,KAAK,oBAAoB,OAAO,KAAK,UAAU,MAAM,IAAI,CAAC,CAAC,KAAK,IAAI,EAAE,EAC1F;GAKF,QAAO,OAFiBC,QAAqB,MAAM,YAAY,CAAC,CAAC,KAAK,OAAO,eAAe,OAAO,CAAC,EAAA,CAEpF;EAClB,CAAC,EACH;CACF,CAAC,CACH;CAEA,OAAO,OAAO,OAAO,OAAO,EAAE,MAAM,CAAC;AACvC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsDA,MAAa,UAE4B"}
|
package/dist/Specialist.d.ts
CHANGED
|
@@ -182,7 +182,7 @@ export interface Built {
|
|
|
182
182
|
*
|
|
183
183
|
* export const MyAssistant = Assistant.make("my-assistant", { specialists: [Research] })
|
|
184
184
|
*
|
|
185
|
-
* const MyAssistantLive = MyAssistant.toLayer((turn) => turn.
|
|
185
|
+
* const MyAssistantLive = MyAssistant.toLayer((turn) => turn.run({ instructions })).pipe(
|
|
186
186
|
* Layer.provide(ResearchLive)
|
|
187
187
|
* )
|
|
188
188
|
* ```
|
package/dist/Specialist.js
CHANGED
|
@@ -41,7 +41,7 @@ const toStream = (result) => Effect.isEffect(result) ? Stream.unwrap(result) : r
|
|
|
41
41
|
*
|
|
42
42
|
* export const MyAssistant = Assistant.make("my-assistant", { specialists: [Research] })
|
|
43
43
|
*
|
|
44
|
-
* const MyAssistantLive = MyAssistant.toLayer((turn) => turn.
|
|
44
|
+
* const MyAssistantLive = MyAssistant.toLayer((turn) => turn.run({ instructions })).pipe(
|
|
45
45
|
* Layer.provide(ResearchLive)
|
|
46
46
|
* )
|
|
47
47
|
* ```
|
package/dist/Specialist.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Specialist.js","names":[],"sources":["../src/Specialist.ts"],"sourcesContent":["/**\n * Defines specialists, which are focused agents an assistant hands work to in the background.\n *\n * A specialist has a definition, created with {@link make}, that gives its name and when to use\n * it, and a handler, provided with its `toLayer`, that works on a {@link Brief} as a loop. The\n * {@link Runner} service starts, steers, and stops a chat's specialist runs.\n *\n * @since 0.1.0\n */\nimport type * as AiError from \"effect/ai/AiError\"\nimport type * as LanguageModel from \"effect/ai/LanguageModel\"\nimport type * as Prompt from \"effect/ai/Prompt\"\nimport type * as Tool from \"effect/ai/Tool\"\nimport type * as Toolkit from \"effect/ai/Toolkit\"\nimport * as Context from \"effect/Context\"\nimport * as Effect from \"effect/Effect\"\nimport * as Layer from \"effect/Layer\"\nimport * as Schema from \"effect/Schema\"\nimport type * as Scope from \"effect/Scope\"\nimport * as Stream from \"effect/Stream\"\nimport type * as AssistantLoop from \"./AssistantLoop.ts\"\nimport type { ChatId } from \"./ChatId.ts\"\nimport type { CurrentChat } from \"./CurrentChat.ts\"\nimport type * as Instructions from \"./Instructions.ts\"\nimport type { RunId } from \"./RunId.ts\"\n\nconst TypeId = \"~effect-assistant/Specialist\"\n\n/**\n * Represents a specialist's definition.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface Specialist<Name extends string = string> {\n readonly [TypeId]: typeof TypeId\n readonly name: Name\n /** When to use the specialist. The assistant sees this. */\n readonly description: string\n /** The key of the specialist's handler, as `toLayer` provides it. */\n readonly Handler: Context.Key<SpecialistHandler<Name>, Built>\n /**\n * Provides the specialist's handler. The handler may use the {@link RunServices} and the\n * services its build Effect required (see {@link CheckServices}).\n */\n readonly toLayer: <R = never, RE = never, E = never, RX = never>(\n build: (Handler<R, RE> | Effect.Effect<Handler<R, RE>, E, RX>) &\n CheckServices<Exclude<R | RE, RunServices | RX | Tool.Handler<string>>>\n ) => Layer.Layer<SpecialistHandler<Name>, never, Exclude<RX, Scope.Scope | CurrentChat> | ToolHandlers<R | RE>>\n}\n\n/**\n * Represents the check `toLayer` applies to the services a handler uses.\n *\n * **Details**\n *\n * Every service a handler uses must be one the run (or turn) provides, or one its build Effect\n * required, which the layer provides. A handler that uses any other service gets this type\n * error, which names those services.\n *\n * @category utility types\n * @since 0.1.0\n */\nexport type CheckServices<Undeclared> = [Undeclared] extends [never]\n ? unknown\n : {\n readonly \"~undeclaredServices\": {\n readonly error: \"The handler uses services that neither the turn provides nor its build Effect requires\"\n readonly services: Undeclared\n }\n }\n\n/**\n * Extracts the tool handlers (`Toolkit.toLayer`) that a handler's loop uses.\n *\n * They are requirements of the layer, so `Layer.provide(MyToolsLive)` provides them, and a\n * missing one is a type error.\n *\n * @category utility types\n * @since 0.1.0\n */\nexport type ToolHandlers<R> = Extract<R, Tool.Handler<string>>\n\n/**\n * Represents any specialist.\n *\n * @category utility types\n * @since 0.1.0\n */\nexport type Any = Specialist<any>\n\n/**\n * Identifies a specialist's handler in a context.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface SpecialistHandler<Name extends string> {\n readonly _SpecialistHandler: Name\n}\n\n/**\n * Extracts the handlers that these specialists need.\n *\n * @category utility types\n * @since 0.1.0\n */\nexport type HandlerOf<S> = S extends Specialist<infer Name> ? SpecialistHandler<Name> : never\n\n/**\n * The services a run provides to its handler, which are its chat, the chat's runner, and the\n * model.\n *\n * @category utility types\n * @since 0.1.0\n */\nexport type RunServices = CurrentChat | Runner | LanguageModel.LanguageModel\n\n/**\n * Represents the options for `run.loop`.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface LoopOptions<T extends Record<string, Tool.Any> = Record<never, never>> {\n readonly instructions: string | Instructions.Resolved\n /**\n * The loop's tools, as a native toolkit whose handlers come from the layer. Merge in a\n * `Skill.toolkit(...)` for playbooks.\n */\n readonly toolkit?: Toolkit.Toolkit<T> | undefined\n /** The model calls allowed per brief. Defaults to 8. */\n readonly maxSteps?: number | undefined\n}\n\n/**\n * Represents one run of a specialist on a brief, as its handler sees it.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface Run {\n readonly runId: RunId\n /** The chat the run belongs to, which is the chat of the turn that spawned it. */\n readonly chatId: ChatId\n readonly brief: Brief\n /**\n * The run's history, with the brief, what the run did so far, and the messages sent to it since\n * (follow-ups and steering). Read it before every model call, as the loop does.\n */\n readonly history: Effect.Effect<ReadonlyArray<Prompt.Message>>\n /** Runs the agent loop over the run's history, with the given instructions and tools. */\n readonly loop: <T extends Record<string, Tool.Any> = Record<never, never>>(\n options: LoopOptions<T>\n ) => Stream.Stream<\n AssistantLoop.Event,\n AiError.AiError,\n LanguageModel.LanguageModel | AssistantLoop.ToolkitServices<T>\n >\n}\n\n/**\n * Represents a specialist's handler, which works on a brief and returns a stream of what the loop\n * did.\n *\n * It may return the stream from an Effect. `R` is the services the stream needs, and `RE` the\n * services the Effect needs.\n *\n * @category models\n * @since 0.1.0\n */\nexport type Handler<R = never, RE = R> = (\n run: Run\n) =>\n | Stream.Stream<AssistantLoop.Event, unknown, R>\n | Effect.Effect<Stream.Stream<AssistantLoop.Event, unknown, R>, unknown, RE>\n\n/**\n * Represents a specialist's handler as its layer provides it.\n *\n * `open` runs the build for a chat (once per chat activation, see `CurrentChat`) and returns\n * the handler with the layer's services already provided.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface Built {\n readonly open: Effect.Effect<\n (run: Run) => Stream.Stream<AssistantLoop.Event, unknown, RunServices>,\n never,\n Scope.Scope | CurrentChat\n >\n}\n\nconst toStream = <R, RE>(\n result:\n | Stream.Stream<AssistantLoop.Event, unknown, R>\n | Effect.Effect<Stream.Stream<AssistantLoop.Event, unknown, R>, unknown, RE>\n): Stream.Stream<AssistantLoop.Event, unknown, R | RE> => (Effect.isEffect(result) ? Stream.unwrap(result) : result)\n\n/**\n * Creates a specialist from its name and a description of when to use it.\n *\n * **Details**\n *\n * The specialist's `toLayer` provides its handler, which is usually `run.loop` with the\n * specialist's own instructions and tools. List the specialist in `Assistant.make`'s\n * `specialists`, and provide its layer to the assistant's.\n *\n * **Example** (Defining a research specialist)\n *\n * ```ts\n * import { Assistant, Specialist } from \"effect-assistant\"\n * import * as Layer from \"effect/Layer\"\n *\n * const Research = Specialist.make(\"research\", {\n * description: \"Researches past campaigns and orders. Give it the question and any context.\"\n * })\n *\n * const ResearchLive = Research.toLayer((run) =>\n * run.loop({\n * instructions: \"Answer with the findings and numbers the colleague needs, in a few lines.\",\n * toolkit: ResearchTools\n * })\n * ).pipe(Layer.provide(ResearchToolsLive))\n *\n * export const MyAssistant = Assistant.make(\"my-assistant\", { specialists: [Research] })\n *\n * const MyAssistantLive = MyAssistant.toLayer((turn) => turn.loop({ instructions })).pipe(\n * Layer.provide(ResearchLive)\n * )\n * ```\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const make = <const Name extends string>(\n name: Name,\n options: {\n /** When to use it. The assistant sees this. */\n readonly description: string\n }\n): Specialist<Name> => {\n const Handler = Context.Service<SpecialistHandler<Name>, Built>(`effect-assistant/Specialist/${name}`)\n\n // The signature checks what the handler uses (`CheckServices`); the layer's services, which the\n // build required, are provided to it and to every run's stream, so what's left is the run\n // services.\n function toLayer<R = never, RE = never, E = never, RX = never>(\n build: (Handler<R, RE> | Effect.Effect<Handler<R, RE>, E, RX>) &\n CheckServices<Exclude<R | RE, RunServices | RX | Tool.Handler<string>>>\n ): Layer.Layer<SpecialistHandler<Name>, never, Exclude<RX, Scope.Scope | CurrentChat> | ToolHandlers<R | RE>>\n function toLayer(\n build: Handler<any, any> | Effect.Effect<Handler<any, any>, unknown, any>\n ): Layer.Layer<SpecialistHandler<Name>, never, any> {\n return Layer.effect(\n Handler,\n Effect.gen(function* () {\n const context = yield* Effect.context()\n\n return {\n // The build runs per chat activation, in the chat; here it only keeps the layer's\n // services for it.\n open: Effect.gen(function* () {\n const handler = Effect.isEffect(build) ? yield* build : build\n\n return (run: Run) => Stream.provideContext(toStream(handler(run)), context)\n }).pipe(Effect.provideContext(context), Effect.orDie)\n }\n })\n )\n }\n\n return { [TypeId]: TypeId, name, description: options.description, Handler, toLayer }\n}\n\n/**\n * Schema for the brief the assistant gives a specialist.\n *\n * Only `goal` is required.\n *\n * @category schemas\n * @since 0.1.0\n */\nexport const Brief = Schema.Struct({\n goal: Schema.String.annotate({ description: \"What to find out or do.\" }),\n context: Schema.optional(Schema.String).annotate({\n description: \"What it needs to know. It can't see this chat.\"\n }),\n successCriteria: Schema.optional(Schema.String).annotate({ description: \"When it is done.\" }),\n report: Schema.optional(Schema.String).annotate({\n description: \"What to put in the answer, and how.\"\n }),\n limits: Schema.optional(Schema.String).annotate({\n description: \"Hard limits for this job, like read-only or a deadline.\"\n })\n})\n\n/**\n * Represents the brief the assistant gives a specialist.\n *\n * @category models\n * @since 0.1.0\n */\nexport type Brief = typeof Brief.Type\n\n/**\n * Represents a request to start a specialist run in the runner's chat.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface SpawnRequest {\n /** The specialist's name. */\n readonly specialist: string\n readonly brief: Brief\n}\n\n/**\n * Represents why a run request can't be carried out.\n *\n * **Details**\n *\n * The `reason` says which request failed: `\"UnknownSpecialist\"` when `spawn` names no specialist\n * of the assistant, `\"RunNotFound\"` when `message` names a run that is gone, and `\"NotRunning\"`\n * when `stop` names a run that isn't running. The `message` is written for the model.\n *\n * @category errors\n * @since 0.1.0\n */\nexport class SpecialistError extends Schema.Error<SpecialistError>(\"effect-assistant/Specialist/SpecialistError\")({\n _tag: Schema.tag(\"SpecialistError\"),\n reason: Schema.Literals([\"UnknownSpecialist\", \"RunNotFound\", \"NotRunning\"]),\n message: Schema.String\n}) {}\n\n/**\n * Service that runs a chat's specialists and keeps finished runs warm for follow-ups.\n *\n * The engine provides it to every turn. The specialist tools use it, and a handler can too.\n *\n * @category services\n * @since 0.1.0\n */\nexport class Runner extends Context.Service<\n Runner,\n {\n /** Starts a run and returns its id. */\n readonly spawn: (request: SpawnRequest) => Effect.Effect<RunId, SpecialistError>\n /** Steers a running run, or continues a finished one. */\n readonly message: (runId: RunId, text: string) => Effect.Effect<\"steered\" | \"continued\", SpecialistError>\n /** Interrupts a running run. */\n readonly stop: (runId: RunId) => Effect.Effect<void, SpecialistError>\n }\n>()(\"effect-assistant/Specialist/Runner\") {}\n"],"mappings":";;;;;;;;;;;;;AA0BA,MAAM,SAAS;AAwKf,MAAM,YACJ,WAGyD,OAAO,SAAS,MAAM,IAAI,OAAO,OAAO,MAAM,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsC7G,MAAa,QACX,MACA,YAIqB;CACrB,MAAM,UAAU,QAAQ,QAAwC,+BAA+B,MAAM;CASrG,SAAS,QACP,OACkD;EAClD,OAAO,MAAM,OACX,SACA,OAAO,IAAI,aAAa;GACtB,MAAM,UAAU,OAAO,OAAO,QAAQ;GAEtC,OAAO,EAGL,MAAM,OAAO,IAAI,aAAa;IAC5B,MAAM,UAAU,OAAO,SAAS,KAAK,IAAI,OAAO,QAAQ;IAExD,QAAQ,QAAa,OAAO,eAAe,SAAS,QAAQ,GAAG,CAAC,GAAG,OAAO;GAC5E,CAAC,CAAC,CAAC,KAAK,OAAO,eAAe,OAAO,GAAG,OAAO,KAAK,EACtD;EACF,CAAC,CACH;CACF;CAEA,OAAO;GAAG,SAAS;EAAQ;EAAM,aAAa,QAAQ;EAAa;EAAS;CAAQ;AACtF;;;;;;;;;AAUA,MAAa,QAAQ,OAAO,OAAO;CACjC,MAAM,OAAO,OAAO,SAAS,EAAE,aAAa,0BAA0B,CAAC;CACvE,SAAS,OAAO,SAAS,OAAO,MAAM,CAAC,CAAC,SAAS,EAC/C,aAAa,iDACf,CAAC;CACD,iBAAiB,OAAO,SAAS,OAAO,MAAM,CAAC,CAAC,SAAS,EAAE,aAAa,mBAAmB,CAAC;CAC5F,QAAQ,OAAO,SAAS,OAAO,MAAM,CAAC,CAAC,SAAS,EAC9C,aAAa,sCACf,CAAC;CACD,QAAQ,OAAO,SAAS,OAAO,MAAM,CAAC,CAAC,SAAS,EAC9C,aAAa,0DACf,CAAC;AACH,CAAC;;;;;;;;;;;;;AAkCD,IAAa,kBAAb,cAAqC,OAAO,MAAuB,6CAA6C,CAAC,CAAC;CAChH,MAAM,OAAO,IAAI,iBAAiB;CAClC,QAAQ,OAAO,SAAS;EAAC;EAAqB;EAAe;CAAY,CAAC;CAC1E,SAAS,OAAO;AAClB,CAAC,CAAC,CAAC,CAAC;;;;;;;;;AAUJ,IAAa,SAAb,cAA4B,QAAQ,QAUlC,CAAC,CAAC,oCAAoC,CAAC,CAAC,CAAC"}
|
|
1
|
+
{"version":3,"file":"Specialist.js","names":[],"sources":["../src/Specialist.ts"],"sourcesContent":["/**\n * Defines specialists, which are focused agents an assistant hands work to in the background.\n *\n * A specialist has a definition, created with {@link make}, that gives its name and when to use\n * it, and a handler, provided with its `toLayer`, that works on a {@link Brief} as a loop. The\n * {@link Runner} service starts, steers, and stops a chat's specialist runs.\n *\n * @since 0.1.0\n */\nimport type * as AiError from \"effect/ai/AiError\"\nimport type * as LanguageModel from \"effect/ai/LanguageModel\"\nimport type * as Prompt from \"effect/ai/Prompt\"\nimport type * as Tool from \"effect/ai/Tool\"\nimport type * as Toolkit from \"effect/ai/Toolkit\"\nimport * as Context from \"effect/Context\"\nimport * as Effect from \"effect/Effect\"\nimport * as Layer from \"effect/Layer\"\nimport * as Schema from \"effect/Schema\"\nimport type * as Scope from \"effect/Scope\"\nimport * as Stream from \"effect/Stream\"\nimport type * as AssistantLoop from \"./AssistantLoop.ts\"\nimport type { ChatId } from \"./ChatId.ts\"\nimport type { CurrentChat } from \"./CurrentChat.ts\"\nimport type * as Instructions from \"./Instructions.ts\"\nimport type { RunId } from \"./RunId.ts\"\n\nconst TypeId = \"~effect-assistant/Specialist\"\n\n/**\n * Represents a specialist's definition.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface Specialist<Name extends string = string> {\n readonly [TypeId]: typeof TypeId\n readonly name: Name\n /** When to use the specialist. The assistant sees this. */\n readonly description: string\n /** The key of the specialist's handler, as `toLayer` provides it. */\n readonly Handler: Context.Key<SpecialistHandler<Name>, Built>\n /**\n * Provides the specialist's handler. The handler may use the {@link RunServices} and the\n * services its build Effect required (see {@link CheckServices}).\n */\n readonly toLayer: <R = never, RE = never, E = never, RX = never>(\n build: (Handler<R, RE> | Effect.Effect<Handler<R, RE>, E, RX>) &\n CheckServices<Exclude<R | RE, RunServices | RX | Tool.Handler<string>>>\n ) => Layer.Layer<SpecialistHandler<Name>, never, Exclude<RX, Scope.Scope | CurrentChat> | ToolHandlers<R | RE>>\n}\n\n/**\n * Represents the check `toLayer` applies to the services a handler uses.\n *\n * **Details**\n *\n * Every service a handler uses must be one the run (or turn) provides, or one its build Effect\n * required, which the layer provides. A handler that uses any other service gets this type\n * error, which names those services.\n *\n * @category utility types\n * @since 0.1.0\n */\nexport type CheckServices<Undeclared> = [Undeclared] extends [never]\n ? unknown\n : {\n readonly \"~undeclaredServices\": {\n readonly error: \"The handler uses services that neither the turn provides nor its build Effect requires\"\n readonly services: Undeclared\n }\n }\n\n/**\n * Extracts the tool handlers (`Toolkit.toLayer`) that a handler's loop uses.\n *\n * They are requirements of the layer, so `Layer.provide(MyToolsLive)` provides them, and a\n * missing one is a type error.\n *\n * @category utility types\n * @since 0.1.0\n */\nexport type ToolHandlers<R> = Extract<R, Tool.Handler<string>>\n\n/**\n * Represents any specialist.\n *\n * @category utility types\n * @since 0.1.0\n */\nexport type Any = Specialist<any>\n\n/**\n * Identifies a specialist's handler in a context.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface SpecialistHandler<Name extends string> {\n readonly _SpecialistHandler: Name\n}\n\n/**\n * Extracts the handlers that these specialists need.\n *\n * @category utility types\n * @since 0.1.0\n */\nexport type HandlerOf<S> = S extends Specialist<infer Name> ? SpecialistHandler<Name> : never\n\n/**\n * The services a run provides to its handler, which are its chat, the chat's runner, and the\n * model.\n *\n * @category utility types\n * @since 0.1.0\n */\nexport type RunServices = CurrentChat | Runner | LanguageModel.LanguageModel\n\n/**\n * Represents the options for `run.loop`.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface LoopOptions<T extends Record<string, Tool.Any> = Record<never, never>> {\n readonly instructions: string | Instructions.Resolved\n /**\n * The loop's tools, as a native toolkit whose handlers come from the layer. Merge in a\n * `Skill.toolkit(...)` for playbooks.\n */\n readonly toolkit?: Toolkit.Toolkit<T> | undefined\n /** The model calls allowed per brief. Defaults to 8. */\n readonly maxSteps?: number | undefined\n}\n\n/**\n * Represents one run of a specialist on a brief, as its handler sees it.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface Run {\n readonly runId: RunId\n /** The chat the run belongs to, which is the chat of the turn that spawned it. */\n readonly chatId: ChatId\n readonly brief: Brief\n /**\n * The run's history, with the brief, what the run did so far, and the messages sent to it since\n * (follow-ups and steering). Read it before every model call, as the loop does.\n */\n readonly history: Effect.Effect<ReadonlyArray<Prompt.Message>>\n /** Runs the agent loop over the run's history, with the given instructions and tools. */\n readonly loop: <T extends Record<string, Tool.Any> = Record<never, never>>(\n options: LoopOptions<T>\n ) => Stream.Stream<\n AssistantLoop.Event,\n AiError.AiError,\n LanguageModel.LanguageModel | AssistantLoop.ToolkitServices<T>\n >\n}\n\n/**\n * Represents a specialist's handler, which works on a brief and returns a stream of what the loop\n * did.\n *\n * It may return the stream from an Effect. `R` is the services the stream needs, and `RE` the\n * services the Effect needs.\n *\n * @category models\n * @since 0.1.0\n */\nexport type Handler<R = never, RE = R> = (\n run: Run\n) =>\n | Stream.Stream<AssistantLoop.Event, unknown, R>\n | Effect.Effect<Stream.Stream<AssistantLoop.Event, unknown, R>, unknown, RE>\n\n/**\n * Represents a specialist's handler as its layer provides it.\n *\n * `open` runs the build for a chat (once per chat activation, see `CurrentChat`) and returns\n * the handler with the layer's services already provided.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface Built {\n readonly open: Effect.Effect<\n (run: Run) => Stream.Stream<AssistantLoop.Event, unknown, RunServices>,\n never,\n Scope.Scope | CurrentChat\n >\n}\n\nconst toStream = <R, RE>(\n result:\n | Stream.Stream<AssistantLoop.Event, unknown, R>\n | Effect.Effect<Stream.Stream<AssistantLoop.Event, unknown, R>, unknown, RE>\n): Stream.Stream<AssistantLoop.Event, unknown, R | RE> => (Effect.isEffect(result) ? Stream.unwrap(result) : result)\n\n/**\n * Creates a specialist from its name and a description of when to use it.\n *\n * **Details**\n *\n * The specialist's `toLayer` provides its handler, which is usually `run.loop` with the\n * specialist's own instructions and tools. List the specialist in `Assistant.make`'s\n * `specialists`, and provide its layer to the assistant's.\n *\n * **Example** (Defining a research specialist)\n *\n * ```ts\n * import { Assistant, Specialist } from \"effect-assistant\"\n * import * as Layer from \"effect/Layer\"\n *\n * const Research = Specialist.make(\"research\", {\n * description: \"Researches past campaigns and orders. Give it the question and any context.\"\n * })\n *\n * const ResearchLive = Research.toLayer((run) =>\n * run.loop({\n * instructions: \"Answer with the findings and numbers the colleague needs, in a few lines.\",\n * toolkit: ResearchTools\n * })\n * ).pipe(Layer.provide(ResearchToolsLive))\n *\n * export const MyAssistant = Assistant.make(\"my-assistant\", { specialists: [Research] })\n *\n * const MyAssistantLive = MyAssistant.toLayer((turn) => turn.run({ instructions })).pipe(\n * Layer.provide(ResearchLive)\n * )\n * ```\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const make = <const Name extends string>(\n name: Name,\n options: {\n /** When to use it. The assistant sees this. */\n readonly description: string\n }\n): Specialist<Name> => {\n const Handler = Context.Service<SpecialistHandler<Name>, Built>(`effect-assistant/Specialist/${name}`)\n\n // The signature checks what the handler uses (`CheckServices`); the layer's services, which the\n // build required, are provided to it and to every run's stream, so what's left is the run\n // services.\n function toLayer<R = never, RE = never, E = never, RX = never>(\n build: (Handler<R, RE> | Effect.Effect<Handler<R, RE>, E, RX>) &\n CheckServices<Exclude<R | RE, RunServices | RX | Tool.Handler<string>>>\n ): Layer.Layer<SpecialistHandler<Name>, never, Exclude<RX, Scope.Scope | CurrentChat> | ToolHandlers<R | RE>>\n function toLayer(\n build: Handler<any, any> | Effect.Effect<Handler<any, any>, unknown, any>\n ): Layer.Layer<SpecialistHandler<Name>, never, any> {\n return Layer.effect(\n Handler,\n Effect.gen(function* () {\n const context = yield* Effect.context()\n\n return {\n // The build runs per chat activation, in the chat; here it only keeps the layer's\n // services for it.\n open: Effect.gen(function* () {\n const handler = Effect.isEffect(build) ? yield* build : build\n\n return (run: Run) => Stream.provideContext(toStream(handler(run)), context)\n }).pipe(Effect.provideContext(context), Effect.orDie)\n }\n })\n )\n }\n\n return { [TypeId]: TypeId, name, description: options.description, Handler, toLayer }\n}\n\n/**\n * Schema for the brief the assistant gives a specialist.\n *\n * Only `goal` is required.\n *\n * @category schemas\n * @since 0.1.0\n */\nexport const Brief = Schema.Struct({\n goal: Schema.String.annotate({ description: \"What to find out or do.\" }),\n context: Schema.optional(Schema.String).annotate({\n description: \"What it needs to know. It can't see this chat.\"\n }),\n successCriteria: Schema.optional(Schema.String).annotate({ description: \"When it is done.\" }),\n report: Schema.optional(Schema.String).annotate({\n description: \"What to put in the answer, and how.\"\n }),\n limits: Schema.optional(Schema.String).annotate({\n description: \"Hard limits for this job, like read-only or a deadline.\"\n })\n})\n\n/**\n * Represents the brief the assistant gives a specialist.\n *\n * @category models\n * @since 0.1.0\n */\nexport type Brief = typeof Brief.Type\n\n/**\n * Represents a request to start a specialist run in the runner's chat.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface SpawnRequest {\n /** The specialist's name. */\n readonly specialist: string\n readonly brief: Brief\n}\n\n/**\n * Represents why a run request can't be carried out.\n *\n * **Details**\n *\n * The `reason` says which request failed: `\"UnknownSpecialist\"` when `spawn` names no specialist\n * of the assistant, `\"RunNotFound\"` when `message` names a run that is gone, and `\"NotRunning\"`\n * when `stop` names a run that isn't running. The `message` is written for the model.\n *\n * @category errors\n * @since 0.1.0\n */\nexport class SpecialistError extends Schema.Error<SpecialistError>(\"effect-assistant/Specialist/SpecialistError\")({\n _tag: Schema.tag(\"SpecialistError\"),\n reason: Schema.Literals([\"UnknownSpecialist\", \"RunNotFound\", \"NotRunning\"]),\n message: Schema.String\n}) {}\n\n/**\n * Service that runs a chat's specialists and keeps finished runs warm for follow-ups.\n *\n * The engine provides it to every turn. The specialist tools use it, and a handler can too.\n *\n * @category services\n * @since 0.1.0\n */\nexport class Runner extends Context.Service<\n Runner,\n {\n /** Starts a run and returns its id. */\n readonly spawn: (request: SpawnRequest) => Effect.Effect<RunId, SpecialistError>\n /** Steers a running run, or continues a finished one. */\n readonly message: (runId: RunId, text: string) => Effect.Effect<\"steered\" | \"continued\", SpecialistError>\n /** Interrupts a running run. */\n readonly stop: (runId: RunId) => Effect.Effect<void, SpecialistError>\n }\n>()(\"effect-assistant/Specialist/Runner\") {}\n"],"mappings":";;;;;;;;;;;;;AA0BA,MAAM,SAAS;AAwKf,MAAM,YACJ,WAGyD,OAAO,SAAS,MAAM,IAAI,OAAO,OAAO,MAAM,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsC7G,MAAa,QACX,MACA,YAIqB;CACrB,MAAM,UAAU,QAAQ,QAAwC,+BAA+B,MAAM;CASrG,SAAS,QACP,OACkD;EAClD,OAAO,MAAM,OACX,SACA,OAAO,IAAI,aAAa;GACtB,MAAM,UAAU,OAAO,OAAO,QAAQ;GAEtC,OAAO,EAGL,MAAM,OAAO,IAAI,aAAa;IAC5B,MAAM,UAAU,OAAO,SAAS,KAAK,IAAI,OAAO,QAAQ;IAExD,QAAQ,QAAa,OAAO,eAAe,SAAS,QAAQ,GAAG,CAAC,GAAG,OAAO;GAC5E,CAAC,CAAC,CAAC,KAAK,OAAO,eAAe,OAAO,GAAG,OAAO,KAAK,EACtD;EACF,CAAC,CACH;CACF;CAEA,OAAO;GAAG,SAAS;EAAQ;EAAM,aAAa,QAAQ;EAAa;EAAS;CAAQ;AACtF;;;;;;;;;AAUA,MAAa,QAAQ,OAAO,OAAO;CACjC,MAAM,OAAO,OAAO,SAAS,EAAE,aAAa,0BAA0B,CAAC;CACvE,SAAS,OAAO,SAAS,OAAO,MAAM,CAAC,CAAC,SAAS,EAC/C,aAAa,iDACf,CAAC;CACD,iBAAiB,OAAO,SAAS,OAAO,MAAM,CAAC,CAAC,SAAS,EAAE,aAAa,mBAAmB,CAAC;CAC5F,QAAQ,OAAO,SAAS,OAAO,MAAM,CAAC,CAAC,SAAS,EAC9C,aAAa,sCACf,CAAC;CACD,QAAQ,OAAO,SAAS,OAAO,MAAM,CAAC,CAAC,SAAS,EAC9C,aAAa,0DACf,CAAC;AACH,CAAC;;;;;;;;;;;;;AAkCD,IAAa,kBAAb,cAAqC,OAAO,MAAuB,6CAA6C,CAAC,CAAC;CAChH,MAAM,OAAO,IAAI,iBAAiB;CAClC,QAAQ,OAAO,SAAS;EAAC;EAAqB;EAAe;CAAY,CAAC;CAC1E,SAAS,OAAO;AAClB,CAAC,CAAC,CAAC,CAAC;;;;;;;;;AAUJ,IAAa,SAAb,cAA4B,QAAQ,QAUlC,CAAC,CAAC,oCAAoC,CAAC,CAAC,CAAC"}
|
package/dist/Timeline.d.ts
CHANGED
|
@@ -221,13 +221,13 @@ export type Activity = typeof Activity.Type;
|
|
|
221
221
|
/**
|
|
222
222
|
* Represents a chat's timeline: the chat as its user sees it, typed with the app's events it shows.
|
|
223
223
|
* It holds the chat's items in order, what is working in it, whether the last turn failed, and
|
|
224
|
-
* whether the chat was deleted. `
|
|
224
|
+
* whether the chat was deleted. `sequence` is the position in the chat's log that the timeline is up to.
|
|
225
225
|
*
|
|
226
226
|
* @category models
|
|
227
227
|
* @since 0.3.0
|
|
228
228
|
*/
|
|
229
229
|
export interface Timeline<Events = never> {
|
|
230
|
-
readonly
|
|
230
|
+
readonly sequence: number;
|
|
231
231
|
readonly items: ReadonlyArray<Item<Events>>;
|
|
232
232
|
readonly activity: Activity;
|
|
233
233
|
/** Whether the last turn failed with no message after it. */
|
|
@@ -242,14 +242,14 @@ export interface Timeline<Events = never> {
|
|
|
242
242
|
*
|
|
243
243
|
* A change holds each item that is new or changed (for example, a message whose block was
|
|
244
244
|
* answered or closed, or a call the user approved), in full. It also holds the current activity, whether the last turn failed,
|
|
245
|
-
* and whether the chat was deleted. `
|
|
245
|
+
* and whether the chat was deleted. `sequence` is the position in the chat's log that the change is up
|
|
246
246
|
* to.
|
|
247
247
|
*
|
|
248
248
|
* @category models
|
|
249
249
|
* @since 0.3.0
|
|
250
250
|
*/
|
|
251
251
|
export interface Change<Events = never> {
|
|
252
|
-
readonly
|
|
252
|
+
readonly sequence: number;
|
|
253
253
|
readonly items: ReadonlyArray<Item<Events>>;
|
|
254
254
|
readonly activity: Activity;
|
|
255
255
|
readonly failed: boolean;
|
|
@@ -459,7 +459,7 @@ export interface Definition<Events = never> {
|
|
|
459
459
|
*/
|
|
460
460
|
export interface ChangesOptions<Show extends ReadonlyArray<AppEventSchema> = readonly []> extends ShowOptions<Show> {
|
|
461
461
|
/**
|
|
462
|
-
* The `
|
|
462
|
+
* The `sequence` up to which the client already has the timeline. The result contains only the
|
|
463
463
|
* changes after it. Defaults to `0`, every change.
|
|
464
464
|
*/
|
|
465
465
|
readonly after?: number | undefined;
|
|
@@ -471,9 +471,9 @@ export interface ChangesOptions<Show extends ReadonlyArray<AppEventSchema> = rea
|
|
|
471
471
|
*
|
|
472
472
|
* The entries must stream from the start of the chat's log, as `assistant.subscribe(chatId)` does
|
|
473
473
|
* without `after`. The fold reads every entry, because a message depends on earlier entries: its
|
|
474
|
-
* block is answered or closed later. `after` only skips emitting the changes up to that `
|
|
475
|
-
* client that has the timeline up to a `
|
|
476
|
-
* not to `subscribe`: a subscription that starts after a `
|
|
474
|
+
* block is answered or closed later. `after` only skips emitting the changes up to that `sequence`, so a
|
|
475
|
+
* client that has the timeline up to a `sequence` receives only what changed since. Pass `after` here,
|
|
476
|
+
* not to `subscribe`: a subscription that starts after a `sequence` gives the fold an incomplete log,
|
|
477
477
|
* and the changes it produces are wrong.
|
|
478
478
|
*
|
|
479
479
|
* Each chunk of entries becomes one change; the stored entries, for example, arrive as one chunk.
|
|
@@ -492,7 +492,7 @@ export interface ChangesOptions<Show extends ReadonlyArray<AppEventSchema> = rea
|
|
|
492
492
|
* import * as Schema from "effect/Schema"
|
|
493
493
|
* import * as Stream from "effect/Stream"
|
|
494
494
|
*
|
|
495
|
-
* class Summary extends Schema.TaggedClass<Summary>()("Summary", { text: Schema.String,
|
|
495
|
+
* class Summary extends Schema.TaggedClass<Summary>()("Summary", { text: Schema.String, throughSequence: Schema.Int }) {}
|
|
496
496
|
* class DailyBudgetReached extends Schema.TaggedClass<DailyBudgetReached>()("DailyBudgetReached", {}) {}
|
|
497
497
|
*
|
|
498
498
|
* const MyAssistant = Assistant.make("my-assistant", { events: [Summary, DailyBudgetReached] })
|
package/dist/Timeline.js
CHANGED
|
@@ -175,7 +175,7 @@ function makeChange(events = []) {
|
|
|
175
175
|
const decodeItem = Schema.decodeUnknownOption(AnyItem);
|
|
176
176
|
const encodeItem = Schema.encodeUnknownSync(AnyItem);
|
|
177
177
|
return Schema.Struct({
|
|
178
|
-
|
|
178
|
+
sequence: Schema.Int,
|
|
179
179
|
items: Schema.Array(Schema.Json).pipe(Schema.decodeTo(Schema.Array(Schema.toType(AnyItem)), SchemaTransformation.transform({
|
|
180
180
|
decode: (json) => json.flatMap((item) => Option.toArray(decodeItem(item))),
|
|
181
181
|
encode: (items) => items.map((item) => encodeItem(item))
|
|
@@ -227,7 +227,7 @@ const Change = makeChange;
|
|
|
227
227
|
* @since 0.1.0
|
|
228
228
|
*/
|
|
229
229
|
const empty = {
|
|
230
|
-
|
|
230
|
+
sequence: 0,
|
|
231
231
|
items: [],
|
|
232
232
|
activity: {
|
|
233
233
|
working: false,
|
|
@@ -274,7 +274,7 @@ const apply = (timeline, change) => {
|
|
|
274
274
|
const changed = new Map(change.items.map((item) => [item.id, item]));
|
|
275
275
|
const known = new Set(timeline.items.map((item) => item.id));
|
|
276
276
|
return {
|
|
277
|
-
|
|
277
|
+
sequence: Math.max(timeline.sequence, change.sequence),
|
|
278
278
|
items: [...timeline.items.map((item) => changed.get(item.id) ?? item), ...change.items.filter((item) => !known.has(item.id))],
|
|
279
279
|
activity: change.activity,
|
|
280
280
|
failed: change.failed,
|
|
@@ -383,7 +383,7 @@ const sameActivity = (a, b) => a.working === b.working && a.specialists.length =
|
|
|
383
383
|
const folding = (show) => {
|
|
384
384
|
const shown = new Map(show.map((schema) => [tagOf(schema), schema]));
|
|
385
385
|
let approvals = /* @__PURE__ */ new Map();
|
|
386
|
-
let
|
|
386
|
+
let sequence = 0;
|
|
387
387
|
let failed = false;
|
|
388
388
|
let deleted = false;
|
|
389
389
|
let activity = empty$1;
|
|
@@ -481,7 +481,7 @@ const folding = (show) => {
|
|
|
481
481
|
const event = Schema.is(schema)(entry.event) ? Option.some(entry.event) : decodeWith(schema, entry.event);
|
|
482
482
|
return Option.map(event, (decoded) => ({
|
|
483
483
|
_tag: decoded._tag,
|
|
484
|
-
id: `app-event:${entry.
|
|
484
|
+
id: `app-event:${entry.sequence}`,
|
|
485
485
|
at: entry.at,
|
|
486
486
|
event: decoded
|
|
487
487
|
}));
|
|
@@ -508,13 +508,13 @@ const folding = (show) => {
|
|
|
508
508
|
deleted
|
|
509
509
|
};
|
|
510
510
|
const { event } = entry;
|
|
511
|
-
|
|
511
|
+
sequence = entry.sequence;
|
|
512
512
|
activity = apply$3(activity, entry);
|
|
513
513
|
if (!isLibraryEvent(event)) {
|
|
514
514
|
const now = activityOf(activity);
|
|
515
515
|
return Option.match(eventItemOf(entry), {
|
|
516
516
|
onNone: () => sameActivity(previous.activity, now) ? void 0 : {
|
|
517
|
-
|
|
517
|
+
sequence,
|
|
518
518
|
items: [],
|
|
519
519
|
activity: now,
|
|
520
520
|
failed,
|
|
@@ -523,7 +523,7 @@ const folding = (show) => {
|
|
|
523
523
|
onSome: (item) => {
|
|
524
524
|
items.set(item.id, item);
|
|
525
525
|
return {
|
|
526
|
-
|
|
526
|
+
sequence,
|
|
527
527
|
items: [item],
|
|
528
528
|
activity: now,
|
|
529
529
|
failed,
|
|
@@ -545,7 +545,7 @@ const folding = (show) => {
|
|
|
545
545
|
}), ...approvalItems(event, decidedBefore)];
|
|
546
546
|
const now = activityOf(activity);
|
|
547
547
|
return all.length === 0 && failed === previous.failed && deleted === previous.deleted && sameActivity(previous.activity, now) ? void 0 : {
|
|
548
|
-
|
|
548
|
+
sequence,
|
|
549
549
|
items: all,
|
|
550
550
|
activity: now,
|
|
551
551
|
failed,
|
|
@@ -556,7 +556,7 @@ const folding = (show) => {
|
|
|
556
556
|
step,
|
|
557
557
|
get snapshot() {
|
|
558
558
|
return {
|
|
559
|
-
|
|
559
|
+
sequence,
|
|
560
560
|
items: [...items.values()],
|
|
561
561
|
activity: activityOf(activity),
|
|
562
562
|
failed,
|
|
@@ -571,7 +571,7 @@ const merge = (changes) => {
|
|
|
571
571
|
const items = /* @__PURE__ */ new Map();
|
|
572
572
|
for (const change of changes) for (const item of change.items) items.set(item.id, item);
|
|
573
573
|
return {
|
|
574
|
-
|
|
574
|
+
sequence: last.sequence,
|
|
575
575
|
items: [...items.values()],
|
|
576
576
|
activity: last.activity,
|
|
577
577
|
failed: last.failed,
|
|
@@ -583,7 +583,7 @@ function changesWith(entries, _definition, options) {
|
|
|
583
583
|
return entries.pipe(Stream.mapAccumArray(() => folding(options?.show ?? []), (timeline, chunk) => {
|
|
584
584
|
const changed = chunk.flatMap((entry) => {
|
|
585
585
|
const change = timeline.step(entry);
|
|
586
|
-
return change === void 0 || entry.
|
|
586
|
+
return change === void 0 || entry.sequence <= after ? [] : [change];
|
|
587
587
|
});
|
|
588
588
|
const merged = merge(changed);
|
|
589
589
|
return [timeline, merged === void 0 ? [] : [merged]];
|
|
@@ -596,9 +596,9 @@ function changesWith(entries, _definition, options) {
|
|
|
596
596
|
*
|
|
597
597
|
* The entries must stream from the start of the chat's log, as `assistant.subscribe(chatId)` does
|
|
598
598
|
* without `after`. The fold reads every entry, because a message depends on earlier entries: its
|
|
599
|
-
* block is answered or closed later. `after` only skips emitting the changes up to that `
|
|
600
|
-
* client that has the timeline up to a `
|
|
601
|
-
* not to `subscribe`: a subscription that starts after a `
|
|
599
|
+
* block is answered or closed later. `after` only skips emitting the changes up to that `sequence`, so a
|
|
600
|
+
* client that has the timeline up to a `sequence` receives only what changed since. Pass `after` here,
|
|
601
|
+
* not to `subscribe`: a subscription that starts after a `sequence` gives the fold an incomplete log,
|
|
602
602
|
* and the changes it produces are wrong.
|
|
603
603
|
*
|
|
604
604
|
* Each chunk of entries becomes one change; the stored entries, for example, arrive as one chunk.
|
|
@@ -617,7 +617,7 @@ function changesWith(entries, _definition, options) {
|
|
|
617
617
|
* import * as Schema from "effect/Schema"
|
|
618
618
|
* import * as Stream from "effect/Stream"
|
|
619
619
|
*
|
|
620
|
-
* class Summary extends Schema.TaggedClass<Summary>()("Summary", { text: Schema.String,
|
|
620
|
+
* class Summary extends Schema.TaggedClass<Summary>()("Summary", { text: Schema.String, throughSequence: Schema.Int }) {}
|
|
621
621
|
* class DailyBudgetReached extends Schema.TaggedClass<DailyBudgetReached>()("DailyBudgetReached", {}) {}
|
|
622
622
|
*
|
|
623
623
|
* const MyAssistant = Assistant.make("my-assistant", { events: [Summary, DailyBudgetReached] })
|