effect-assistant 0.5.0 → 0.7.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.
Files changed (77) hide show
  1. package/dist/Assistant.d.ts +10 -10
  2. package/dist/Assistant.d.ts.map +1 -1
  3. package/dist/Assistant.js +1 -1
  4. package/dist/Assistant.js.map +1 -1
  5. package/dist/AssistantActivity.d.ts +2 -2
  6. package/dist/AssistantActivity.d.ts.map +1 -1
  7. package/dist/AssistantActivity.js +5 -5
  8. package/dist/AssistantActivity.js.map +1 -1
  9. package/dist/AssistantEngine.d.ts +3 -3
  10. package/dist/AssistantEngine.js +2 -2
  11. package/dist/AssistantEngine.js.map +1 -1
  12. package/dist/AssistantEvent.d.ts +8 -8
  13. package/dist/AssistantEvent.js +7 -7
  14. package/dist/AssistantEvent.js.map +1 -1
  15. package/dist/AssistantLoop.d.ts +2 -2
  16. package/dist/AssistantLoop.js.map +1 -1
  17. package/dist/ChatLog.d.ts +9 -9
  18. package/dist/ChatLog.d.ts.map +1 -1
  19. package/dist/ChatLog.js +21 -21
  20. package/dist/ChatLog.js.map +1 -1
  21. package/dist/Compaction.d.ts +10 -10
  22. package/dist/Compaction.d.ts.map +1 -1
  23. package/dist/Compaction.js +16 -16
  24. package/dist/Compaction.js.map +1 -1
  25. package/dist/Memory.d.ts +1 -1
  26. package/dist/Memory.js +1 -1
  27. package/dist/Memory.js.map +1 -1
  28. package/dist/Skill.d.ts +2 -2
  29. package/dist/Skill.js +2 -2
  30. package/dist/Skill.js.map +1 -1
  31. package/dist/Specialist.d.ts +17 -16
  32. package/dist/Specialist.d.ts.map +1 -1
  33. package/dist/Specialist.js +5 -5
  34. package/dist/Specialist.js.map +1 -1
  35. package/dist/Timeline.d.ts +9 -9
  36. package/dist/Timeline.js +16 -16
  37. package/dist/Timeline.js.map +1 -1
  38. package/dist/ToolApproval.d.ts +1 -1
  39. package/dist/ToolApproval.js +1 -1
  40. package/dist/ToolApproval.js.map +1 -1
  41. package/dist/ToolDiscovery.d.ts +1 -1
  42. package/dist/ToolDiscovery.js +1 -1
  43. package/dist/ToolDiscovery.js.map +1 -1
  44. package/dist/internal/chat.js +4 -4
  45. package/dist/internal/chat.js.map +1 -1
  46. package/dist/internal/events.js +1 -1
  47. package/dist/internal/events.js.map +1 -1
  48. package/dist/internal/history.js +23 -23
  49. package/dist/internal/history.js.map +1 -1
  50. package/dist/internal/loop.d.ts +1 -1
  51. package/dist/internal/loop.js +4 -4
  52. package/dist/internal/loop.js.map +1 -1
  53. package/dist/internal/specialists.js +6 -6
  54. package/dist/internal/specialists.js.map +1 -1
  55. package/dist/internal/turns.d.ts.map +1 -1
  56. package/dist/internal/turns.js +18 -18
  57. package/dist/internal/turns.js.map +1 -1
  58. package/package.json +1 -1
  59. package/src/Assistant.ts +11 -11
  60. package/src/AssistantActivity.ts +9 -7
  61. package/src/AssistantEngine.ts +3 -3
  62. package/src/AssistantEvent.ts +8 -8
  63. package/src/AssistantLoop.ts +3 -3
  64. package/src/ChatLog.ts +31 -26
  65. package/src/Compaction.ts +21 -21
  66. package/src/Memory.ts +1 -1
  67. package/src/Skill.ts +2 -2
  68. package/src/Specialist.ts +19 -18
  69. package/src/Timeline.ts +22 -22
  70. package/src/ToolApproval.ts +1 -1
  71. package/src/ToolDiscovery.ts +1 -1
  72. package/src/internal/chat.ts +4 -4
  73. package/src/internal/events.ts +1 -1
  74. package/src/internal/history.ts +32 -28
  75. package/src/internal/loop.ts +5 -5
  76. package/src/internal/specialists.ts +7 -7
  77. package/src/internal/turns.ts +28 -26
@@ -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.loop({
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.loop({
166
+ * turn.run({
167
167
  * instructions: `You help a small team.\n\n${Memory.render(facts)}`,
168
168
  * toolkit: Memory.Tools
169
169
  * })
@@ -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.loop({ instructions, toolkit: Toolkit.merge(MyTools, AssistantSkills) })
116
+ * turn.run({ instructions, toolkit: Toolkit.merge(MyTools, AssistantSkills) })
117
117
  * ).pipe(Layer.provide([MyToolsLive, AssistantSkills.layer]))
118
118
  * ```
119
119
  *
@@ -131,7 +131,7 @@ export interface Toolkit<R = never> extends AiToolkit.Toolkit<{
131
131
  * const skills = Skill.toolkit(allSkills.filter((skill) => allowed(role, skill)))
132
132
  *
133
133
  * return turn
134
- * .loop({ instructions, toolkit: Toolkit.merge(MyTools, skills) })
134
+ * .run({ instructions, toolkit: Toolkit.merge(MyTools, skills) })
135
135
  * .pipe(Stream.provide(skills.layer))
136
136
  * })
137
137
  * )
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.loop({ instructions, toolkit: Toolkit.merge(MyTools, AssistantSkills) })
101
+ * turn.run({ instructions, toolkit: Toolkit.merge(MyTools, AssistantSkills) })
102
102
  * ).pipe(Layer.provide([MyToolsLive, AssistantSkills.layer]))
103
103
  * ```
104
104
  *
@@ -116,7 +116,7 @@ function toolkitOf(skills) {
116
116
  * const skills = Skill.toolkit(allSkills.filter((skill) => allowed(role, skill)))
117
117
  *
118
118
  * return turn
119
- * .loop({ instructions, toolkit: Toolkit.merge(MyTools, skills) })
119
+ * .run({ instructions, toolkit: Toolkit.merge(MyTools, skills) })
120
120
  * .pipe(Stream.provide(skills.layer))
121
121
  * })
122
122
  * )
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.loop({ 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"}
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 * .run({ 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"}
@@ -15,7 +15,7 @@ import * as Toolkit from "effect/ai/Toolkit";
15
15
  import * as LanguageModel from "effect/ai/LanguageModel";
16
16
  import * as AiError from "effect/ai/AiError";
17
17
  declare namespace Specialist_d_exports {
18
- export { Any, Brief, Built, CheckServices, Handler, HandlerOf, LoopOptions, Run, RunServices, Runner, SpawnRequest, Specialist, SpecialistError, SpecialistHandler, ToolHandlers, make };
18
+ export { Any, Brief, Built, CheckServices, Handler, HandlerOf, RunOptions, Runner, SpawnRequest, Specialist, SpecialistError, SpecialistHandler, ToolHandlers, Turn, TurnServices, make };
19
19
  }
20
20
  declare const TypeId = "~effect-assistant/Specialist";
21
21
  /**
@@ -32,10 +32,10 @@ export interface Specialist<Name extends string = string> {
32
32
  /** The key of the specialist's handler, as `toLayer` provides it. */
33
33
  readonly Handler: Context.Key<SpecialistHandler<Name>, Built>;
34
34
  /**
35
- * Provides the specialist's handler. The handler may use the {@link RunServices} and the
35
+ * Provides the specialist's handler. The handler may use the {@link TurnServices} and the
36
36
  * services its build Effect required (see {@link CheckServices}).
37
37
  */
38
- readonly toLayer: <R = never, RE = never, E = never, RX = never>(build: (Handler<R, RE> | Effect.Effect<Handler<R, RE>, E, RX>) & CheckServices<Exclude<R | RE, RunServices | RX | Tool.Handler<string>>>) => Layer.Layer<SpecialistHandler<Name>, never, Exclude<RX, Scope.Scope | CurrentChat> | ToolHandlers<R | RE>>;
38
+ readonly toLayer: <R = never, RE = never, E = never, RX = never>(build: (Handler<R, RE> | Effect.Effect<Handler<R, RE>, E, RX>) & CheckServices<Exclude<R | RE, TurnServices | RX | Tool.Handler<string>>>) => Layer.Layer<SpecialistHandler<Name>, never, Exclude<RX, Scope.Scope | CurrentChat> | ToolHandlers<R | RE>>;
39
39
  }
40
40
  /**
41
41
  * Represents the check `toLayer` applies to the services a handler uses.
@@ -89,20 +89,20 @@ export interface SpecialistHandler<Name extends string> {
89
89
  */
90
90
  export type HandlerOf<S> = S extends Specialist<infer Name> ? SpecialistHandler<Name> : never;
91
91
  /**
92
- * The services a run provides to its handler, which are its chat, the chat's runner, and the
92
+ * The services a turn provides to its handler, which are its chat, the chat's runner, and the
93
93
  * model.
94
94
  *
95
95
  * @category utility types
96
96
  * @since 0.1.0
97
97
  */
98
- export type RunServices = CurrentChat | Runner | LanguageModel.LanguageModel;
98
+ export type TurnServices = CurrentChat | Runner | LanguageModel.LanguageModel;
99
99
  /**
100
- * Represents the options for `run.loop`.
100
+ * Represents the options for `turn.run`.
101
101
  *
102
102
  * @category models
103
103
  * @since 0.1.0
104
104
  */
105
- export interface LoopOptions<T extends Record<string, Tool.Any> = Record<never, never>> {
105
+ export interface RunOptions<T extends Record<string, Tool.Any> = Record<never, never>> {
106
106
  readonly instructions: string | Resolved;
107
107
  /**
108
108
  * The loop's tools, as a native toolkit whose handlers come from the layer. Merge in a
@@ -113,12 +113,13 @@ export interface LoopOptions<T extends Record<string, Tool.Any> = Record<never,
113
113
  readonly maxSteps?: number | undefined;
114
114
  }
115
115
  /**
116
- * Represents one run of a specialist on a brief, as its handler sees it.
116
+ * Represents one turn of a specialist's run, as its handler sees it. The first turn works on the
117
+ * brief, and each later one on the messages sent to the run since (follow-ups and steering).
117
118
  *
118
119
  * @category models
119
120
  * @since 0.1.0
120
121
  */
121
- export interface Run {
122
+ export interface Turn {
122
123
  readonly runId: RunId;
123
124
  /** The chat the run belongs to, which is the chat of the turn that spawned it. */
124
125
  readonly chatId: ChatId;
@@ -129,7 +130,7 @@ export interface Run {
129
130
  */
130
131
  readonly history: Effect.Effect<ReadonlyArray<Prompt.Message>>;
131
132
  /** Runs the agent loop over the run's history, with the given instructions and tools. */
132
- readonly loop: <T extends Record<string, Tool.Any> = Record<never, never>>(options: LoopOptions<T>) => Stream.Stream<Event, AiError.AiError, LanguageModel.LanguageModel | ToolkitServices<T>>;
133
+ readonly run: <T extends Record<string, Tool.Any> = Record<never, never>>(options: RunOptions<T>) => Stream.Stream<Event, AiError.AiError, LanguageModel.LanguageModel | ToolkitServices<T>>;
133
134
  }
134
135
  /**
135
136
  * Represents a specialist's handler, which works on a brief and returns a stream of what the loop
@@ -141,7 +142,7 @@ export interface Run {
141
142
  * @category models
142
143
  * @since 0.1.0
143
144
  */
144
- export type Handler<R = never, RE = R> = (run: Run) => Stream.Stream<Event, unknown, R> | Effect.Effect<Stream.Stream<Event, unknown, R>, unknown, RE>;
145
+ export type Handler<R = never, RE = R> = (turn: Turn) => Stream.Stream<Event, unknown, R> | Effect.Effect<Stream.Stream<Event, unknown, R>, unknown, RE>;
145
146
  /**
146
147
  * Represents a specialist's handler as its layer provides it.
147
148
  *
@@ -152,14 +153,14 @@ export type Handler<R = never, RE = R> = (run: Run) => Stream.Stream<Event, unkn
152
153
  * @since 0.1.0
153
154
  */
154
155
  export interface Built {
155
- readonly open: Effect.Effect<(run: Run) => Stream.Stream<Event, unknown, RunServices>, never, Scope.Scope | CurrentChat>;
156
+ readonly open: Effect.Effect<(turn: Turn) => Stream.Stream<Event, unknown, TurnServices>, never, Scope.Scope | CurrentChat>;
156
157
  }
157
158
  /**
158
159
  * Creates a specialist from its name and a description of when to use it.
159
160
  *
160
161
  * **Details**
161
162
  *
162
- * The specialist's `toLayer` provides its handler, which is usually `run.loop` with the
163
+ * The specialist's `toLayer` provides its handler, which is usually `turn.run` with the
163
164
  * specialist's own instructions and tools. List the specialist in `Assistant.make`'s
164
165
  * `specialists`, and provide its layer to the assistant's.
165
166
  *
@@ -173,8 +174,8 @@ export interface Built {
173
174
  * description: "Researches past campaigns and orders. Give it the question and any context."
174
175
  * })
175
176
  *
176
- * const ResearchLive = Research.toLayer((run) =>
177
- * run.loop({
177
+ * const ResearchLive = Research.toLayer((turn) =>
178
+ * turn.run({
178
179
  * instructions: "Answer with the findings and numbers the colleague needs, in a few lines.",
179
180
  * toolkit: ResearchTools
180
181
  * })
@@ -182,7 +183,7 @@ export interface Built {
182
183
  *
183
184
  * export const MyAssistant = Assistant.make("my-assistant", { specialists: [Research] })
184
185
  *
185
- * const MyAssistantLive = MyAssistant.toLayer((turn) => turn.loop({ instructions })).pipe(
186
+ * const MyAssistantLive = MyAssistant.toLayer((turn) => turn.run({ instructions })).pipe(
186
187
  * Layer.provide(ResearchLive)
187
188
  * )
188
189
  * ```
@@ -1 +1 @@
1
- {"version":3,"file":"Specialist.d.ts","names":[],"sources":["../src/Specialist.ts"],"mappings":";;;;;;;;;;;;;;;;;;;cA0BM;;;;;;;iBAQW,WAAW;YAChB,gBAAgB;WACjB,MAAM;;WAEN;;WAEA,SAAS,QAAQ,IAAI,kBAAkB,OAAO;;;;;WAK9C,UAAU,WAAW,YAAY,WAAW,YACnD,QAAQ,QAAQ,GAAG,MAAM,OAAO,OAAO,QAAQ,GAAG,KAAK,GAAG,OACxD,cAAc,QAAQ,IAAI,IAAI,cAAc,KAAK,KAAK,sBACrD,MAAM,MAAM,kBAAkB,cAAc,QAAQ,IAAI,MAAM,QAAQ,eAAe,aAAa,IAAI;;;;;;;;;;;;;;YAejG,cAAc,eAAe;WAG1B;aACE;aACA,UAAU;;;;;;;;;;;;YAaf,aAAa,KAAK,QAAQ,GAAG,KAAK;;;;;;;YAQlC,MAAM;;;;;;;iBAQD,kBAAkB;WACxB,oBAAoB;;;;;;;;YASnB,UAAU,KAAK,UAAU,iBAAiB,QAAQ,kBAAkB;;;;;;;;YASpE,cAAc,cAAc,SAAS,cAAc;;;;;;;iBAQ9C,YAAY,UAAU,eAAe,KAAK,OAAO;WACvD,uBAAuB;;;;;WAKvB,UAAU,QAAQ,QAAQ;;WAE1B;;;;;;;;iBASM;WACN,OAAO;;WAEP,QAAQ;WACR,OAAO;;;;;WAKP,SAAS,OAAO,OAAO,cAAc,OAAO;;WAE5C,OAAO,UAAU,eAAe,KAAK,OAAO,sBACnD,SAAS,YAAY,OAClB,OAAO,OACV,OACA,QAAQ,SACR,cAAc,gBAAgB,gBAA8B;;;;;;;;;;;;YAcpD,QAAQ,WAAW,KAAK,MAClC,KAAK,QAEH,OAAO,OAAO,gBAA8B,KAC5C,OAAO,OAAO,OAAO,OAAO,gBAA8B,aAAa;;;;;;;;;;iBAW1D;WACN,MAAM,OAAO,QACnB,KAAK,QAAQ,OAAO,OAAO,gBAA8B,qBAE1D,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBA8CL,aAAc,qBAAmB,MACtC,MAAI;;WAGC;MAEV,WAAW;;;;;;;;;qBA0CD,OAAK,OAAA;;;;;;;;;;;;;YAoBN,eAAe,MAAM;;;;;;;iBAQhB;;WAEN;WACA,OAAO;;;;;;;;;;;;;;;;;;;qBAeL,wBAAwB;;;WAkBjB,QAAC,SAAS,iBAAiB,OAAO,OAAO,OAAO;;WAE9C,UAAC,OAAO,OAAO,iBAAiB,OAAO,gCAAgC;;WAE1E,OAAC,OAAO,UAAU,OAAO,aAAa;;;;;;;;;;qBAR5C,eAAe"}
1
+ {"version":3,"file":"Specialist.d.ts","names":[],"sources":["../src/Specialist.ts"],"mappings":";;;;;;;;;;;;;;;;;;;cA0BM;;;;;;;iBAQW,WAAW;YAChB,gBAAgB;WACjB,MAAM;;WAEN;;WAEA,SAAS,QAAQ,IAAI,kBAAkB,OAAO;;;;;WAK9C,UAAU,WAAW,YAAY,WAAW,YACnD,QAAQ,QAAQ,GAAG,MAAM,OAAO,OAAO,QAAQ,GAAG,KAAK,GAAG,OACxD,cAAc,QAAQ,IAAI,IAAI,eAAe,KAAK,KAAK,sBACtD,MAAM,MAAM,kBAAkB,cAAc,QAAQ,IAAI,MAAM,QAAQ,eAAe,aAAa,IAAI;;;;;;;;;;;;;;YAejG,cAAc,eAAe;WAG1B;aACE;aACA,UAAU;;;;;;;;;;;;YAaf,aAAa,KAAK,QAAQ,GAAG,KAAK;;;;;;;YAQlC,MAAM;;;;;;;iBAQD,kBAAkB;WACxB,oBAAoB;;;;;;;;YASnB,UAAU,KAAK,UAAU,iBAAiB,QAAQ,kBAAkB;;;;;;;;YASpE,eAAe,cAAc,SAAS,cAAc;;;;;;;iBAQ/C,WAAW,UAAU,eAAe,KAAK,OAAO;WACtD,uBAAuB;;;;;WAKvB,UAAU,QAAQ,QAAQ;;WAE1B;;;;;;;;;iBAUM;WACN,OAAO;;WAEP,QAAQ;WACR,OAAO;;;;;WAKP,SAAS,OAAO,OAAO,cAAc,OAAO;;WAE5C,MAAM,UAAU,eAAe,KAAK,OAAO,sBAClD,SAAS,WAAW,OACjB,OAAO,OACV,OACA,QAAQ,SACR,cAAc,gBAAgB,gBAA8B;;;;;;;;;;;;YAcpD,QAAQ,WAAW,KAAK,MAClC,MAAM,SAEJ,OAAO,OAAO,gBAA8B,KAC5C,OAAO,OAAO,OAAO,OAAO,gBAA8B,aAAa;;;;;;;;;;iBAW1D;WACN,MAAM,OAAO,QACnB,MAAM,SAAS,OAAO,OAAO,gBAA8B,sBAE5D,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBA8CL,aAAc,qBAAmB,MACtC,MAAI;;WAGC;MAEV,WAAW;;;;;;;;;qBA0CD,OAAK,OAAA;;;;;;;;;;;;;YAoBN,eAAe,MAAM;;;;;;;iBAQhB;;WAEN;WACA,OAAO;;;;;;;;;;;;;;;;;;;qBAeL,wBAAwB;;;WAkBjB,QAAC,SAAS,iBAAiB,OAAO,OAAO,OAAO;;WAE9C,UAAC,OAAO,OAAO,iBAAiB,OAAO,gCAAgC;;WAE1E,OAAC,OAAO,UAAU,OAAO,aAAa;;;;;;;;;;qBAR5C,eAAe"}
@@ -18,7 +18,7 @@ const toStream = (result) => Effect.isEffect(result) ? Stream.unwrap(result) : r
18
18
  *
19
19
  * **Details**
20
20
  *
21
- * The specialist's `toLayer` provides its handler, which is usually `run.loop` with the
21
+ * The specialist's `toLayer` provides its handler, which is usually `turn.run` with the
22
22
  * specialist's own instructions and tools. List the specialist in `Assistant.make`'s
23
23
  * `specialists`, and provide its layer to the assistant's.
24
24
  *
@@ -32,8 +32,8 @@ const toStream = (result) => Effect.isEffect(result) ? Stream.unwrap(result) : r
32
32
  * description: "Researches past campaigns and orders. Give it the question and any context."
33
33
  * })
34
34
  *
35
- * const ResearchLive = Research.toLayer((run) =>
36
- * run.loop({
35
+ * const ResearchLive = Research.toLayer((turn) =>
36
+ * turn.run({
37
37
  * instructions: "Answer with the findings and numbers the colleague needs, in a few lines.",
38
38
  * toolkit: ResearchTools
39
39
  * })
@@ -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.loop({ instructions })).pipe(
44
+ * const MyAssistantLive = MyAssistant.toLayer((turn) => turn.run({ instructions })).pipe(
45
45
  * Layer.provide(ResearchLive)
46
46
  * )
47
47
  * ```
@@ -56,7 +56,7 @@ const make = (name, options) => {
56
56
  const context = yield* Effect.context();
57
57
  return { open: Effect.gen(function* () {
58
58
  const handler = Effect.isEffect(build) ? yield* build : build;
59
- return (run) => Stream.provideContext(toStream(handler(run)), context);
59
+ return (turn) => Stream.provideContext(toStream(handler(turn)), context);
60
60
  }).pipe(Effect.provideContext(context), Effect.orDie) };
61
61
  }));
62
62
  }
@@ -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 TurnServices} 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, TurnServices | 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 turn 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 TurnServices = CurrentChat | Runner | LanguageModel.LanguageModel\n\n/**\n * Represents the options for `turn.run`.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface RunOptions<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 turn of a specialist's run, as its handler sees it. The first turn works on the\n * brief, and each later one on the messages sent to the run since (follow-ups and steering).\n *\n * @category models\n * @since 0.1.0\n */\nexport interface Turn {\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 run: <T extends Record<string, Tool.Any> = Record<never, never>>(\n options: RunOptions<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 turn: Turn\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 (turn: Turn) => Stream.Stream<AssistantLoop.Event, unknown, TurnServices>,\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 `turn.run` 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((turn) =>\n * turn.run({\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, TurnServices | 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 (turn: Turn) => Stream.provideContext(toStream(handler(turn)), 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;AAyKf,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,SAAe,OAAO,eAAe,SAAS,QAAQ,IAAI,CAAC,GAAG,OAAO;GAC/E,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"}
@@ -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. `seq` is the position in the chat's log that the timeline is up to.
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 seq: number;
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. `seq` is the position in the chat's log that the change is up
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 seq: number;
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 `seq` up to which the client already has the timeline. The result contains only 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 `seq`, so a
475
- * client that has the timeline up to a `seq` receives only what changed since. Pass `after` here,
476
- * not to `subscribe`: a subscription that starts after a `seq` gives the fold an incomplete log,
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, throughSeq: Schema.Int }) {}
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] })