effect-assistant 0.1.0 → 0.3.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 (131) hide show
  1. package/README.md +1 -1
  2. package/dist/Assistant.d.ts +74 -28
  3. package/dist/Assistant.d.ts.map +1 -1
  4. package/dist/Assistant.js +11 -4
  5. package/dist/Assistant.js.map +1 -1
  6. package/dist/AssistantActivity.d.ts +10 -16
  7. package/dist/AssistantActivity.d.ts.map +1 -1
  8. package/dist/AssistantActivity.js +11 -4
  9. package/dist/AssistantActivity.js.map +1 -1
  10. package/dist/AssistantEngine.d.ts +39 -1
  11. package/dist/AssistantEngine.d.ts.map +1 -1
  12. package/dist/AssistantEngine.js +81 -28
  13. package/dist/AssistantEngine.js.map +1 -1
  14. package/dist/AssistantEvent.d.ts +52 -70
  15. package/dist/AssistantEvent.d.ts.map +1 -1
  16. package/dist/AssistantEvent.js +28 -57
  17. package/dist/AssistantEvent.js.map +1 -1
  18. package/dist/AssistantLoop.d.ts +7 -5
  19. package/dist/AssistantLoop.d.ts.map +1 -1
  20. package/dist/AssistantLoop.js +6 -3
  21. package/dist/AssistantLoop.js.map +1 -1
  22. package/dist/Block.js.map +1 -1
  23. package/dist/BlockKit.d.ts +14 -18
  24. package/dist/BlockKit.d.ts.map +1 -1
  25. package/dist/BlockKit.js +4 -7
  26. package/dist/BlockKit.js.map +1 -1
  27. package/dist/ChatLog.js +1 -1
  28. package/dist/Compaction.d.ts +8 -4
  29. package/dist/Compaction.d.ts.map +1 -1
  30. package/dist/Compaction.js +12 -15
  31. package/dist/Compaction.js.map +1 -1
  32. package/dist/CurrentChat.d.ts +2 -1
  33. package/dist/CurrentChat.d.ts.map +1 -1
  34. package/dist/CurrentChat.js.map +1 -1
  35. package/dist/Instructions.js +1 -1
  36. package/dist/Skill.d.ts +33 -0
  37. package/dist/Skill.d.ts.map +1 -1
  38. package/dist/Skill.js +27 -1
  39. package/dist/Skill.js.map +1 -1
  40. package/dist/Specialist.d.ts +4 -1
  41. package/dist/Specialist.d.ts.map +1 -1
  42. package/dist/Specialist.js.map +1 -1
  43. package/dist/Timeline.d.ts +482 -0
  44. package/dist/Timeline.d.ts.map +1 -0
  45. package/dist/Timeline.js +572 -0
  46. package/dist/Timeline.js.map +1 -0
  47. package/dist/ToolDiscovery.d.ts +4 -5
  48. package/dist/ToolDiscovery.d.ts.map +1 -1
  49. package/dist/ToolDiscovery.js +5 -4
  50. package/dist/ToolDiscovery.js.map +1 -1
  51. package/dist/ToolFailure.d.ts +1 -1
  52. package/dist/ToolFailure.js +1 -1
  53. package/dist/ToolFailure.js.map +1 -1
  54. package/dist/index.d.ts +2 -5
  55. package/dist/index.js +4 -7
  56. package/dist/internal/chat.js +11 -37
  57. package/dist/internal/chat.js.map +1 -1
  58. package/dist/internal/events.d.ts +15 -16
  59. package/dist/internal/events.d.ts.map +1 -1
  60. package/dist/internal/events.js +29 -28
  61. package/dist/internal/events.js.map +1 -1
  62. package/dist/internal/history.js +26 -58
  63. package/dist/internal/history.js.map +1 -1
  64. package/dist/internal/input.js +7 -0
  65. package/dist/internal/input.js.map +1 -0
  66. package/dist/internal/loop.d.ts +2 -2
  67. package/dist/internal/loop.d.ts.map +1 -1
  68. package/dist/internal/loop.js +21 -56
  69. package/dist/internal/loop.js.map +1 -1
  70. package/dist/internal/skills.js +6 -1
  71. package/dist/internal/skills.js.map +1 -1
  72. package/dist/internal/specialists.js +7 -7
  73. package/dist/internal/specialists.js.map +1 -1
  74. package/dist/internal/toolkit.d.ts +6 -20
  75. package/dist/internal/toolkit.d.ts.map +1 -1
  76. package/dist/internal/toolkit.js +10 -31
  77. package/dist/internal/toolkit.js.map +1 -1
  78. package/dist/internal/tools.js +2 -21
  79. package/dist/internal/tools.js.map +1 -1
  80. package/dist/internal/turns.d.ts.map +1 -1
  81. package/dist/internal/turns.js +24 -43
  82. package/dist/internal/turns.js.map +1 -1
  83. package/package.json +3 -2
  84. package/src/Assistant.ts +75 -29
  85. package/src/AssistantActivity.ts +11 -18
  86. package/src/AssistantEngine.ts +110 -43
  87. package/src/AssistantEvent.ts +46 -74
  88. package/src/AssistantLoop.ts +7 -5
  89. package/src/Block.ts +1 -1
  90. package/src/BlockKit.ts +18 -31
  91. package/src/Compaction.ts +8 -21
  92. package/src/CurrentChat.ts +2 -1
  93. package/src/Skill.ts +34 -1
  94. package/src/Specialist.ts +4 -1
  95. package/src/Timeline.ts +861 -0
  96. package/src/ToolDiscovery.ts +5 -13
  97. package/src/ToolFailure.ts +1 -1
  98. package/src/index.ts +5 -20
  99. package/src/internal/chat.ts +8 -57
  100. package/src/internal/events.ts +43 -43
  101. package/src/internal/history.ts +36 -101
  102. package/src/internal/input.ts +3 -0
  103. package/src/internal/loop.ts +31 -101
  104. package/src/internal/skills.ts +11 -0
  105. package/src/internal/specialists.ts +6 -9
  106. package/src/internal/toolkit.ts +17 -60
  107. package/src/internal/tools.ts +2 -53
  108. package/src/internal/turns.ts +37 -79
  109. package/dist/AppEvent.d.ts +0 -111
  110. package/dist/AppEvent.d.ts.map +0 -1
  111. package/dist/AppEvent.js +0 -71
  112. package/dist/AppEvent.js.map +0 -1
  113. package/dist/ChatView.d.ts +0 -459
  114. package/dist/ChatView.d.ts.map +0 -1
  115. package/dist/ChatView.js +0 -496
  116. package/dist/ChatView.js.map +0 -1
  117. package/dist/Decision.d.ts +0 -145
  118. package/dist/Decision.d.ts.map +0 -1
  119. package/dist/Decision.js +0 -178
  120. package/dist/Decision.js.map +0 -1
  121. package/dist/DecisionId.d.ts +0 -37
  122. package/dist/DecisionId.d.ts.map +0 -1
  123. package/dist/DecisionId.js +0 -44
  124. package/dist/DecisionId.js.map +0 -1
  125. package/dist/internal/decisions.js +0 -66
  126. package/dist/internal/decisions.js.map +0 -1
  127. package/src/AppEvent.ts +0 -134
  128. package/src/ChatView.ts +0 -658
  129. package/src/Decision.ts +0 -241
  130. package/src/DecisionId.ts +0 -46
  131. package/src/internal/decisions.ts +0 -131
@@ -1 +1 @@
1
- {"version":3,"file":"Compaction.js","names":["AssistantEvent.isLibraryEvent","Decision.fromEntries","BlockKit.fromEntries","AssistantActivity.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 * as Decision from \"./Decision.ts\"\nimport { userText } from \"./internal/history.ts\"\nimport { toJsonText } from \"./internal/schema.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: decisions waiting for the\n * user, blocks the user hasn't answered, and specialists that are working. 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 decisions = [...Decision.fromEntries(entries).values()].flatMap((state) =>\n state.outcome === undefined &&\n opened(\n seqOf(\n (event) =>\n AssistantEvent.isLibraryEvent(event) &&\n Predicate.isTagged(event, \"DecisionRequested\") &&\n event.id === state.request.id\n )\n )\n ? [`- decision ${state.request.id}, for ${state.request.origin.tool}: ${toJsonText(state.request.origin.params)}`]\n : []\n )\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 sections = [\n decisions.length > 0 ? `Decisions waiting for the user:\\n${decisions.join(\"\\n\")}` : \"\",\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 ].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 * **Example** (Compacting a long chat after each turn)\n *\n * ```ts\n * import { AppEvent, 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: [AppEvent.make(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","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\"\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, and specialists that are working. 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 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 ].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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"}
@@ -16,7 +16,8 @@ declare const CurrentChat_base: Context.ServiceClass<CurrentChat, "effect-assist
16
16
  /**
17
17
  * Appends events after the chat's last entry and sends them to the chat's subscribers. The
18
18
  * events can be the library's or the assistant's own (registered with its `events` option),
19
- * such as a summary. An event that wakes a turn is handled by the next turn.
19
+ * such as a summary. An app event appended here wakes no turn: only one that comes in through
20
+ * the assistant's `submit` does.
20
21
  */
21
22
  readonly append: (...events: ReadonlyArray<{
22
23
  readonly _tag: string;
@@ -1 +1 @@
1
- {"version":3,"file":"CurrentChat.d.ts","names":[],"sources":["../src/CurrentChat.ts"],"mappings":";;;;;;;;WAgCqB,QAAA;;;;;;WAMC,SAAA,OAAO,OAAO,cAAc;;;;;;WAM7B,YAAI,QAAQ;aAAyB;SAAoB,OAAO;;;;;;;;;;;;;;qBAfxE,oBAAoB"}
1
+ {"version":3,"file":"CurrentChat.d.ts","names":[],"sources":["../src/CurrentChat.ts"],"mappings":";;;;;;;;WAgCqB,QAAA;;;;;;WAMC,SAAA,OAAO,OAAO,cAAc;;;;;;;WAO7B,YAAI,QAAQ;aAAyB;SAAoB,OAAO;;;;;;;;;;;;;;qBAhBxE,oBAAoB"}
@@ -1 +1 @@
1
- {"version":3,"file":"CurrentChat.js","names":[],"sources":["../src/CurrentChat.ts"],"sourcesContent":["/**\n * This module provides the `CurrentChat` service, which gives code that runs inside a chat\n * access to that chat: its id, its entries, and a way to append to it.\n *\n * A turn's handler, its tools, and its specialists' runs get the chat with `yield* CurrentChat`.\n * Inside a specialist's run, it is the chat the run belongs to, which is the chat of the turn\n * that spawned the run. The service doesn't carry an assistant's event types, so code that\n * doesn't know the assistant, such as a shared tool, can use it. Code that knows the assistant\n * can use `MyAssistant.Chat`, which is the same chat typed with the assistant's events.\n *\n * @since 0.1.0\n */\nimport * as Context from \"effect/Context\"\nimport type * as Effect from \"effect/Effect\"\nimport type * as AssistantEvent from \"./AssistantEvent.ts\"\nimport type { ChatId } from \"./ChatId.ts\"\n\n/**\n * Service that represents the chat the current code runs in.\n *\n * **Details**\n *\n * In a turn, it is the chat the turn runs in. In a specialist's run, and in its handler's build,\n * it is the chat the run belongs to: the chat of the turn that spawned the run. A run's tools\n * read and append to that chat.\n *\n * @category services\n * @since 0.1.0\n */\nexport class CurrentChat extends Context.Service<\n CurrentChat,\n {\n readonly chatId: ChatId\n /**\n * The chat's entries so far, in order. They include the library's events and the assistant's\n * own events, decoded; check for these with `Schema.is(MyEvent)` or `instanceof`. An app event\n * that no longer decodes is left out.\n */\n readonly entries: Effect.Effect<ReadonlyArray<AssistantEvent.AnyEntry>>\n /**\n * Appends events after the chat's last entry and sends them to the chat's subscribers. The\n * events can be the library's or the assistant's own (registered with its `events` option),\n * such as a summary. An event that wakes a turn is handled by the next turn.\n */\n readonly append: (...events: ReadonlyArray<{ readonly _tag: string }>) => Effect.Effect<void>\n }\n>()(\"effect-assistant/CurrentChat\") {}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BA,IAAa,cAAb,cAAiC,QAAQ,QAiBvC,CAAC,CAAC,8BAA8B,CAAC,CAAC,CAAC"}
1
+ {"version":3,"file":"CurrentChat.js","names":[],"sources":["../src/CurrentChat.ts"],"sourcesContent":["/**\n * This module provides the `CurrentChat` service, which gives code that runs inside a chat\n * access to that chat: its id, its entries, and a way to append to it.\n *\n * A turn's handler, its tools, and its specialists' runs get the chat with `yield* CurrentChat`.\n * Inside a specialist's run, it is the chat the run belongs to, which is the chat of the turn\n * that spawned the run. The service doesn't carry an assistant's event types, so code that\n * doesn't know the assistant, such as a shared tool, can use it. Code that knows the assistant\n * can use `MyAssistant.Chat`, which is the same chat typed with the assistant's events.\n *\n * @since 0.1.0\n */\nimport * as Context from \"effect/Context\"\nimport type * as Effect from \"effect/Effect\"\nimport type * as AssistantEvent from \"./AssistantEvent.ts\"\nimport type { ChatId } from \"./ChatId.ts\"\n\n/**\n * Service that represents the chat the current code runs in.\n *\n * **Details**\n *\n * In a turn, it is the chat the turn runs in. In a specialist's run, and in its handler's build,\n * it is the chat the run belongs to: the chat of the turn that spawned the run. A run's tools\n * read and append to that chat.\n *\n * @category services\n * @since 0.1.0\n */\nexport class CurrentChat extends Context.Service<\n CurrentChat,\n {\n readonly chatId: ChatId\n /**\n * The chat's entries so far, in order. They include the library's events and the assistant's\n * own events, decoded; check for these with `Schema.is(MyEvent)` or `instanceof`. An app event\n * that no longer decodes is left out.\n */\n readonly entries: Effect.Effect<ReadonlyArray<AssistantEvent.AnyEntry>>\n /**\n * Appends events after the chat's last entry and sends them to the chat's subscribers. The\n * events can be the library's or the assistant's own (registered with its `events` option),\n * such as a summary. An app event appended here wakes no turn: only one that comes in through\n * the assistant's `submit` does.\n */\n readonly append: (...events: ReadonlyArray<{ readonly _tag: string }>) => Effect.Effect<void>\n }\n>()(\"effect-assistant/CurrentChat\") {}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BA,IAAa,cAAb,cAAiC,QAAQ,QAkBvC,CAAC,CAAC,8BAA8B,CAAC,CAAC,CAAC"}
@@ -1,6 +1,6 @@
1
1
  import { __exportAll } from "./_virtual/_rolldown/runtime.js";
2
- import * as Predicate from "effect/Predicate";
3
2
  import * as Effect from "effect/Effect";
3
+ import * as Predicate from "effect/Predicate";
4
4
  //#region src/Instructions.ts
5
5
  /**
6
6
  * Defines the instructions that tell an assistant, specialist, or skill what to do.
package/dist/Skill.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Instructions, Instructions_d_exports } from "./Instructions.js";
2
+ import * as Tool from "effect/ai/Tool";
2
3
  declare namespace Skill_d_exports {
3
4
  export { Services, Skill, make, withCatalog };
4
5
  }
@@ -16,6 +17,11 @@ export interface Skill<R = never> {
16
17
  readonly description: string;
17
18
  /** Text, or an Effect resolved each time the skill is loaded. */
18
19
  readonly instructions: Instructions<R>;
20
+ /**
21
+ * The tools its instructions use. A loop offers the skill only when its toolkit has every one of
22
+ * them, matched by name. Unset, a loop always offers it.
23
+ */
24
+ readonly tools?: ReadonlyArray<Tool.Any> | undefined;
19
25
  }
20
26
  /**
21
27
  * Creates a skill from its name, a description of when to use it, and its instructions.
@@ -26,6 +32,11 @@ export interface Skill<R = never> {
26
32
  * the skill. The instructions are text, or an Effect that is resolved each time the skill is
27
33
  * loaded.
28
34
  *
35
+ * `tools` lists the tools the instructions use. A loop whose toolkit lacks any of them, matched by
36
+ * name, leaves the skill out of its catalog and of `load_skill`, so a loop that gets its tools per
37
+ * user (by role, say) never offers a playbook it can't follow. A tool that only `ToolDiscovery`
38
+ * finds isn't in the toolkit, so it doesn't count. Without `tools`, every loop offers the skill.
39
+ *
29
40
  * **Example** (Defining a playbook for ordering campaigns)
30
41
  *
31
42
  * ```ts
@@ -43,6 +54,26 @@ export interface Skill<R = never> {
43
54
  * )
44
55
  * ```
45
56
  *
57
+ * **Example** (Offering a skill only with the tools it uses)
58
+ *
59
+ * ```ts
60
+ * import { Skill } from "effect-assistant"
61
+ * import * as Tool from "effect/ai/Tool"
62
+ * import * as Schema from "effect/Schema"
63
+ *
64
+ * const StartCampaign = Tool.make("start_campaign", {
65
+ * parameters: Schema.Struct({ campaignId: Schema.String, budget: Schema.Finite }),
66
+ * success: Schema.String
67
+ * })
68
+ *
69
+ * // Left out of a loop whose toolkit has no start_campaign, like a viewer's.
70
+ * const orderingCampaigns = Skill.make("Ordering a campaign", {
71
+ * description: "Use when the user wants to order or start a campaign.",
72
+ * instructions: "When you know who it should reach and the budget, call start_campaign.",
73
+ * tools: [StartCampaign]
74
+ * })
75
+ * ```
76
+ *
46
77
  * @category constructors
47
78
  * @since 0.1.0
48
79
  */
@@ -51,6 +82,8 @@ export declare const make: <R = never>(name: string, options: {
51
82
  readonly description: string;
52
83
  /** Text, or an Effect resolved each time the skill is loaded. */
53
84
  readonly instructions: Instructions<R>;
85
+ /** The tools its instructions use: a loop whose toolkit lacks one leaves the skill out. */
86
+ readonly tools?: ReadonlyArray<Tool.Any> | undefined;
54
87
  }) => Skill<R>;
55
88
  /**
56
89
  * Extracts the services that skills need when they're loaded.
@@ -1 +1 @@
1
- {"version":3,"file":"Skill.d.ts","names":[],"sources":["../src/Skill.ts"],"mappings":";;;;;;;;;;;;iBAmBiB,MAAM;WACZ;;WAEA;;WAEA,cAAc,aAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAgCtC,OAAQ,WAAS,cAChB;;WAGD;;WAEA,cAAc,aAA0B;MAElD,MAAM;;;;;;;YAQG,SAAS,KAAK,UAAU,YAAY,KAAK;;;;;;;;qBASxC,cAAe,GAAC,gBAAgB,QAAU,cAAc,MAAM"}
1
+ {"version":3,"file":"Skill.d.ts","names":[],"sources":["../src/Skill.ts"],"mappings":";;;;;;;;;;;;;iBAoBiB,MAAM;WACZ;;WAEA;;WAEA,cAAc,aAA0B;;;;;WAKxC,QAAQ,cAAc,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAyDzB,OAAQ,WAAS,cAChB;;WAGD;;WAEA,cAAc,aAA0B;;WAExC,QAAQ,cAAc,KAAK;MAErC,MAAM;;;;;;;YAQG,SAAS,KAAK,UAAU,YAAY,KAAK;;;;;;;;qBASxC,cAAe,GAAC,gBAAgB,QAAU,cAAc,MAAM"}
package/dist/Skill.js CHANGED
@@ -13,6 +13,11 @@ var Skill_exports = /* @__PURE__ */ __exportAll({
13
13
  * the skill. The instructions are text, or an Effect that is resolved each time the skill is
14
14
  * loaded.
15
15
  *
16
+ * `tools` lists the tools the instructions use. A loop whose toolkit lacks any of them, matched by
17
+ * name, leaves the skill out of its catalog and of `load_skill`, so a loop that gets its tools per
18
+ * user (by role, say) never offers a playbook it can't follow. A tool that only `ToolDiscovery`
19
+ * finds isn't in the toolkit, so it doesn't count. Without `tools`, every loop offers the skill.
20
+ *
16
21
  * **Example** (Defining a playbook for ordering campaigns)
17
22
  *
18
23
  * ```ts
@@ -30,13 +35,34 @@ var Skill_exports = /* @__PURE__ */ __exportAll({
30
35
  * )
31
36
  * ```
32
37
  *
38
+ * **Example** (Offering a skill only with the tools it uses)
39
+ *
40
+ * ```ts
41
+ * import { Skill } from "effect-assistant"
42
+ * import * as Tool from "effect/ai/Tool"
43
+ * import * as Schema from "effect/Schema"
44
+ *
45
+ * const StartCampaign = Tool.make("start_campaign", {
46
+ * parameters: Schema.Struct({ campaignId: Schema.String, budget: Schema.Finite }),
47
+ * success: Schema.String
48
+ * })
49
+ *
50
+ * // Left out of a loop whose toolkit has no start_campaign, like a viewer's.
51
+ * const orderingCampaigns = Skill.make("Ordering a campaign", {
52
+ * description: "Use when the user wants to order or start a campaign.",
53
+ * instructions: "When you know who it should reach and the budget, call start_campaign.",
54
+ * tools: [StartCampaign]
55
+ * })
56
+ * ```
57
+ *
33
58
  * @category constructors
34
59
  * @since 0.1.0
35
60
  */
36
61
  const make = (name, options) => ({
37
62
  name,
38
63
  description: options.description,
39
- instructions: options.instructions
64
+ instructions: options.instructions,
65
+ tools: options.tools
40
66
  });
41
67
  /**
42
68
  * Returns the prompt with the skills' catalog appended, or the prompt unchanged when there are no
package/dist/Skill.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"Skill.js","names":[],"sources":["../src/Skill.ts"],"sourcesContent":["/**\n * Defines skills, which are playbooks with step-by-step instructions for a kind of task.\n *\n * A loop lists its skills as a catalog in the prompt, with each skill's name and description\n * ({@link withCatalog}). The model loads a skill's instructions with the `load_skill` tool when a\n * task matches.\n *\n * @since 0.1.0\n */\nimport type * as Instructions from \"./Instructions.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.\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 * const MyAssistantLive = MyAssistant.toLayer((turn) =>\n * turn.loop({ instructions, skills: [orderingCampaigns] })\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\n/**\n * Returns the prompt with the skills' catalog appended, or the prompt unchanged when there are no\n * skills.\n *\n * @category utils\n * @since 0.1.0\n */\nexport const withCatalog = <R>(prompt: string, skills: ReadonlyArray<Skill<R>>): string =>\n skills.length === 0\n ? prompt\n : [\n prompt,\n \"\",\n \"## Skills\",\n \"Playbooks for common tasks. When one matches what you're doing, load it with load_skill in the same step as your first lookups, and follow it.\",\n ...skills.map((skill) => `- ${skill.name}: ${skill.description}`)\n ].join(\"\\n\")\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwDA,MAAa,QACX,MACA,aAMc;CAAE;CAAM,aAAa,QAAQ;CAAa,cAAc,QAAQ;AAAa;;;;;;;;AAiB7F,MAAa,eAAkB,QAAgB,WAC7C,OAAO,WAAW,IACd,SACA;CACE;CACA;CACA;CACA;CACA,GAAG,OAAO,KAAK,UAAU,KAAK,MAAM,KAAK,IAAI,MAAM,aAAa;AAClE,CAAC,CAAC,KAAK,IAAI"}
1
+ {"version":3,"file":"Skill.js","names":[],"sources":["../src/Skill.ts"],"sourcesContent":["/**\n * Defines skills, which are playbooks with step-by-step instructions for a kind of task.\n *\n * A loop lists its skills as a catalog in the prompt, with each skill's name and description\n * ({@link withCatalog}). The model loads a skill's instructions with the `load_skill` tool when a\n * task matches.\n *\n * @since 0.1.0\n */\nimport type * as Tool from \"effect/ai/Tool\"\nimport type * as Instructions from \"./Instructions.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 * The tools its instructions use. A loop offers the skill only when its toolkit has every one of\n * them, matched by name. Unset, a loop always offers it.\n */\n readonly tools?: ReadonlyArray<Tool.Any> | undefined\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.\n *\n * `tools` lists the tools the instructions use. A loop whose toolkit lacks any of them, matched by\n * name, leaves the skill out of its catalog and of `load_skill`, so a loop that gets its tools per\n * user (by role, say) never offers a playbook it can't follow. A tool that only `ToolDiscovery`\n * finds isn't in the toolkit, so it doesn't count. Without `tools`, every loop offers the skill.\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 * const MyAssistantLive = MyAssistant.toLayer((turn) =>\n * turn.loop({ instructions, skills: [orderingCampaigns] })\n * )\n * ```\n *\n * **Example** (Offering a skill only with the tools it uses)\n *\n * ```ts\n * import { Skill } from \"effect-assistant\"\n * import * as Tool from \"effect/ai/Tool\"\n * import * as Schema from \"effect/Schema\"\n *\n * const StartCampaign = Tool.make(\"start_campaign\", {\n * parameters: Schema.Struct({ campaignId: Schema.String, budget: Schema.Finite }),\n * success: Schema.String\n * })\n *\n * // Left out of a loop whose toolkit has no start_campaign, like a viewer's.\n * const orderingCampaigns = Skill.make(\"Ordering a campaign\", {\n * description: \"Use when the user wants to order or start a campaign.\",\n * instructions: \"When you know who it should reach and the budget, call start_campaign.\",\n * tools: [StartCampaign]\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 /** The tools its instructions use: a loop whose toolkit lacks one leaves the skill out. */\n readonly tools?: ReadonlyArray<Tool.Any> | undefined\n }\n): Skill<R> => ({ name, description: options.description, instructions: options.instructions, tools: options.tools })\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\n/**\n * Returns the prompt with the skills' catalog appended, or the prompt unchanged when there are no\n * skills.\n *\n * @category utils\n * @since 0.1.0\n */\nexport const withCatalog = <R>(prompt: string, skills: ReadonlyArray<Skill<R>>): string =>\n skills.length === 0\n ? prompt\n : [\n prompt,\n \"\",\n \"## Skills\",\n \"Playbooks for common tasks. When one matches what you're doing, load it with load_skill in the same step as your first lookups, and follow it.\",\n ...skills.map((skill) => `- ${skill.name}: ${skill.description}`)\n ].join(\"\\n\")\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuFA,MAAa,QACX,MACA,aAQc;CAAE;CAAM,aAAa,QAAQ;CAAa,cAAc,QAAQ;CAAc,OAAO,QAAQ;AAAM;;;;;;;;AAiBnH,MAAa,eAAkB,QAAgB,WAC7C,OAAO,WAAW,IACd,SACA;CACE;CACA;CACA;CACA;CACA,GAAG,OAAO,KAAK,UAAU,KAAK,MAAM,KAAK,IAAI,MAAM,aAAa;AAClE,CAAC,CAAC,KAAK,IAAI"}
@@ -107,7 +107,10 @@ export interface LoopOptions<T extends Record<string, Tool.Any> = Record<never,
107
107
  readonly instructions: string | Resolved;
108
108
  /** The loop's tools, as a native toolkit whose handlers come from the layer. */
109
109
  readonly toolkit?: Toolkit.Toolkit<T> | undefined;
110
- /** Playbooks, listed as a catalog in the prompt and loaded with `load_skill` when one matches. */
110
+ /**
111
+ * Playbooks, listed as a catalog in the prompt and loaded with `load_skill` when one matches. A
112
+ * skill made with `tools` is left out when the toolkit lacks one of them.
113
+ */
111
114
  readonly skills?: ReadonlyArray<Skill<RS>> | undefined;
112
115
  /** The model calls allowed per brief. Defaults to 8. */
113
116
  readonly maxSteps?: number | undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"Specialist.d.ts","names":[],"sources":["../src/Specialist.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;cA2BM;;;;;;;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,sBAAsB;WAC7E,uBAAuB;;WAEvB,UAAU,QAAQ,QAAQ;;WAE1B,SAAS,cAAc,MAAM;;WAE7B;;;;;;;;iBASM;WACN,OAAO;;WAEP,QAAQ;WACR,OAAO;;;;;WAKP,SAAS,OAAO,OAAO,cAAc,OAAO;;WAE5C,OAAO,UAAU,eAAe,KAAK,OAAO,sBAAsB,YACzE,SAAS,YAAY,GAAG,QACrB,OAAO,OACV,OACA,QAAQ,SACR,cAAc,gBAAgB,gBAA8B,KAAK;;;;;;;;;;;;YAczD,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":";;;;;;;;;;;;;;;;;;;;cA2BM;;;;;;;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,sBAAsB;WAC7E,uBAAuB;;WAEvB,UAAU,QAAQ,QAAQ;;;;;WAK1B,SAAS,cAAc,MAAM;;WAE7B;;;;;;;;iBASM;WACN,OAAO;;WAEP,QAAQ;WACR,OAAO;;;;;WAKP,SAAS,OAAO,OAAO,cAAc,OAAO;;WAE5C,OAAO,UAAU,eAAe,KAAK,OAAO,sBAAsB,YACzE,SAAS,YAAY,GAAG,QACrB,OAAO,OACV,OACA,QAAQ,SACR,cAAc,gBAAgB,gBAA8B,KAAK;;;;;;;;;;;;YAczD,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 +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\"\nimport type { Skill } from \"./Skill.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>, RS = never> {\n readonly instructions: string | Instructions.Resolved\n /** The loop's tools, as a native toolkit whose handlers come from the layer. */\n readonly toolkit?: Toolkit.Toolkit<T> | undefined\n /** Playbooks, listed as a catalog in the prompt and loaded with `load_skill` when one matches. */\n readonly skills?: ReadonlyArray<Skill<RS>> | 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, tools, and skills. */\n readonly loop: <T extends Record<string, Tool.Any> = Record<never, never>, RS = never>(\n options: LoopOptions<T, RS>\n ) => Stream.Stream<\n AssistantLoop.Event,\n AiError.AiError,\n LanguageModel.LanguageModel | AssistantLoop.ToolkitServices<T> | RS\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":";;;;;;;;;;;;;AA2BA,MAAM,SAAS;AAuKf,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\"\nimport type { Skill } from \"./Skill.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>, RS = never> {\n readonly instructions: string | Instructions.Resolved\n /** The loop's tools, as a native toolkit whose handlers come from the layer. */\n readonly toolkit?: Toolkit.Toolkit<T> | undefined\n /**\n * Playbooks, listed as a catalog in the prompt and loaded with `load_skill` when one matches. A\n * skill made with `tools` is left out when the toolkit lacks one of them.\n */\n readonly skills?: ReadonlyArray<Skill<RS>> | 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, tools, and skills. */\n readonly loop: <T extends Record<string, Tool.Any> = Record<never, never>, RS = never>(\n options: LoopOptions<T, RS>\n ) => Stream.Stream<\n AssistantLoop.Event,\n AiError.AiError,\n LanguageModel.LanguageModel | AssistantLoop.ToolkitServices<T> | RS\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":";;;;;;;;;;;;;AA2BA,MAAM,SAAS;AA0Kf,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"}