effect-assistant 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) 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 +19 -8
  13. package/dist/AssistantEvent.d.ts.map +1 -1
  14. package/dist/AssistantEvent.js +20 -9
  15. package/dist/AssistantEvent.js.map +1 -1
  16. package/dist/AssistantLoop.d.ts +2 -2
  17. package/dist/AssistantLoop.js.map +1 -1
  18. package/dist/Block.d.ts +63 -1
  19. package/dist/Block.d.ts.map +1 -1
  20. package/dist/Block.js +63 -2
  21. package/dist/Block.js.map +1 -1
  22. package/dist/BlockKit.d.ts +25 -3
  23. package/dist/BlockKit.d.ts.map +1 -1
  24. package/dist/BlockKit.js +14 -12
  25. package/dist/BlockKit.js.map +1 -1
  26. package/dist/ChatLog.d.ts +9 -9
  27. package/dist/ChatLog.d.ts.map +1 -1
  28. package/dist/ChatLog.js +21 -21
  29. package/dist/ChatLog.js.map +1 -1
  30. package/dist/Compaction.d.ts +10 -10
  31. package/dist/Compaction.d.ts.map +1 -1
  32. package/dist/Compaction.js +19 -19
  33. package/dist/Compaction.js.map +1 -1
  34. package/dist/Memory.d.ts +1 -1
  35. package/dist/Memory.js +1 -1
  36. package/dist/Memory.js.map +1 -1
  37. package/dist/Skill.d.ts +1 -1
  38. package/dist/Skill.js +1 -1
  39. package/dist/Skill.js.map +1 -1
  40. package/dist/Specialist.d.ts +1 -1
  41. package/dist/Specialist.js +1 -1
  42. package/dist/Specialist.js.map +1 -1
  43. package/dist/Timeline.d.ts +13 -9
  44. package/dist/Timeline.d.ts.map +1 -1
  45. package/dist/Timeline.js +35 -23
  46. package/dist/Timeline.js.map +1 -1
  47. package/dist/ToolApproval.d.ts +1 -1
  48. package/dist/ToolApproval.js +1 -1
  49. package/dist/ToolApproval.js.map +1 -1
  50. package/dist/ToolDiscovery.d.ts +1 -1
  51. package/dist/ToolDiscovery.js +1 -1
  52. package/dist/ToolDiscovery.js.map +1 -1
  53. package/dist/ToolResult.d.ts +56 -0
  54. package/dist/ToolResult.d.ts.map +1 -0
  55. package/dist/ToolResult.js +49 -0
  56. package/dist/ToolResult.js.map +1 -0
  57. package/dist/index.d.ts +2 -1
  58. package/dist/index.js +4 -3
  59. package/dist/internal/chat.js +4 -4
  60. package/dist/internal/chat.js.map +1 -1
  61. package/dist/internal/events.js +1 -1
  62. package/dist/internal/events.js.map +1 -1
  63. package/dist/internal/history.js +23 -23
  64. package/dist/internal/history.js.map +1 -1
  65. package/dist/internal/loop.d.ts +1 -1
  66. package/dist/internal/loop.d.ts.map +1 -1
  67. package/dist/internal/loop.js +12 -6
  68. package/dist/internal/loop.js.map +1 -1
  69. package/dist/internal/toolkit.d.ts +7 -1
  70. package/dist/internal/toolkit.d.ts.map +1 -1
  71. package/dist/internal/toolkit.js +47 -4
  72. package/dist/internal/toolkit.js.map +1 -1
  73. package/dist/internal/tools.js +39 -6
  74. package/dist/internal/tools.js.map +1 -1
  75. package/dist/internal/turns.d.ts.map +1 -1
  76. package/dist/internal/turns.js +24 -21
  77. package/dist/internal/turns.js.map +1 -1
  78. package/package.json +1 -1
  79. package/src/Assistant.ts +11 -11
  80. package/src/AssistantActivity.ts +9 -7
  81. package/src/AssistantEngine.ts +3 -3
  82. package/src/AssistantEvent.ts +21 -10
  83. package/src/AssistantLoop.ts +3 -3
  84. package/src/Block.ts +82 -1
  85. package/src/BlockKit.ts +37 -4
  86. package/src/ChatLog.ts +31 -26
  87. package/src/Compaction.ts +21 -21
  88. package/src/Memory.ts +1 -1
  89. package/src/Skill.ts +1 -1
  90. package/src/Specialist.ts +1 -1
  91. package/src/Timeline.ts +38 -25
  92. package/src/ToolApproval.ts +1 -1
  93. package/src/ToolDiscovery.ts +1 -1
  94. package/src/ToolResult.ts +80 -0
  95. package/src/index.ts +5 -0
  96. package/src/internal/chat.ts +4 -4
  97. package/src/internal/events.ts +1 -1
  98. package/src/internal/history.ts +32 -28
  99. package/src/internal/loop.ts +24 -7
  100. package/src/internal/toolkit.ts +55 -5
  101. package/src/internal/tools.ts +81 -5
  102. package/src/internal/turns.ts +35 -29
@@ -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
  *
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
  *
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 * .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"}
@@ -182,7 +182,7 @@ export interface Built {
182
182
  *
183
183
  * export const MyAssistant = Assistant.make("my-assistant", { specialists: [Research] })
184
184
  *
185
- * const MyAssistantLive = MyAssistant.toLayer((turn) => turn.loop({ instructions })).pipe(
185
+ * const MyAssistantLive = MyAssistant.toLayer((turn) => turn.run({ instructions })).pipe(
186
186
  * Layer.provide(ResearchLive)
187
187
  * )
188
188
  * ```
@@ -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
  * ```
@@ -1 +1 @@
1
- {"version":3,"file":"Specialist.js","names":[],"sources":["../src/Specialist.ts"],"sourcesContent":["/**\n * Defines specialists, which are focused agents an assistant hands work to in the background.\n *\n * A specialist has a definition, created with {@link make}, that gives its name and when to use\n * it, and a handler, provided with its `toLayer`, that works on a {@link Brief} as a loop. The\n * {@link Runner} service starts, steers, and stops a chat's specialist runs.\n *\n * @since 0.1.0\n */\nimport type * as AiError from \"effect/ai/AiError\"\nimport type * as LanguageModel from \"effect/ai/LanguageModel\"\nimport type * as Prompt from \"effect/ai/Prompt\"\nimport type * as Tool from \"effect/ai/Tool\"\nimport type * as Toolkit from \"effect/ai/Toolkit\"\nimport * as Context from \"effect/Context\"\nimport * as Effect from \"effect/Effect\"\nimport * as Layer from \"effect/Layer\"\nimport * as Schema from \"effect/Schema\"\nimport type * as Scope from \"effect/Scope\"\nimport * as Stream from \"effect/Stream\"\nimport type * as AssistantLoop from \"./AssistantLoop.ts\"\nimport type { ChatId } from \"./ChatId.ts\"\nimport type { CurrentChat } from \"./CurrentChat.ts\"\nimport type * as Instructions from \"./Instructions.ts\"\nimport type { RunId } from \"./RunId.ts\"\n\nconst TypeId = \"~effect-assistant/Specialist\"\n\n/**\n * Represents a specialist's definition.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface Specialist<Name extends string = string> {\n readonly [TypeId]: typeof TypeId\n readonly name: Name\n /** When to use the specialist. The assistant sees this. */\n readonly description: string\n /** The key of the specialist's handler, as `toLayer` provides it. */\n readonly Handler: Context.Key<SpecialistHandler<Name>, Built>\n /**\n * Provides the specialist's handler. The handler may use the {@link RunServices} and the\n * services its build Effect required (see {@link CheckServices}).\n */\n readonly toLayer: <R = never, RE = never, E = never, RX = never>(\n build: (Handler<R, RE> | Effect.Effect<Handler<R, RE>, E, RX>) &\n CheckServices<Exclude<R | RE, RunServices | RX | Tool.Handler<string>>>\n ) => Layer.Layer<SpecialistHandler<Name>, never, Exclude<RX, Scope.Scope | CurrentChat> | ToolHandlers<R | RE>>\n}\n\n/**\n * Represents the check `toLayer` applies to the services a handler uses.\n *\n * **Details**\n *\n * Every service a handler uses must be one the run (or turn) provides, or one its build Effect\n * required, which the layer provides. A handler that uses any other service gets this type\n * error, which names those services.\n *\n * @category utility types\n * @since 0.1.0\n */\nexport type CheckServices<Undeclared> = [Undeclared] extends [never]\n ? unknown\n : {\n readonly \"~undeclaredServices\": {\n readonly error: \"The handler uses services that neither the turn provides nor its build Effect requires\"\n readonly services: Undeclared\n }\n }\n\n/**\n * Extracts the tool handlers (`Toolkit.toLayer`) that a handler's loop uses.\n *\n * They are requirements of the layer, so `Layer.provide(MyToolsLive)` provides them, and a\n * missing one is a type error.\n *\n * @category utility types\n * @since 0.1.0\n */\nexport type ToolHandlers<R> = Extract<R, Tool.Handler<string>>\n\n/**\n * Represents any specialist.\n *\n * @category utility types\n * @since 0.1.0\n */\nexport type Any = Specialist<any>\n\n/**\n * Identifies a specialist's handler in a context.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface SpecialistHandler<Name extends string> {\n readonly _SpecialistHandler: Name\n}\n\n/**\n * Extracts the handlers that these specialists need.\n *\n * @category utility types\n * @since 0.1.0\n */\nexport type HandlerOf<S> = S extends Specialist<infer Name> ? SpecialistHandler<Name> : never\n\n/**\n * The services a run provides to its handler, which are its chat, the chat's runner, and the\n * model.\n *\n * @category utility types\n * @since 0.1.0\n */\nexport type RunServices = CurrentChat | Runner | LanguageModel.LanguageModel\n\n/**\n * Represents the options for `run.loop`.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface LoopOptions<T extends Record<string, Tool.Any> = Record<never, never>> {\n readonly instructions: string | Instructions.Resolved\n /**\n * The loop's tools, as a native toolkit whose handlers come from the layer. Merge in a\n * `Skill.toolkit(...)` for playbooks.\n */\n readonly toolkit?: Toolkit.Toolkit<T> | undefined\n /** The model calls allowed per brief. Defaults to 8. */\n readonly maxSteps?: number | undefined\n}\n\n/**\n * Represents one run of a specialist on a brief, as its handler sees it.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface Run {\n readonly runId: RunId\n /** The chat the run belongs to, which is the chat of the turn that spawned it. */\n readonly chatId: ChatId\n readonly brief: Brief\n /**\n * The run's history, with the brief, what the run did so far, and the messages sent to it since\n * (follow-ups and steering). Read it before every model call, as the loop does.\n */\n readonly history: Effect.Effect<ReadonlyArray<Prompt.Message>>\n /** Runs the agent loop over the run's history, with the given instructions and tools. */\n readonly loop: <T extends Record<string, Tool.Any> = Record<never, never>>(\n options: LoopOptions<T>\n ) => Stream.Stream<\n AssistantLoop.Event,\n AiError.AiError,\n LanguageModel.LanguageModel | AssistantLoop.ToolkitServices<T>\n >\n}\n\n/**\n * Represents a specialist's handler, which works on a brief and returns a stream of what the loop\n * did.\n *\n * It may return the stream from an Effect. `R` is the services the stream needs, and `RE` the\n * services the Effect needs.\n *\n * @category models\n * @since 0.1.0\n */\nexport type Handler<R = never, RE = R> = (\n run: Run\n) =>\n | Stream.Stream<AssistantLoop.Event, unknown, R>\n | Effect.Effect<Stream.Stream<AssistantLoop.Event, unknown, R>, unknown, RE>\n\n/**\n * Represents a specialist's handler as its layer provides it.\n *\n * `open` runs the build for a chat (once per chat activation, see `CurrentChat`) and returns\n * the handler with the layer's services already provided.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface Built {\n readonly open: Effect.Effect<\n (run: Run) => Stream.Stream<AssistantLoop.Event, unknown, RunServices>,\n never,\n Scope.Scope | CurrentChat\n >\n}\n\nconst toStream = <R, RE>(\n result:\n | Stream.Stream<AssistantLoop.Event, unknown, R>\n | Effect.Effect<Stream.Stream<AssistantLoop.Event, unknown, R>, unknown, RE>\n): Stream.Stream<AssistantLoop.Event, unknown, R | RE> => (Effect.isEffect(result) ? Stream.unwrap(result) : result)\n\n/**\n * Creates a specialist from its name and a description of when to use it.\n *\n * **Details**\n *\n * The specialist's `toLayer` provides its handler, which is usually `run.loop` with the\n * specialist's own instructions and tools. List the specialist in `Assistant.make`'s\n * `specialists`, and provide its layer to the assistant's.\n *\n * **Example** (Defining a research specialist)\n *\n * ```ts\n * import { Assistant, Specialist } from \"effect-assistant\"\n * import * as Layer from \"effect/Layer\"\n *\n * const Research = Specialist.make(\"research\", {\n * description: \"Researches past campaigns and orders. Give it the question and any context.\"\n * })\n *\n * const ResearchLive = Research.toLayer((run) =>\n * run.loop({\n * instructions: \"Answer with the findings and numbers the colleague needs, in a few lines.\",\n * toolkit: ResearchTools\n * })\n * ).pipe(Layer.provide(ResearchToolsLive))\n *\n * export const MyAssistant = Assistant.make(\"my-assistant\", { specialists: [Research] })\n *\n * const MyAssistantLive = MyAssistant.toLayer((turn) => turn.loop({ instructions })).pipe(\n * Layer.provide(ResearchLive)\n * )\n * ```\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const make = <const Name extends string>(\n name: Name,\n options: {\n /** When to use it. The assistant sees this. */\n readonly description: string\n }\n): Specialist<Name> => {\n const Handler = Context.Service<SpecialistHandler<Name>, Built>(`effect-assistant/Specialist/${name}`)\n\n // The signature checks what the handler uses (`CheckServices`); the layer's services, which the\n // build required, are provided to it and to every run's stream, so what's left is the run\n // services.\n function toLayer<R = never, RE = never, E = never, RX = never>(\n build: (Handler<R, RE> | Effect.Effect<Handler<R, RE>, E, RX>) &\n CheckServices<Exclude<R | RE, RunServices | RX | Tool.Handler<string>>>\n ): Layer.Layer<SpecialistHandler<Name>, never, Exclude<RX, Scope.Scope | CurrentChat> | ToolHandlers<R | RE>>\n function toLayer(\n build: Handler<any, any> | Effect.Effect<Handler<any, any>, unknown, any>\n ): Layer.Layer<SpecialistHandler<Name>, never, any> {\n return Layer.effect(\n Handler,\n Effect.gen(function* () {\n const context = yield* Effect.context()\n\n return {\n // The build runs per chat activation, in the chat; here it only keeps the layer's\n // services for it.\n open: Effect.gen(function* () {\n const handler = Effect.isEffect(build) ? yield* build : build\n\n return (run: Run) => Stream.provideContext(toStream(handler(run)), context)\n }).pipe(Effect.provideContext(context), Effect.orDie)\n }\n })\n )\n }\n\n return { [TypeId]: TypeId, name, description: options.description, Handler, toLayer }\n}\n\n/**\n * Schema for the brief the assistant gives a specialist.\n *\n * Only `goal` is required.\n *\n * @category schemas\n * @since 0.1.0\n */\nexport const Brief = Schema.Struct({\n goal: Schema.String.annotate({ description: \"What to find out or do.\" }),\n context: Schema.optional(Schema.String).annotate({\n description: \"What it needs to know. It can't see this chat.\"\n }),\n successCriteria: Schema.optional(Schema.String).annotate({ description: \"When it is done.\" }),\n report: Schema.optional(Schema.String).annotate({\n description: \"What to put in the answer, and how.\"\n }),\n limits: Schema.optional(Schema.String).annotate({\n description: \"Hard limits for this job, like read-only or a deadline.\"\n })\n})\n\n/**\n * Represents the brief the assistant gives a specialist.\n *\n * @category models\n * @since 0.1.0\n */\nexport type Brief = typeof Brief.Type\n\n/**\n * Represents a request to start a specialist run in the runner's chat.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface SpawnRequest {\n /** The specialist's name. */\n readonly specialist: string\n readonly brief: Brief\n}\n\n/**\n * Represents why a run request can't be carried out.\n *\n * **Details**\n *\n * The `reason` says which request failed: `\"UnknownSpecialist\"` when `spawn` names no specialist\n * of the assistant, `\"RunNotFound\"` when `message` names a run that is gone, and `\"NotRunning\"`\n * when `stop` names a run that isn't running. The `message` is written for the model.\n *\n * @category errors\n * @since 0.1.0\n */\nexport class SpecialistError extends Schema.Error<SpecialistError>(\"effect-assistant/Specialist/SpecialistError\")({\n _tag: Schema.tag(\"SpecialistError\"),\n reason: Schema.Literals([\"UnknownSpecialist\", \"RunNotFound\", \"NotRunning\"]),\n message: Schema.String\n}) {}\n\n/**\n * Service that runs a chat's specialists and keeps finished runs warm for follow-ups.\n *\n * The engine provides it to every turn. The specialist tools use it, and a handler can too.\n *\n * @category services\n * @since 0.1.0\n */\nexport class Runner extends Context.Service<\n Runner,\n {\n /** Starts a run and returns its id. */\n readonly spawn: (request: SpawnRequest) => Effect.Effect<RunId, SpecialistError>\n /** Steers a running run, or continues a finished one. */\n readonly message: (runId: RunId, text: string) => Effect.Effect<\"steered\" | \"continued\", SpecialistError>\n /** Interrupts a running run. */\n readonly stop: (runId: RunId) => Effect.Effect<void, SpecialistError>\n }\n>()(\"effect-assistant/Specialist/Runner\") {}\n"],"mappings":";;;;;;;;;;;;;AA0BA,MAAM,SAAS;AAwKf,MAAM,YACJ,WAGyD,OAAO,SAAS,MAAM,IAAI,OAAO,OAAO,MAAM,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsC7G,MAAa,QACX,MACA,YAIqB;CACrB,MAAM,UAAU,QAAQ,QAAwC,+BAA+B,MAAM;CASrG,SAAS,QACP,OACkD;EAClD,OAAO,MAAM,OACX,SACA,OAAO,IAAI,aAAa;GACtB,MAAM,UAAU,OAAO,OAAO,QAAQ;GAEtC,OAAO,EAGL,MAAM,OAAO,IAAI,aAAa;IAC5B,MAAM,UAAU,OAAO,SAAS,KAAK,IAAI,OAAO,QAAQ;IAExD,QAAQ,QAAa,OAAO,eAAe,SAAS,QAAQ,GAAG,CAAC,GAAG,OAAO;GAC5E,CAAC,CAAC,CAAC,KAAK,OAAO,eAAe,OAAO,GAAG,OAAO,KAAK,EACtD;EACF,CAAC,CACH;CACF;CAEA,OAAO;GAAG,SAAS;EAAQ;EAAM,aAAa,QAAQ;EAAa;EAAS;CAAQ;AACtF;;;;;;;;;AAUA,MAAa,QAAQ,OAAO,OAAO;CACjC,MAAM,OAAO,OAAO,SAAS,EAAE,aAAa,0BAA0B,CAAC;CACvE,SAAS,OAAO,SAAS,OAAO,MAAM,CAAC,CAAC,SAAS,EAC/C,aAAa,iDACf,CAAC;CACD,iBAAiB,OAAO,SAAS,OAAO,MAAM,CAAC,CAAC,SAAS,EAAE,aAAa,mBAAmB,CAAC;CAC5F,QAAQ,OAAO,SAAS,OAAO,MAAM,CAAC,CAAC,SAAS,EAC9C,aAAa,sCACf,CAAC;CACD,QAAQ,OAAO,SAAS,OAAO,MAAM,CAAC,CAAC,SAAS,EAC9C,aAAa,0DACf,CAAC;AACH,CAAC;;;;;;;;;;;;;AAkCD,IAAa,kBAAb,cAAqC,OAAO,MAAuB,6CAA6C,CAAC,CAAC;CAChH,MAAM,OAAO,IAAI,iBAAiB;CAClC,QAAQ,OAAO,SAAS;EAAC;EAAqB;EAAe;CAAY,CAAC;CAC1E,SAAS,OAAO;AAClB,CAAC,CAAC,CAAC,CAAC;;;;;;;;;AAUJ,IAAa,SAAb,cAA4B,QAAQ,QAUlC,CAAC,CAAC,oCAAoC,CAAC,CAAC,CAAC"}
1
+ {"version":3,"file":"Specialist.js","names":[],"sources":["../src/Specialist.ts"],"sourcesContent":["/**\n * Defines specialists, which are focused agents an assistant hands work to in the background.\n *\n * A specialist has a definition, created with {@link make}, that gives its name and when to use\n * it, and a handler, provided with its `toLayer`, that works on a {@link Brief} as a loop. The\n * {@link Runner} service starts, steers, and stops a chat's specialist runs.\n *\n * @since 0.1.0\n */\nimport type * as AiError from \"effect/ai/AiError\"\nimport type * as LanguageModel from \"effect/ai/LanguageModel\"\nimport type * as Prompt from \"effect/ai/Prompt\"\nimport type * as Tool from \"effect/ai/Tool\"\nimport type * as Toolkit from \"effect/ai/Toolkit\"\nimport * as Context from \"effect/Context\"\nimport * as Effect from \"effect/Effect\"\nimport * as Layer from \"effect/Layer\"\nimport * as Schema from \"effect/Schema\"\nimport type * as Scope from \"effect/Scope\"\nimport * as Stream from \"effect/Stream\"\nimport type * as AssistantLoop from \"./AssistantLoop.ts\"\nimport type { ChatId } from \"./ChatId.ts\"\nimport type { CurrentChat } from \"./CurrentChat.ts\"\nimport type * as Instructions from \"./Instructions.ts\"\nimport type { RunId } from \"./RunId.ts\"\n\nconst TypeId = \"~effect-assistant/Specialist\"\n\n/**\n * Represents a specialist's definition.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface Specialist<Name extends string = string> {\n readonly [TypeId]: typeof TypeId\n readonly name: Name\n /** When to use the specialist. The assistant sees this. */\n readonly description: string\n /** The key of the specialist's handler, as `toLayer` provides it. */\n readonly Handler: Context.Key<SpecialistHandler<Name>, Built>\n /**\n * Provides the specialist's handler. The handler may use the {@link RunServices} and the\n * services its build Effect required (see {@link CheckServices}).\n */\n readonly toLayer: <R = never, RE = never, E = never, RX = never>(\n build: (Handler<R, RE> | Effect.Effect<Handler<R, RE>, E, RX>) &\n CheckServices<Exclude<R | RE, RunServices | RX | Tool.Handler<string>>>\n ) => Layer.Layer<SpecialistHandler<Name>, never, Exclude<RX, Scope.Scope | CurrentChat> | ToolHandlers<R | RE>>\n}\n\n/**\n * Represents the check `toLayer` applies to the services a handler uses.\n *\n * **Details**\n *\n * Every service a handler uses must be one the run (or turn) provides, or one its build Effect\n * required, which the layer provides. A handler that uses any other service gets this type\n * error, which names those services.\n *\n * @category utility types\n * @since 0.1.0\n */\nexport type CheckServices<Undeclared> = [Undeclared] extends [never]\n ? unknown\n : {\n readonly \"~undeclaredServices\": {\n readonly error: \"The handler uses services that neither the turn provides nor its build Effect requires\"\n readonly services: Undeclared\n }\n }\n\n/**\n * Extracts the tool handlers (`Toolkit.toLayer`) that a handler's loop uses.\n *\n * They are requirements of the layer, so `Layer.provide(MyToolsLive)` provides them, and a\n * missing one is a type error.\n *\n * @category utility types\n * @since 0.1.0\n */\nexport type ToolHandlers<R> = Extract<R, Tool.Handler<string>>\n\n/**\n * Represents any specialist.\n *\n * @category utility types\n * @since 0.1.0\n */\nexport type Any = Specialist<any>\n\n/**\n * Identifies a specialist's handler in a context.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface SpecialistHandler<Name extends string> {\n readonly _SpecialistHandler: Name\n}\n\n/**\n * Extracts the handlers that these specialists need.\n *\n * @category utility types\n * @since 0.1.0\n */\nexport type HandlerOf<S> = S extends Specialist<infer Name> ? SpecialistHandler<Name> : never\n\n/**\n * The services a run provides to its handler, which are its chat, the chat's runner, and the\n * model.\n *\n * @category utility types\n * @since 0.1.0\n */\nexport type RunServices = CurrentChat | Runner | LanguageModel.LanguageModel\n\n/**\n * Represents the options for `run.loop`.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface LoopOptions<T extends Record<string, Tool.Any> = Record<never, never>> {\n readonly instructions: string | Instructions.Resolved\n /**\n * The loop's tools, as a native toolkit whose handlers come from the layer. Merge in a\n * `Skill.toolkit(...)` for playbooks.\n */\n readonly toolkit?: Toolkit.Toolkit<T> | undefined\n /** The model calls allowed per brief. Defaults to 8. */\n readonly maxSteps?: number | undefined\n}\n\n/**\n * Represents one run of a specialist on a brief, as its handler sees it.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface Run {\n readonly runId: RunId\n /** The chat the run belongs to, which is the chat of the turn that spawned it. */\n readonly chatId: ChatId\n readonly brief: Brief\n /**\n * The run's history, with the brief, what the run did so far, and the messages sent to it since\n * (follow-ups and steering). Read it before every model call, as the loop does.\n */\n readonly history: Effect.Effect<ReadonlyArray<Prompt.Message>>\n /** Runs the agent loop over the run's history, with the given instructions and tools. */\n readonly loop: <T extends Record<string, Tool.Any> = Record<never, never>>(\n options: LoopOptions<T>\n ) => Stream.Stream<\n AssistantLoop.Event,\n AiError.AiError,\n LanguageModel.LanguageModel | AssistantLoop.ToolkitServices<T>\n >\n}\n\n/**\n * Represents a specialist's handler, which works on a brief and returns a stream of what the loop\n * did.\n *\n * It may return the stream from an Effect. `R` is the services the stream needs, and `RE` the\n * services the Effect needs.\n *\n * @category models\n * @since 0.1.0\n */\nexport type Handler<R = never, RE = R> = (\n run: Run\n) =>\n | Stream.Stream<AssistantLoop.Event, unknown, R>\n | Effect.Effect<Stream.Stream<AssistantLoop.Event, unknown, R>, unknown, RE>\n\n/**\n * Represents a specialist's handler as its layer provides it.\n *\n * `open` runs the build for a chat (once per chat activation, see `CurrentChat`) and returns\n * the handler with the layer's services already provided.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface Built {\n readonly open: Effect.Effect<\n (run: Run) => Stream.Stream<AssistantLoop.Event, unknown, RunServices>,\n never,\n Scope.Scope | CurrentChat\n >\n}\n\nconst toStream = <R, RE>(\n result:\n | Stream.Stream<AssistantLoop.Event, unknown, R>\n | Effect.Effect<Stream.Stream<AssistantLoop.Event, unknown, R>, unknown, RE>\n): Stream.Stream<AssistantLoop.Event, unknown, R | RE> => (Effect.isEffect(result) ? Stream.unwrap(result) : result)\n\n/**\n * Creates a specialist from its name and a description of when to use it.\n *\n * **Details**\n *\n * The specialist's `toLayer` provides its handler, which is usually `run.loop` with the\n * specialist's own instructions and tools. List the specialist in `Assistant.make`'s\n * `specialists`, and provide its layer to the assistant's.\n *\n * **Example** (Defining a research specialist)\n *\n * ```ts\n * import { Assistant, Specialist } from \"effect-assistant\"\n * import * as Layer from \"effect/Layer\"\n *\n * const Research = Specialist.make(\"research\", {\n * description: \"Researches past campaigns and orders. Give it the question and any context.\"\n * })\n *\n * const ResearchLive = Research.toLayer((run) =>\n * run.loop({\n * instructions: \"Answer with the findings and numbers the colleague needs, in a few lines.\",\n * toolkit: ResearchTools\n * })\n * ).pipe(Layer.provide(ResearchToolsLive))\n *\n * export const MyAssistant = Assistant.make(\"my-assistant\", { specialists: [Research] })\n *\n * const MyAssistantLive = MyAssistant.toLayer((turn) => turn.run({ instructions })).pipe(\n * Layer.provide(ResearchLive)\n * )\n * ```\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const make = <const Name extends string>(\n name: Name,\n options: {\n /** When to use it. The assistant sees this. */\n readonly description: string\n }\n): Specialist<Name> => {\n const Handler = Context.Service<SpecialistHandler<Name>, Built>(`effect-assistant/Specialist/${name}`)\n\n // The signature checks what the handler uses (`CheckServices`); the layer's services, which the\n // build required, are provided to it and to every run's stream, so what's left is the run\n // services.\n function toLayer<R = never, RE = never, E = never, RX = never>(\n build: (Handler<R, RE> | Effect.Effect<Handler<R, RE>, E, RX>) &\n CheckServices<Exclude<R | RE, RunServices | RX | Tool.Handler<string>>>\n ): Layer.Layer<SpecialistHandler<Name>, never, Exclude<RX, Scope.Scope | CurrentChat> | ToolHandlers<R | RE>>\n function toLayer(\n build: Handler<any, any> | Effect.Effect<Handler<any, any>, unknown, any>\n ): Layer.Layer<SpecialistHandler<Name>, never, any> {\n return Layer.effect(\n Handler,\n Effect.gen(function* () {\n const context = yield* Effect.context()\n\n return {\n // The build runs per chat activation, in the chat; here it only keeps the layer's\n // services for it.\n open: Effect.gen(function* () {\n const handler = Effect.isEffect(build) ? yield* build : build\n\n return (run: Run) => Stream.provideContext(toStream(handler(run)), context)\n }).pipe(Effect.provideContext(context), Effect.orDie)\n }\n })\n )\n }\n\n return { [TypeId]: TypeId, name, description: options.description, Handler, toLayer }\n}\n\n/**\n * Schema for the brief the assistant gives a specialist.\n *\n * Only `goal` is required.\n *\n * @category schemas\n * @since 0.1.0\n */\nexport const Brief = Schema.Struct({\n goal: Schema.String.annotate({ description: \"What to find out or do.\" }),\n context: Schema.optional(Schema.String).annotate({\n description: \"What it needs to know. It can't see this chat.\"\n }),\n successCriteria: Schema.optional(Schema.String).annotate({ description: \"When it is done.\" }),\n report: Schema.optional(Schema.String).annotate({\n description: \"What to put in the answer, and how.\"\n }),\n limits: Schema.optional(Schema.String).annotate({\n description: \"Hard limits for this job, like read-only or a deadline.\"\n })\n})\n\n/**\n * Represents the brief the assistant gives a specialist.\n *\n * @category models\n * @since 0.1.0\n */\nexport type Brief = typeof Brief.Type\n\n/**\n * Represents a request to start a specialist run in the runner's chat.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface SpawnRequest {\n /** The specialist's name. */\n readonly specialist: string\n readonly brief: Brief\n}\n\n/**\n * Represents why a run request can't be carried out.\n *\n * **Details**\n *\n * The `reason` says which request failed: `\"UnknownSpecialist\"` when `spawn` names no specialist\n * of the assistant, `\"RunNotFound\"` when `message` names a run that is gone, and `\"NotRunning\"`\n * when `stop` names a run that isn't running. The `message` is written for the model.\n *\n * @category errors\n * @since 0.1.0\n */\nexport class SpecialistError extends Schema.Error<SpecialistError>(\"effect-assistant/Specialist/SpecialistError\")({\n _tag: Schema.tag(\"SpecialistError\"),\n reason: Schema.Literals([\"UnknownSpecialist\", \"RunNotFound\", \"NotRunning\"]),\n message: Schema.String\n}) {}\n\n/**\n * Service that runs a chat's specialists and keeps finished runs warm for follow-ups.\n *\n * The engine provides it to every turn. The specialist tools use it, and a handler can too.\n *\n * @category services\n * @since 0.1.0\n */\nexport class Runner extends Context.Service<\n Runner,\n {\n /** Starts a run and returns its id. */\n readonly spawn: (request: SpawnRequest) => Effect.Effect<RunId, SpecialistError>\n /** Steers a running run, or continues a finished one. */\n readonly message: (runId: RunId, text: string) => Effect.Effect<\"steered\" | \"continued\", SpecialistError>\n /** Interrupts a running run. */\n readonly stop: (runId: RunId) => Effect.Effect<void, SpecialistError>\n }\n>()(\"effect-assistant/Specialist/Runner\") {}\n"],"mappings":";;;;;;;;;;;;;AA0BA,MAAM,SAAS;AAwKf,MAAM,YACJ,WAGyD,OAAO,SAAS,MAAM,IAAI,OAAO,OAAO,MAAM,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsC7G,MAAa,QACX,MACA,YAIqB;CACrB,MAAM,UAAU,QAAQ,QAAwC,+BAA+B,MAAM;CASrG,SAAS,QACP,OACkD;EAClD,OAAO,MAAM,OACX,SACA,OAAO,IAAI,aAAa;GACtB,MAAM,UAAU,OAAO,OAAO,QAAQ;GAEtC,OAAO,EAGL,MAAM,OAAO,IAAI,aAAa;IAC5B,MAAM,UAAU,OAAO,SAAS,KAAK,IAAI,OAAO,QAAQ;IAExD,QAAQ,QAAa,OAAO,eAAe,SAAS,QAAQ,GAAG,CAAC,GAAG,OAAO;GAC5E,CAAC,CAAC,CAAC,KAAK,OAAO,eAAe,OAAO,GAAG,OAAO,KAAK,EACtD;EACF,CAAC,CACH;CACF;CAEA,OAAO;GAAG,SAAS;EAAQ;EAAM,aAAa,QAAQ;EAAa;EAAS;CAAQ;AACtF;;;;;;;;;AAUA,MAAa,QAAQ,OAAO,OAAO;CACjC,MAAM,OAAO,OAAO,SAAS,EAAE,aAAa,0BAA0B,CAAC;CACvE,SAAS,OAAO,SAAS,OAAO,MAAM,CAAC,CAAC,SAAS,EAC/C,aAAa,iDACf,CAAC;CACD,iBAAiB,OAAO,SAAS,OAAO,MAAM,CAAC,CAAC,SAAS,EAAE,aAAa,mBAAmB,CAAC;CAC5F,QAAQ,OAAO,SAAS,OAAO,MAAM,CAAC,CAAC,SAAS,EAC9C,aAAa,sCACf,CAAC;CACD,QAAQ,OAAO,SAAS,OAAO,MAAM,CAAC,CAAC,SAAS,EAC9C,aAAa,0DACf,CAAC;AACH,CAAC;;;;;;;;;;;;;AAkCD,IAAa,kBAAb,cAAqC,OAAO,MAAuB,6CAA6C,CAAC,CAAC;CAChH,MAAM,OAAO,IAAI,iBAAiB;CAClC,QAAQ,OAAO,SAAS;EAAC;EAAqB;EAAe;CAAY,CAAC;CAC1E,SAAS,OAAO;AAClB,CAAC,CAAC,CAAC,CAAC;;;;;;;;;AAUJ,IAAa,SAAb,cAA4B,QAAQ,QAUlC,CAAC,CAAC,oCAAoC,CAAC,CAAC,CAAC"}
@@ -34,6 +34,8 @@ export declare const Block: Schema.Struct<{
34
34
  readonly props: Schema.Codec<Schema.Json, Schema.Json, never, never>;
35
35
  readonly status: Schema.Literals<readonly ["open", "answered", "closed"]>;
36
36
  readonly answer: Schema.optional<Schema.Codec<Schema.Json, Schema.Json, never, never>>;
37
+ /** For a block that shows a tool's result (`Block.fromToolResult`): the result, as the tool's success schema encodes it. */
38
+ readonly result: Schema.optional<Schema.Codec<Schema.Json, Schema.Json, never, never>>;
37
39
  }>;
38
40
  /**
39
41
  * Represents a block on an assistant's message.
@@ -82,6 +84,8 @@ export declare const AssistantMessage: Schema.TaggedStruct<"AssistantMessage", {
82
84
  readonly props: Schema.Codec<Schema.Json, Schema.Json, never, never>;
83
85
  readonly status: Schema.Literals<readonly ["open", "answered", "closed"]>;
84
86
  readonly answer: Schema.optional<Schema.Codec<Schema.Json, Schema.Json, never, never>>;
87
+ /** For a block that shows a tool's result (`Block.fromToolResult`): the result, as the tool's success schema encodes it. */
88
+ readonly result: Schema.optional<Schema.Codec<Schema.Json, Schema.Json, never, never>>;
85
89
  }>>;
86
90
  }>;
87
91
  /**
@@ -217,13 +221,13 @@ export type Activity = typeof Activity.Type;
217
221
  /**
218
222
  * Represents a chat's timeline: the chat as its user sees it, typed with the app's events it shows.
219
223
  * It holds the chat's items in order, what is working in it, whether the last turn failed, and
220
- * 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.
221
225
  *
222
226
  * @category models
223
227
  * @since 0.3.0
224
228
  */
225
229
  export interface Timeline<Events = never> {
226
- readonly seq: number;
230
+ readonly sequence: number;
227
231
  readonly items: ReadonlyArray<Item<Events>>;
228
232
  readonly activity: Activity;
229
233
  /** Whether the last turn failed with no message after it. */
@@ -238,14 +242,14 @@ export interface Timeline<Events = never> {
238
242
  *
239
243
  * A change holds each item that is new or changed (for example, a message whose block was
240
244
  * answered or closed, or a call the user approved), in full. It also holds the current activity, whether the last turn failed,
241
- * 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
242
246
  * to.
243
247
  *
244
248
  * @category models
245
249
  * @since 0.3.0
246
250
  */
247
251
  export interface Change<Events = never> {
248
- readonly seq: number;
252
+ readonly sequence: number;
249
253
  readonly items: ReadonlyArray<Item<Events>>;
250
254
  readonly activity: Activity;
251
255
  readonly failed: boolean;
@@ -455,7 +459,7 @@ export interface Definition<Events = never> {
455
459
  */
456
460
  export interface ChangesOptions<Show extends ReadonlyArray<AppEventSchema> = readonly []> extends ShowOptions<Show> {
457
461
  /**
458
- * 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
459
463
  * changes after it. Defaults to `0`, every change.
460
464
  */
461
465
  readonly after?: number | undefined;
@@ -467,9 +471,9 @@ export interface ChangesOptions<Show extends ReadonlyArray<AppEventSchema> = rea
467
471
  *
468
472
  * The entries must stream from the start of the chat's log, as `assistant.subscribe(chatId)` does
469
473
  * without `after`. The fold reads every entry, because a message depends on earlier entries: its
470
- * block is answered or closed later. `after` only skips emitting the changes up to that `seq`, so a
471
- * client that has the timeline up to a `seq` receives only what changed since. Pass `after` here,
472
- * 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,
473
477
  * and the changes it produces are wrong.
474
478
  *
475
479
  * Each chunk of entries becomes one change; the stored entries, for example, arrive as one chunk.
@@ -488,7 +492,7 @@ export interface ChangesOptions<Show extends ReadonlyArray<AppEventSchema> = rea
488
492
  * import * as Schema from "effect/Schema"
489
493
  * import * as Stream from "effect/Stream"
490
494
  *
491
- * 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 }) {}
492
496
  * class DailyBudgetReached extends Schema.TaggedClass<DailyBudgetReached>()("DailyBudgetReached", {}) {}
493
497
  *
494
498
  * const MyAssistant = Assistant.make("my-assistant", { events: [Summary, DailyBudgetReached] })
@@ -1 +1 @@
1
- {"version":3,"file":"Timeline.d.ts","names":[],"sources":["../src/Timeline.ts"],"mappings":";;;;;;;;;;;;;;;qBA4Ca,aAAW,OAAA;;;;;;;YAQZ,qBAAqB,YAAY;;;;;;;;;qBAUhC,OAAK,OAAA;;;;;;;;;;;;YAaN,eAAe,MAAM;;;;;;;;qBASpB,aAAW,OAAA;;;;;;;;;;;;;;;;;YAaZ,qBAAqB,YAAY;;;;;;;qBAQhC,kBAAgB,OAAA;;;;;;;;;;;;;;;;;YAajB,0BAA0B,iBAAiB;;;;;;;;;;;;;;;;YAiB3C,UAAU,KAAK;WAAqB,YAAY;;WAC7C,MAAM;WAAc;WAAqB;WAAqB,OAAO;;;;;;;;;;;;;;;;;;qBAmBvE,cAAY,OAAA;;;;;;;;;;;;;;;YAgBb,sBAAsB,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;YA2BnC,KAAK,kBAAkB,cAAc,mBAAmB,UAAU,UAAU;;;;;;;;qBAS3E,mBAAiB,OAAA;;;;;;;;;;YAWlB,2BAA2B,kBAAkB;;;;;;;;;;;qBAY5C,UAAQ,OAAA;;;;;;;;;;;;;YAWT,kBAAkB,SAAS;;;;;;;;;iBAUtB,SAAS;WACf;WACA,OAAO,cAAc,KAAK;WAC1B,UAAU;;WAEV;;WAEA;;;;;;;;;;;;;;;iBAgBM,OAAO;WACb;WACA,OAAO,cAAc,KAAK;WAC1B,UAAU;WACV;WACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAsEE,eAAe,eAAe,cAAc,+BACvD,SAAS,WACN,OAAO,MAAM,OAAO,yBAAyB,OAAO;;;;;;;;qBAS5C,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBA0CP,QAAS,QAAM,UAAY,SAAS,SAAO,QAAU,OAAO,YAAU,SAAS;;;;;;;;;;;;;;;;;;;;;;;qBAsC/E,OAAQ,QAAQ,GAAG,GAAC,SACtB,OAAO,OAAO,OAAO,SAAS,GAAG,OACzC,OAAO,OAAO,SAAS,SAAS,GAAG;;;;;;;;qBASzB,SAAU,QAAM,UAAY,SAAS;;;;;;;iBA2BjC,YAAY,aAAa,cAAc;;;;;WAK7C,OAAO;;;;;;;;YASN,MAAM,UAAU,cAAc;WAA2C,MAAM;;;;;;;;;;;;;;;;;qBAmC9E;GACV,SAAS,cAAc,YAA2B;GAClD,cAAc,aAAa,MAAM,uBAChC,SAAS,cAAc,WACvB,YAAY,WAAW,SACvB,UAAU,YAAY,QACrB,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAmDC;GACV,SAAS,cAAc,YAA2B;GAClD,cAAc,aAAa,MAAM,uBAChC,SAAS,cAAc,WACvB,YAAY,WAAW,SACvB,UAAU,YAAY,QACrB,SAAS;;;;;;;;;iBAsPG,WAAW;;WAEjB,QAAQ,cAAc;;WAEtB,OAAO,OAAO,MAAM,MAAgB,SAAS,OAAO;;;;;;;;iBAS9C,eACf,aAAa,cAAc,uCACnB,YAAY;;;;;WAKX;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAgFE;GACV,cAAc,aAAa,MAAM,uBAChC,YAAY,WAAW,SACvB,UAAU,eAAe,SACvB,GAAG,GAAG,SAAS,OAAO,OAAO,UAAyB,GAAG,OAAO,OAAO,OAAO,OAAO,uBAAuB,GAAG;GAClH,GAAG,GAAG,cAAc,aAAa,MAAM,uBACtC,SAAS,OAAO,OAAO,UAAyB,GAAG,IACnD,YAAY,WAAW,SACvB,UAAU,eAAe,QACxB,OAAO,OAAO,OAAO,uBAAuB,GAAG"}
1
+ {"version":3,"file":"Timeline.d.ts","names":[],"sources":["../src/Timeline.ts"],"mappings":";;;;;;;;;;;;;;;qBA4Ca,aAAW,OAAA;;;;;;;YAQZ,qBAAqB,YAAY;;;;;;;;;qBAUhC,OAAK,OAAA;;;;;;;;;;;;;;YAeN,eAAe,MAAM;;;;;;;;qBASpB,aAAW,OAAA;;;;;;;;;;;;;;;;;YAaZ,qBAAqB,YAAY;;;;;;;qBAQhC,kBAAgB,OAAA;;;;;;;;;;;;;;;;;;;YAajB,0BAA0B,iBAAiB;;;;;;;;;;;;;;;;YAiB3C,UAAU,KAAK;WAAqB,YAAY;;WAC7C,MAAM;WAAc;WAAqB;WAAqB,OAAO;;;;;;;;;;;;;;;;;;qBAmBvE,cAAY,OAAA;;;;;;;;;;;;;;;YAgBb,sBAAsB,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;YA2BnC,KAAK,kBAAkB,cAAc,mBAAmB,UAAU,UAAU;;;;;;;;qBAS3E,mBAAiB,OAAA;;;;;;;;;;YAWlB,2BAA2B,kBAAkB;;;;;;;;;;;qBAY5C,UAAQ,OAAA;;;;;;;;;;;;;YAWT,kBAAkB,SAAS;;;;;;;;;iBAUtB,SAAS;WACf;WACA,OAAO,cAAc,KAAK;WAC1B,UAAU;;WAEV;;WAEA;;;;;;;;;;;;;;;iBAgBM,OAAO;WACb;WACA,OAAO,cAAc,KAAK;WAC1B,UAAU;WACV;WACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAsEE,eAAe,eAAe,cAAc,+BACvD,SAAS,WACN,OAAO,MAAM,OAAO,yBAAyB,OAAO;;;;;;;;qBAS5C,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBA0CP,QAAS,QAAM,UAAY,SAAS,SAAO,QAAU,OAAO,YAAU,SAAS;;;;;;;;;;;;;;;;;;;;;;;qBAsC/E,OAAQ,QAAQ,GAAG,GAAC,SACtB,OAAO,OAAO,OAAO,SAAS,GAAG,OACzC,OAAO,OAAO,SAAS,SAAS,GAAG;;;;;;;;qBASzB,SAAU,QAAM,UAAY,SAAS;;;;;;;iBA2BjC,YAAY,aAAa,cAAc;;;;;WAK7C,OAAO;;;;;;;;YASN,MAAM,UAAU,cAAc;WAA2C,MAAM;;;;;;;;;;;;;;;;;qBAmC9E;GACV,SAAS,cAAc,YAA2B;GAClD,cAAc,aAAa,MAAM,uBAChC,SAAS,cAAc,WACvB,YAAY,WAAW,SACvB,UAAU,YAAY,QACrB,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAmDC;GACV,SAAS,cAAc,YAA2B;GAClD,cAAc,aAAa,MAAM,uBAChC,SAAS,cAAc,WACvB,YAAY,WAAW,SACvB,UAAU,YAAY,QACrB,SAAS;;;;;;;;;iBAiQG,WAAW;;WAEjB,QAAQ,cAAc;;WAEtB,OAAO,OAAO,MAAM,MAAgB,SAAS,OAAO;;;;;;;;iBAS9C,eACf,aAAa,cAAc,uCACnB,YAAY;;;;;WAKX;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAgFE;GACV,cAAc,aAAa,MAAM,uBAChC,YAAY,WAAW,SACvB,UAAU,eAAe,SACvB,GAAG,GAAG,SAAS,OAAO,OAAO,UAAyB,GAAG,OAAO,OAAO,OAAO,OAAO,uBAAuB,GAAG;GAClH,GAAG,GAAG,cAAc,aAAa,MAAM,uBACtC,SAAS,OAAO,OAAO,UAAyB,GAAG,IACnD,YAAY,WAAW,SACvB,UAAU,eAAe,QACxB,OAAO,OAAO,OAAO,uBAAuB,GAAG"}
package/dist/Timeline.js CHANGED
@@ -4,8 +4,8 @@ import { Attachment, ToolOutcome, isLibraryEvent } from "./AssistantEvent.js";
4
4
  import { apply as apply$1 } from "./ToolApproval.js";
5
5
  import { decodeWith, tagOf } from "./internal/events.js";
6
6
  import { toJson } from "./internal/schema.js";
7
- import { apply as apply$2, empty as empty$1 } from "./AssistantActivity.js";
8
- import { apply as apply$3 } from "./BlockKit.js";
7
+ import { apply as apply$2 } from "./BlockKit.js";
8
+ import { apply as apply$3, empty as empty$1 } from "./AssistantActivity.js";
9
9
  import * as Predicate from "effect/Predicate";
10
10
  import * as Schema from "effect/Schema";
11
11
  import * as Stream from "effect/Stream";
@@ -76,7 +76,9 @@ const Block = Schema.Struct({
76
76
  kind: Schema.String,
77
77
  props: Schema.Json,
78
78
  status: BlockStatus,
79
- answer: Schema.optional(Schema.Json)
79
+ answer: Schema.optional(Schema.Json),
80
+ /** For a block that shows a tool's result (`Block.fromToolResult`): the result, as the tool's success schema encodes it. */
81
+ result: Schema.optional(Schema.Json)
80
82
  });
81
83
  /**
82
84
  * Schema for a message the user sent. `at` is when it was stored, in epoch
@@ -173,7 +175,7 @@ function makeChange(events = []) {
173
175
  const decodeItem = Schema.decodeUnknownOption(AnyItem);
174
176
  const encodeItem = Schema.encodeUnknownSync(AnyItem);
175
177
  return Schema.Struct({
176
- seq: Schema.Int,
178
+ sequence: Schema.Int,
177
179
  items: Schema.Array(Schema.Json).pipe(Schema.decodeTo(Schema.Array(Schema.toType(AnyItem)), SchemaTransformation.transform({
178
180
  decode: (json) => json.flatMap((item) => Option.toArray(decodeItem(item))),
179
181
  encode: (items) => items.map((item) => encodeItem(item))
@@ -225,7 +227,7 @@ const Change = makeChange;
225
227
  * @since 0.1.0
226
228
  */
227
229
  const empty = {
228
- seq: 0,
230
+ sequence: 0,
229
231
  items: [],
230
232
  activity: {
231
233
  working: false,
@@ -272,7 +274,7 @@ const apply = (timeline, change) => {
272
274
  const changed = new Map(change.items.map((item) => [item.id, item]));
273
275
  const known = new Set(timeline.items.map((item) => item.id));
274
276
  return {
275
- seq: Math.max(timeline.seq, change.seq),
277
+ sequence: Math.max(timeline.sequence, change.sequence),
276
278
  items: [...timeline.items.map((item) => changed.get(item.id) ?? item), ...change.items.filter((item) => !known.has(item.id))],
277
279
  activity: change.activity,
278
280
  failed: change.failed,
@@ -381,13 +383,14 @@ const sameActivity = (a, b) => a.working === b.working && a.specialists.length =
381
383
  const folding = (show) => {
382
384
  const shown = new Map(show.map((schema) => [tagOf(schema), schema]));
383
385
  let approvals = /* @__PURE__ */ new Map();
384
- let seq = 0;
386
+ let sequence = 0;
385
387
  let failed = false;
386
388
  let deleted = false;
387
389
  let activity = empty$1;
388
390
  let blocks = /* @__PURE__ */ new Map();
389
391
  const items = /* @__PURE__ */ new Map();
390
392
  const sent = /* @__PURE__ */ new Map();
393
+ const results = /* @__PURE__ */ new Map();
391
394
  const blockOf = (message) => {
392
395
  if (message.block === void 0) return void 0;
393
396
  const state = blocks.get(message.id);
@@ -397,8 +400,14 @@ const folding = (show) => {
397
400
  props: toJson(message.block.props),
398
401
  status
399
402
  };
400
- return state?.answer === void 0 ? block : {
403
+ const { resultId } = message.block;
404
+ const result = resultId === void 0 ? void 0 : results.get(resultId);
405
+ const shown = result === void 0 ? block : {
401
406
  ...block,
407
+ result
408
+ };
409
+ return state?.answer === void 0 ? shown : {
410
+ ...shown,
402
411
  answer: toJson(state.answer.value)
403
412
  };
404
413
  };
@@ -453,7 +462,10 @@ const folding = (show) => {
453
462
  },
454
463
  ModelOutput: () => [],
455
464
  ToolStarted: () => [],
456
- ToolResult: () => [],
465
+ ToolResult: (result) => {
466
+ if (result.resultId !== void 0 && result.outcome === "success") results.set(result.resultId, result.full ?? result.result);
467
+ return [];
468
+ },
457
469
  TurnStarted: () => [],
458
470
  TurnCompleted: () => [],
459
471
  SpecialistStarted: () => [],
@@ -469,7 +481,7 @@ const folding = (show) => {
469
481
  const event = Schema.is(schema)(entry.event) ? Option.some(entry.event) : decodeWith(schema, entry.event);
470
482
  return Option.map(event, (decoded) => ({
471
483
  _tag: decoded._tag,
472
- id: `app-event:${entry.seq}`,
484
+ id: `app-event:${entry.sequence}`,
473
485
  at: entry.at,
474
486
  event: decoded
475
487
  }));
@@ -496,13 +508,13 @@ const folding = (show) => {
496
508
  deleted
497
509
  };
498
510
  const { event } = entry;
499
- seq = entry.seq;
500
- activity = apply$2(activity, entry);
511
+ sequence = entry.sequence;
512
+ activity = apply$3(activity, entry);
501
513
  if (!isLibraryEvent(event)) {
502
514
  const now = activityOf(activity);
503
515
  return Option.match(eventItemOf(entry), {
504
516
  onNone: () => sameActivity(previous.activity, now) ? void 0 : {
505
- seq,
517
+ sequence,
506
518
  items: [],
507
519
  activity: now,
508
520
  failed,
@@ -511,7 +523,7 @@ const folding = (show) => {
511
523
  onSome: (item) => {
512
524
  items.set(item.id, item);
513
525
  return {
514
- seq,
526
+ sequence,
515
527
  items: [item],
516
528
  activity: now,
517
529
  failed,
@@ -520,7 +532,7 @@ const folding = (show) => {
520
532
  }
521
533
  });
522
534
  }
523
- blocks = apply$3(blocks, event);
535
+ blocks = apply$2(blocks, event);
524
536
  const decidedBefore = approvals;
525
537
  approvals = apply$1(approvals, entry);
526
538
  const all = [...[...new Set(touched(event, entry.at))].flatMap((id) => {
@@ -533,7 +545,7 @@ const folding = (show) => {
533
545
  }), ...approvalItems(event, decidedBefore)];
534
546
  const now = activityOf(activity);
535
547
  return all.length === 0 && failed === previous.failed && deleted === previous.deleted && sameActivity(previous.activity, now) ? void 0 : {
536
- seq,
548
+ sequence,
537
549
  items: all,
538
550
  activity: now,
539
551
  failed,
@@ -544,7 +556,7 @@ const folding = (show) => {
544
556
  step,
545
557
  get snapshot() {
546
558
  return {
547
- seq,
559
+ sequence,
548
560
  items: [...items.values()],
549
561
  activity: activityOf(activity),
550
562
  failed,
@@ -559,7 +571,7 @@ const merge = (changes) => {
559
571
  const items = /* @__PURE__ */ new Map();
560
572
  for (const change of changes) for (const item of change.items) items.set(item.id, item);
561
573
  return {
562
- seq: last.seq,
574
+ sequence: last.sequence,
563
575
  items: [...items.values()],
564
576
  activity: last.activity,
565
577
  failed: last.failed,
@@ -571,7 +583,7 @@ function changesWith(entries, _definition, options) {
571
583
  return entries.pipe(Stream.mapAccumArray(() => folding(options?.show ?? []), (timeline, chunk) => {
572
584
  const changed = chunk.flatMap((entry) => {
573
585
  const change = timeline.step(entry);
574
- return change === void 0 || entry.seq <= after ? [] : [change];
586
+ return change === void 0 || entry.sequence <= after ? [] : [change];
575
587
  });
576
588
  const merged = merge(changed);
577
589
  return [timeline, merged === void 0 ? [] : [merged]];
@@ -584,9 +596,9 @@ function changesWith(entries, _definition, options) {
584
596
  *
585
597
  * The entries must stream from the start of the chat's log, as `assistant.subscribe(chatId)` does
586
598
  * without `after`. The fold reads every entry, because a message depends on earlier entries: its
587
- * block is answered or closed later. `after` only skips emitting the changes up to that `seq`, so a
588
- * client that has the timeline up to a `seq` receives only what changed since. Pass `after` here,
589
- * not to `subscribe`: a subscription that starts after a `seq` gives the fold an incomplete log,
599
+ * block is answered or closed later. `after` only skips emitting the changes up to that `sequence`, so a
600
+ * client that has the timeline up to a `sequence` receives only what changed since. Pass `after` here,
601
+ * not to `subscribe`: a subscription that starts after a `sequence` gives the fold an incomplete log,
590
602
  * and the changes it produces are wrong.
591
603
  *
592
604
  * Each chunk of entries becomes one change; the stored entries, for example, arrive as one chunk.
@@ -605,7 +617,7 @@ function changesWith(entries, _definition, options) {
605
617
  * import * as Schema from "effect/Schema"
606
618
  * import * as Stream from "effect/Stream"
607
619
  *
608
- * class Summary extends Schema.TaggedClass<Summary>()("Summary", { text: Schema.String, throughSeq: Schema.Int }) {}
620
+ * class Summary extends Schema.TaggedClass<Summary>()("Summary", { text: Schema.String, throughSequence: Schema.Int }) {}
609
621
  * class DailyBudgetReached extends Schema.TaggedClass<DailyBudgetReached>()("DailyBudgetReached", {}) {}
610
622
  *
611
623
  * const MyAssistant = Assistant.make("my-assistant", { events: [Summary, DailyBudgetReached] })