effect-assistant 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/dist/Assistant.d.ts +10 -10
  2. package/dist/Assistant.d.ts.map +1 -1
  3. package/dist/Assistant.js +1 -1
  4. package/dist/Assistant.js.map +1 -1
  5. package/dist/AssistantActivity.d.ts +2 -2
  6. package/dist/AssistantActivity.d.ts.map +1 -1
  7. package/dist/AssistantActivity.js +5 -5
  8. package/dist/AssistantActivity.js.map +1 -1
  9. package/dist/AssistantEngine.d.ts +3 -3
  10. package/dist/AssistantEngine.js +2 -2
  11. package/dist/AssistantEngine.js.map +1 -1
  12. package/dist/AssistantEvent.d.ts +8 -8
  13. package/dist/AssistantEvent.js +7 -7
  14. package/dist/AssistantEvent.js.map +1 -1
  15. package/dist/AssistantLoop.d.ts +2 -2
  16. package/dist/AssistantLoop.js.map +1 -1
  17. package/dist/ChatLog.d.ts +9 -9
  18. package/dist/ChatLog.d.ts.map +1 -1
  19. package/dist/ChatLog.js +21 -21
  20. package/dist/ChatLog.js.map +1 -1
  21. package/dist/Compaction.d.ts +10 -10
  22. package/dist/Compaction.d.ts.map +1 -1
  23. package/dist/Compaction.js +16 -16
  24. package/dist/Compaction.js.map +1 -1
  25. package/dist/Memory.d.ts +1 -1
  26. package/dist/Memory.js +1 -1
  27. package/dist/Memory.js.map +1 -1
  28. package/dist/Skill.d.ts +1 -1
  29. package/dist/Skill.js +1 -1
  30. package/dist/Skill.js.map +1 -1
  31. package/dist/Specialist.d.ts +1 -1
  32. package/dist/Specialist.js +1 -1
  33. package/dist/Specialist.js.map +1 -1
  34. package/dist/Timeline.d.ts +9 -9
  35. package/dist/Timeline.js +16 -16
  36. package/dist/Timeline.js.map +1 -1
  37. package/dist/ToolApproval.d.ts +1 -1
  38. package/dist/ToolApproval.js +1 -1
  39. package/dist/ToolApproval.js.map +1 -1
  40. package/dist/ToolDiscovery.d.ts +1 -1
  41. package/dist/ToolDiscovery.js +1 -1
  42. package/dist/ToolDiscovery.js.map +1 -1
  43. package/dist/internal/chat.js +4 -4
  44. package/dist/internal/chat.js.map +1 -1
  45. package/dist/internal/events.js +1 -1
  46. package/dist/internal/events.js.map +1 -1
  47. package/dist/internal/history.js +23 -23
  48. package/dist/internal/history.js.map +1 -1
  49. package/dist/internal/loop.d.ts +1 -1
  50. package/dist/internal/loop.js +4 -4
  51. package/dist/internal/loop.js.map +1 -1
  52. package/dist/internal/turns.d.ts.map +1 -1
  53. package/dist/internal/turns.js +18 -18
  54. package/dist/internal/turns.js.map +1 -1
  55. package/package.json +1 -1
  56. package/src/Assistant.ts +11 -11
  57. package/src/AssistantActivity.ts +9 -7
  58. package/src/AssistantEngine.ts +3 -3
  59. package/src/AssistantEvent.ts +8 -8
  60. package/src/AssistantLoop.ts +3 -3
  61. package/src/ChatLog.ts +31 -26
  62. package/src/Compaction.ts +21 -21
  63. package/src/Memory.ts +1 -1
  64. package/src/Skill.ts +1 -1
  65. package/src/Specialist.ts +1 -1
  66. package/src/Timeline.ts +22 -22
  67. package/src/ToolApproval.ts +1 -1
  68. package/src/ToolDiscovery.ts +1 -1
  69. package/src/internal/chat.ts +4 -4
  70. package/src/internal/events.ts +1 -1
  71. package/src/internal/history.ts +32 -28
  72. package/src/internal/loop.ts +5 -5
  73. package/src/internal/turns.ts +28 -26
@@ -1 +1 @@
1
- {"version":3,"file":"Timeline.js","names":["AssistantEvent.Attachment","AssistantEvent.ToolOutcome","AppEvents.tagOf","AssistantActivity.empty","AppEvents.decodeWith","AssistantActivity.apply","AssistantEvent.isLibraryEvent","BlockKit.apply","ToolApproval.apply"],"sources":["../src/Timeline.ts"],"sourcesContent":["/**\n * This module provides a chat's timeline: the chat as its user sees it. A chat client receives the\n * timeline and nothing else.\n *\n * A {@link Timeline} holds the chat's items: the messages with their blocks, the app's own events\n * it shows (each typed with its event, see {@link EventItem}), and the tool calls waiting for the\n * user's approval ({@link ApprovalItem}). It also holds the chat's\n * {@link Activity}, and whether the last turn failed or the chat was deleted. It never contains the\n * model's output, tool calls or results, what the model read, or why a turn failed. The server\n * streams {@link Change}s by piping the assistant's `subscribe` through {@link changes}, and the\n * client folds them with {@link fold} (or {@link apply}, from {@link empty}). On the server,\n * {@link fromEntries} builds a whole timeline from a chat's log, and {@link snapshot} the whole\n * timeline as one change.\n *\n * **Only the app events in `show` reach the timeline**, and none by default: an event that is only\n * for the assistant, like a summary of the chat, stays out. The RPC's schema, `Change([...])`,\n * names the same events and types the client's timeline with them.\n *\n * @since 0.3.0\n */\nimport { dual } from \"effect/Function\"\nimport * as Match from \"effect/Match\"\nimport * as Option from \"effect/Option\"\nimport * as Predicate from \"effect/Predicate\"\nimport * as Schema from \"effect/Schema\"\nimport * as SchemaTransformation from \"effect/SchemaTransformation\"\nimport * as Stream from \"effect/Stream\"\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 { InputId } from \"./InputId.ts\"\nimport * as AppEvents from \"./internal/events.ts\"\nimport * as ToolApproval from \"./ToolApproval.ts\"\nimport { toJson } from \"./internal/schema.ts\"\n\n/**\n * Schema for the status of a block. A block is `\"open\"` while it accepts an answer, `\"answered\"`\n * once the user has used it, and `\"closed\"` when it closed without an answer because the user\n * answered in words or moved on.\n *\n * @category schemas\n * @since 0.1.0\n */\nexport const BlockStatus = Schema.Literals([\"open\", \"answered\", \"closed\"])\n\n/**\n * The status of a block.\n *\n * @category models\n * @since 0.1.0\n */\nexport type BlockStatus = typeof BlockStatus.Type\n\n/**\n * Schema for a block on an assistant's message. It holds the block's kind, its props (JSON, as\n * the block's props schema encodes them), its status, and the user's answer (JSON, as the block's\n * answer schema encodes it).\n *\n * @category schemas\n * @since 0.1.0\n */\nexport const Block = Schema.Struct({\n kind: Schema.String,\n props: Schema.Json,\n status: BlockStatus,\n answer: Schema.optional(Schema.Json),\n /** For a block that shows a tool's result (`Block.fromToolResult`): the result, as the tool's success schema encodes it. */\n result: Schema.optional(Schema.Json)\n})\n\n/**\n * Represents a block on an assistant's message.\n *\n * @category models\n * @since 0.1.0\n */\nexport type Block = typeof Block.Type\n\n/**\n * Schema for a message the user sent. `at` is when it was stored, in epoch\n * milliseconds.\n *\n * @category schemas\n * @since 0.1.0\n */\nexport const UserMessage = Schema.TaggedStruct(\"UserMessage\", {\n id: InputId,\n text: Schema.String,\n attachments: Schema.optional(Schema.Array(AssistantEvent.Attachment)),\n at: Schema.Finite\n})\n\n/**\n * Represents a message the user sent.\n *\n * @category models\n * @since 0.1.0\n */\nexport type UserMessage = typeof UserMessage.Type\n\n/**\n * Schema for a message the assistant sent the user, including its block if it has one.\n *\n * @category schemas\n * @since 0.1.0\n */\nexport const AssistantMessage = Schema.TaggedStruct(\"AssistantMessage\", {\n id: Schema.String,\n text: Schema.String,\n at: Schema.Finite,\n block: Schema.optional(Block)\n})\n\n/**\n * Represents a message the assistant sent the user.\n *\n * @category models\n * @since 0.1.0\n */\nexport type AssistantMessage = typeof AssistantMessage.Type\n\n/**\n * Represents one of the app's own events in the timeline: an item between the messages, in the\n * order the chat stored it.\n *\n * **Details**\n *\n * The item's `_tag` is the event's, so a match over a timeline's items takes a handler per event\n * beside `UserMessage` and `AssistantMessage`. The event, decoded with its schema, is in `event`,\n * and the item's own fields sit beside it rather than spread into it: an event may have an `id` or\n * an `at` of its own, and a class instance keeps its prototype. `id` is fixed per entry of the\n * chat's log, and `at` is when the event was stored, in epoch milliseconds.\n *\n * @category models\n * @since 0.3.0\n */\nexport type EventItem<E> = E extends { readonly _tag: infer Tag extends string }\n ? { readonly _tag: Tag; readonly id: string; readonly at: number; readonly event: E }\n : never\n\n/**\n * Schema for a tool call that waits, or waited, for the user's approval, as an item on the\n * timeline.\n *\n * **Details**\n *\n * It is only the yes or no: the assistant's own message tells the user what it asks them to\n * approve, so the item carries no params. `id` is the call's id, which the decision is for (see\n * `ToolApproval.approve`), and `tool` the tool's name, for a label. `status` is where the call stands\n * (`ToolApproval.Status`); `reason` is the user's, when they rejected it with one; `outcome` is how\n * the approved call went, once it ran. `at` is when the call started waiting, in epoch\n * milliseconds. A change updates the item in place as the call is decided and run.\n *\n * @category schemas\n * @since 0.4.0\n */\nexport const ApprovalItem = Schema.TaggedStruct(\"ToolApproval\", {\n id: Schema.String,\n tool: Schema.String,\n status: Schema.Literals([\"pending\", \"approved\", \"rejected\", \"expired\"]),\n reason: Schema.optional(Schema.String),\n outcome: Schema.optional(AssistantEvent.ToolOutcome),\n at: Schema.Finite\n})\n\n/**\n * Represents a tool call that waits, or waited, for the user's approval, as an item on the\n * timeline.\n *\n * @category models\n * @since 0.4.0\n */\nexport type ApprovalItem = typeof ApprovalItem.Type\n\n/**\n * Represents an item in the chat's timeline: a message the user or the assistant sent, one of the\n * app's own events it shows (see {@link EventItem}), or a tool call waiting for the user's approval\n * (see {@link ApprovalItem}).\n *\n * **Example** (Showing each item of a chat)\n *\n * ```ts\n * import { Timeline } from \"effect-assistant\"\n * import * as Match from \"effect/Match\"\n * import * as Schema from \"effect/Schema\"\n *\n * class DailyBudgetReached extends Schema.TaggedClass<DailyBudgetReached>()(\"DailyBudgetReached\", {}) {}\n *\n * const line = (item: Timeline.Item<DailyBudgetReached>) =>\n * Match.valueTags(item, {\n * UserMessage: (sent) => `You: ${sent.text}`,\n * AssistantMessage: (sent) => `Assistant: ${sent.text}`,\n * DailyBudgetReached: () => \"You've reached today's budget.\"\n * })\n * ```\n *\n * @category models\n * @since 0.3.0\n */\nexport type Item<Events = never> = UserMessage | AssistantMessage | EventItem<Events> | ApprovalItem\n\n/**\n * Schema for a specialist that is working for the assistant. It holds the specialist's name and\n * when it started, in epoch milliseconds.\n *\n * @category schemas\n * @since 0.1.0\n */\nexport const SpecialistWorking = Schema.Struct({\n specialist: Schema.String,\n since: Schema.Finite\n})\n\n/**\n * Represents a specialist that is working for the assistant.\n *\n * @category models\n * @since 0.1.0\n */\nexport type SpecialistWorking = typeof SpecialistWorking.Type\n\n/**\n * Schema for what is working in the chat.\n *\n * `working` is true while the assistant runs a turn, or when a turn is about to start for\n * something that arrived. `specialists` lists the specialists that are working. When the chat is\n * idle, nothing is working, even if a workflow is waiting on a timer or on the user.\n *\n * @category schemas\n * @since 0.1.0\n */\nexport const Activity = Schema.Struct({\n working: Schema.Boolean,\n specialists: Schema.Array(SpecialistWorking)\n})\n\n/**\n * Represents what is working in the chat.\n *\n * @category models\n * @since 0.1.0\n */\nexport type Activity = typeof Activity.Type\n\n/**\n * Represents a chat's timeline: the chat as its user sees it, typed with the app's events it shows.\n * It holds the chat's items in order, what is working in it, whether the last turn failed, and\n * whether the chat was deleted. `seq` is the position in the chat's log that the timeline is up to.\n *\n * @category models\n * @since 0.3.0\n */\nexport interface Timeline<Events = never> {\n readonly seq: number\n readonly items: ReadonlyArray<Item<Events>>\n readonly activity: Activity\n /** Whether the last turn failed with no message after it. */\n readonly failed: boolean\n /** Whether the chat was deleted. A deleted chat accepts nothing more, and its stream ends. */\n readonly deleted: boolean\n}\n\n/**\n * Represents a change to a chat's timeline, typed with the app's events it shows.\n *\n * **Details**\n *\n * A change holds each item that is new or changed (for example, a message whose block was\n * answered or closed, or a call the user approved), in full. It also holds the current activity, whether the last turn failed,\n * and whether the chat was deleted. `seq` is the position in the chat's log that the change is up\n * to.\n *\n * @category models\n * @since 0.3.0\n */\nexport interface Change<Events = never> {\n readonly seq: number\n readonly items: ReadonlyArray<Item<Events>>\n readonly activity: Activity\n readonly failed: boolean\n readonly deleted: boolean\n}\n\nconst eventItem = (schema: AssistantEvent.AppEventSchema) =>\n Schema.Struct({ _tag: Schema.Literal(AppEvents.tagOf(schema)), id: Schema.String, at: Schema.Finite, event: schema })\n\nfunction makeChange<const Events extends ReadonlyArray<AssistantEvent.AppEventSchema> = readonly []>(\n events?: Events\n): Schema.Codec<Change<Events[number][\"Type\"]>, Schema.Json>\n// The overload states the decoded type: an item per given schema, each typed with its own event.\nfunction makeChange(events: ReadonlyArray<AssistantEvent.AppEventSchema> = []): Schema.Top {\n const AnyItem = Schema.toCodecJson(\n Schema.Union([UserMessage, AssistantMessage, ApprovalItem, ...events.map(eventItem)])\n )\n\n const decodeItem = Schema.decodeUnknownOption(AnyItem)\n const encodeItem = Schema.encodeUnknownSync(AnyItem)\n\n return Schema.Struct({\n seq: Schema.Int,\n // An item for an event this schema doesn't name, or that doesn't decode (from a newer server, say), is left out\n // rather than failing the whole change.\n items: Schema.Array(Schema.Json).pipe(\n Schema.decodeTo(\n Schema.Array(Schema.toType(AnyItem)),\n SchemaTransformation.transform<ReadonlyArray<typeof AnyItem.Type>, ReadonlyArray<Schema.Json>>({\n decode: (json) => json.flatMap((item) => Option.toArray(decodeItem(item))),\n encode: (items) => items.map((item) => encodeItem(item))\n })\n )\n ),\n activity: Activity,\n failed: Schema.Boolean,\n deleted: Schema.Boolean\n })\n}\n\n/**\n * Creates the schema for the changes to a chat's timeline, with items for the given app events,\n * in JSON.\n *\n * **Details**\n *\n * It is the success schema of the RPC that streams a chat's timeline: name the events the chat's\n * users see, the same ones the handler passes to {@link changes} as `show`. Tool calls waiting for\n * approval are always in it, as {@link ApprovalItem}s. Each item decodes its\n * event with that event's schema, so the client gets `Change<Events>` typed with them and folds it\n * with {@link fold}, no definition needed. `Change()` carries no app events.\n *\n * A handler whose changes show an event the schema doesn't name doesn't compile.\n *\n * **Example** (An RPC that streams a chat's timeline, with the budget notice)\n *\n * ```ts\n * import { ChatId, Timeline } from \"effect-assistant\"\n * import * as Rpc from \"effect/rpc/Rpc\"\n * import * as Schema from \"effect/Schema\"\n *\n * class DailyBudgetReached extends Schema.TaggedClass<DailyBudgetReached>()(\"DailyBudgetReached\", {}) {}\n *\n * const Subscribe = Rpc.make(\"Subscribe\", {\n * payload: { chatId: ChatId.ChatId },\n * success: Timeline.Change([DailyBudgetReached]),\n * stream: true\n * })\n * ```\n *\n * @category schemas\n * @since 0.3.0\n */\nexport const Change: <const Events extends ReadonlyArray<AssistantEvent.AppEventSchema> = readonly []>(\n events?: Events\n) => Schema.Codec<Change<Events[number][\"Type\"]>, Schema.Json> = makeChange\n\n/**\n * The timeline of a chat before anything has happened in it, with no items and nothing working.\n * Use it as the starting point when folding changes with {@link apply}.\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const empty: Timeline = {\n seq: 0,\n items: [],\n activity: { working: false, specialists: [] },\n failed: false,\n deleted: false\n}\n\n/**\n * Returns the timeline after a change.\n *\n * **Details**\n *\n * Each item in the change replaces the item with the same id, in place, and new items are added\n * after the rest. The change's activity, `failed`, and `deleted` replace the timeline's. Applying a\n * change twice changes nothing, so a client that resubscribes from the start can fold again and get\n * the same timeline.\n *\n * The timeline is typed with the change's events, as its RPC's schema decoded them (see\n * {@link Change}). To fold a stream of changes, use {@link fold}: in\n * `Stream.scan(() => Timeline.empty, Timeline.apply)`, TypeScript fixes the state's type from\n * `empty` before it sees the changes' events, so that form needs the timeline's type written out.\n *\n * **Example** (Folding changes into your own state)\n *\n * ```ts\n * import { Timeline } from \"effect-assistant\"\n *\n * interface State<Events> {\n * readonly timeline: Timeline.Timeline<Events>\n * readonly unread: number\n * }\n *\n * const next = <Events>(state: State<Events>, change: Timeline.Change<Events>): State<Events> => ({\n * timeline: Timeline.apply(state.timeline, change),\n * unread: state.unread + change.items.length\n * })\n * ```\n *\n * @category combinators\n * @since 0.1.0\n */\nexport const apply = <Events>(timeline: Timeline<Events>, change: Change<Events>): Timeline<Events> => {\n const changed = new Map(change.items.map((item) => [item.id, item]))\n const known = new Set(timeline.items.map((item) => item.id))\n\n return {\n seq: Math.max(timeline.seq, change.seq),\n items: [\n ...timeline.items.map((item) => changed.get(item.id) ?? item),\n ...change.items.filter((item) => !known.has(item.id))\n ],\n activity: change.activity,\n failed: change.failed,\n deleted: change.deleted\n }\n}\n\n/**\n * Returns the timelines a stream of changes folds to, from {@link empty}, typed with the changes'\n * events.\n *\n * **Example** (A chat's timeline on the client)\n *\n * ```ts\n * import { Timeline } from \"effect-assistant\"\n * import * as Schema from \"effect/Schema\"\n * import * as Stream from \"effect/Stream\"\n *\n * class DailyBudgetReached extends Schema.TaggedClass<DailyBudgetReached>()(\"DailyBudgetReached\", {}) {}\n *\n * // The stream of an RPC whose success schema is `Timeline.Change([DailyBudgetReached])`.\n * declare const changes: Stream.Stream<Timeline.Change<DailyBudgetReached>>\n *\n * const timelines = changes.pipe(Timeline.fold) // Stream<Timeline.Timeline<DailyBudgetReached>>\n * ```\n *\n * @category combinators\n * @since 0.3.0\n */\nexport const fold = <Events, E, R>(\n changes: Stream.Stream<Change<Events>, E, R>\n): Stream.Stream<Timeline<Events>, E, R> => changes.pipe(Stream.scan((): Timeline<Events> => empty, apply))\n\n/**\n * Returns whether nothing is working in the chat: no turn is running or about to start, and no\n * specialist is working.\n *\n * @category guards\n * @since 0.1.0\n */\nexport const isIdle = <Events>(timeline: Timeline<Events>): boolean =>\n !timeline.activity.working && timeline.activity.specialists.length === 0\n\ninterface AnyEventItem {\n readonly _tag: string\n readonly id: string\n readonly at: number\n readonly event: { readonly _tag: string }\n}\n\ntype AnyItem = UserMessage | AssistantMessage | AnyEventItem | ApprovalItem\n\n// What the fold below builds: the overloads that use it type its items with the events shown.\ninterface AnyChange {\n readonly seq: number\n readonly items: ReadonlyArray<AnyItem>\n readonly activity: Activity\n readonly failed: boolean\n readonly deleted: boolean\n}\n\n/**\n * Represents the options for building a chat's timeline from its log.\n *\n * @category models\n * @since 0.3.0\n */\nexport interface ShowOptions<Show extends ReadonlyArray<AssistantEvent.AppEventSchema>> {\n /**\n * The schemas of the app's events the chat's users see. Each becomes an item in the timeline;\n * the app's other events are left out. Defaults to none.\n */\n readonly show?: Show | undefined\n}\n\n/**\n * Represents the schemas an assistant's timeline may show: some of its own events.\n *\n * @category utility types\n * @since 0.3.0\n */\nexport type Shown<Events> = ReadonlyArray<AssistantEvent.AppEventSchema & { readonly Type: Events }>\n\nfunction snapshotWith(entries: ReadonlyArray<AssistantEvent.AnyEntry>): Change\nfunction snapshotWith<Events, const Show extends Shown<Events> = readonly []>(\n entries: ReadonlyArray<AssistantEvent.AnyEntry>,\n definition: Definition<Events>,\n options?: ShowOptions<Show>\n): Change<Show[number][\"Type\"]>\nfunction snapshotWith(\n entries: ReadonlyArray<AssistantEvent.AnyEntry>,\n _definition?: Schemas,\n options?: ShowOptions<ReadonlyArray<AssistantEvent.AppEventSchema>>\n): AnyChange {\n const timeline = folding(options?.show ?? [])\n\n for (const entry of entries) timeline.step(entry)\n\n return timeline.snapshot\n}\n\n/**\n * Returns a chat's whole timeline as one change, built from the entries of its log.\n *\n * **When to use**\n *\n * Use it on the server for an RPC that returns a whole chat at once: its success schema is\n * {@link Change} with the same events as `show`, and the client applies it to {@link empty}.\n *\n * **Details**\n *\n * Only the app's events in `show` become items; without it, the timeline has none.\n *\n * @category constructors\n * @since 0.3.0\n */\nexport const snapshot: {\n (entries: ReadonlyArray<AssistantEvent.AnyEntry>): Change\n <Events, const Show extends Shown<Events> = readonly []>(\n entries: ReadonlyArray<AssistantEvent.AnyEntry>,\n definition: Definition<Events>,\n options?: ShowOptions<Show>\n ): Change<Show[number][\"Type\"]>\n} = snapshotWith\n\nfunction fromEntriesWith(entries: ReadonlyArray<AssistantEvent.AnyEntry>): Timeline\nfunction fromEntriesWith<Events, const Show extends Shown<Events> = readonly []>(\n entries: ReadonlyArray<AssistantEvent.AnyEntry>,\n definition: Definition<Events>,\n options?: ShowOptions<Show>\n): Timeline<Show[number][\"Type\"]>\nfunction fromEntriesWith(\n entries: ReadonlyArray<AssistantEvent.AnyEntry>,\n _definition?: Schemas,\n options?: ShowOptions<ReadonlyArray<AssistantEvent.AppEventSchema>>\n): AnyChange {\n const timeline = folding(options?.show ?? [])\n\n for (const entry of entries) timeline.step(entry)\n\n return timeline.snapshot\n}\n\n/**\n * Returns a chat's timeline, built from the entries of its log.\n *\n * **When to use**\n *\n * Use it in server-side code that reads a whole chat. A client gets the timeline as a stream from\n * {@link changes} instead, and an RPC that returns a whole chat sends a {@link snapshot}.\n *\n * **Details**\n *\n * Only the app's events in `show` become items, typed with their schemas; without it, the timeline\n * has none.\n *\n * **Example** (Building a chat's timeline from its log)\n *\n * ```ts\n * import { type ChatId, ChatLog, Timeline } from \"effect-assistant\"\n * import * as Effect from \"effect/Effect\"\n *\n * const timelineOf = (chatId: ChatId.ChatId) =>\n * ChatLog.ChatLog.use((log) => log.read(chatId)).pipe(\n * Effect.map((entries) => Timeline.fromEntries(entries))\n * )\n * ```\n *\n * @see {@link changes} for a stream of changes instead of a whole timeline\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const fromEntries: {\n (entries: ReadonlyArray<AssistantEvent.AnyEntry>): Timeline\n <Events, const Show extends Shown<Events> = readonly []>(\n entries: ReadonlyArray<AssistantEvent.AnyEntry>,\n definition: Definition<Events>,\n options?: ShowOptions<Show>\n ): Timeline<Show[number][\"Type\"]>\n} = fromEntriesWith\n\ninterface Schemas {\n readonly events: ReadonlyArray<AssistantEvent.AppEventSchema>\n}\n\ninterface Folding {\n readonly step: (entry: AssistantEvent.AnyEntry) => AnyChange | undefined\n readonly snapshot: AnyChange\n}\n\nconst activityOf = (activity: AssistantActivity.State): Activity => ({\n working: activity.turnSince !== undefined || activity.queued,\n specialists: [...activity.specialists.values()].map((run) => ({\n specialist: run.specialist,\n since: run.since\n }))\n})\n\nconst sameActivity = (a: Activity, b: Activity) =>\n a.working === b.working &&\n a.specialists.length === b.specialists.length &&\n a.specialists.every((run, i) => {\n const other = b.specialists[i]\n\n return other !== undefined && run.specialist === other.specialist && run.since === other.since\n })\n\nconst folding = (show: ReadonlyArray<AssistantEvent.AppEventSchema>): Folding => {\n const shown = new Map(show.map((schema) => [AppEvents.tagOf(schema), schema]))\n let approvals: ReadonlyMap<string, ToolApproval.State> = new Map()\n let seq = 0\n let failed = false\n let deleted = false\n let activity = AssistantActivity.empty\n let blocks: ReadonlyMap<string, BlockKit.State> = new Map()\n // The items in order, and the assistant's messages as stored: their blocks change as the chat goes on.\n const items = new Map<string, AnyItem>()\n const sent = new Map<string, AssistantEvent.AssistantMessage>()\n // The results a block can show, by id: a message shows one stored before it.\n const results = new Map<string, Schema.Json>()\n\n const blockOf = (message: AssistantEvent.AssistantMessage): Block | undefined => {\n if (message.block === undefined) return undefined\n\n const state = blocks.get(message.id)\n const status = state === undefined || state.open ? \"open\" : state.answer === undefined ? \"closed\" : \"answered\"\n const block: Block = { kind: message.block.kind, props: toJson(message.block.props), status }\n const { resultId } = message.block\n const result = resultId === undefined ? undefined : results.get(resultId)\n const shown = result === undefined ? block : { ...block, result }\n\n return state?.answer === undefined ? shown : { ...shown, answer: toJson(state.answer.value) }\n }\n\n const itemOf = (message: AssistantEvent.AssistantMessage, at: number): AssistantMessage => {\n const block = blockOf(message)\n\n return block === undefined\n ? AssistantMessage.make({ id: message.id, text: message.text, at })\n : AssistantMessage.make({ id: message.id, text: message.text, at, block })\n }\n\n const touched = (event: AssistantEvent.AssistantEvent, at: number): ReadonlyArray<string> =>\n Match.value(event).pipe(\n Match.tagsExhaustive({\n UserMessage: (message) => {\n items.set(\n message.id,\n message.attachments === undefined\n ? UserMessage.make({ id: message.id, text: message.text, at })\n : UserMessage.make({\n id: message.id,\n text: message.text,\n attachments: message.attachments,\n at\n })\n )\n failed = false\n\n return [message.id]\n },\n AssistantMessage: (message) => {\n sent.set(message.id, message)\n items.set(message.id, itemOf(message, at))\n failed = false\n\n return [message.id, ...(message.closes ?? [])]\n },\n BlockAnswer: (answer) => {\n failed = false\n\n return [answer.messageId]\n },\n BlocksClosed: (closed) => closed.messageIds,\n ApprovalRequested: () => [],\n ApprovalDecision: () => {\n failed = false\n\n return []\n },\n ApprovalsExpired: () => [],\n TurnFailed: () => {\n failed = true\n\n return []\n },\n ModelOutput: () => [],\n ToolStarted: () => [],\n ToolResult: (result) => {\n if (result.resultId !== undefined && result.outcome === \"success\") {\n results.set(result.resultId, result.full ?? result.result)\n }\n\n return []\n },\n TurnStarted: () => [],\n TurnCompleted: () => [],\n SpecialistStarted: () => [],\n SpecialistFinished: () => [],\n ChatDeleted: () => {\n deleted = true\n\n return []\n }\n })\n )\n\n // Decoded as `subscribe` streams it, or as the log stores it; one that doesn't decode is left out.\n const eventItemOf = (entry: AssistantEvent.AnyEntry): Option.Option<AnyEventItem> => {\n const schema = shown.get(entry.event._tag)\n\n if (schema === undefined) return Option.none()\n\n const event = Schema.is(schema)(entry.event) ? Option.some(entry.event) : AppEvents.decodeWith(schema, entry.event)\n\n return Option.map(event, (decoded) => ({\n _tag: decoded._tag,\n id: `app-event:${entry.seq}`,\n at: entry.at,\n event: decoded\n }))\n }\n\n const callsOf = (event: AssistantEvent.AssistantEvent): ReadonlyArray<string> =>\n Predicate.isTagged(event, \"ApprovalsExpired\")\n ? event.callIds\n : Predicate.isTagged(event, \"ApprovalRequested\") ||\n Predicate.isTagged(event, \"ApprovalDecision\") ||\n Predicate.isTagged(event, \"ToolResult\")\n ? [event.callId]\n : []\n\n // The items of the calls this entry changed.\n const approvalItems = (event: AssistantEvent.AssistantEvent, before: ReadonlyMap<string, ToolApproval.State>) =>\n callsOf(event).flatMap((callId) => {\n const state = approvals.get(callId)\n\n if (state === undefined || state === before.get(callId)) return []\n\n const item = ApprovalItem.make({\n id: callId,\n tool: state.request.tool,\n status: state.status,\n reason: state.reason,\n outcome: state.outcome,\n at: state.at\n })\n\n items.set(callId, item)\n\n return [item]\n })\n\n const step = (entry: AssistantEvent.AnyEntry): AnyChange | undefined => {\n const previous = { activity: activityOf(activity), failed, deleted }\n const { event } = entry\n\n seq = entry.seq\n activity = AssistantActivity.apply(activity, entry)\n\n if (!AssistantEvent.isLibraryEvent(event)) {\n const now = activityOf(activity)\n\n return Option.match(eventItemOf(entry), {\n onNone: () =>\n sameActivity(previous.activity, now) ? undefined : { seq, items: [], activity: now, failed, deleted },\n onSome: (item) => {\n items.set(item.id, item)\n\n return { seq, items: [item], activity: now, failed, deleted }\n }\n })\n }\n\n blocks = BlockKit.apply(blocks, event)\n\n const decidedBefore = approvals\n approvals = ToolApproval.apply(approvals, entry)\n\n const changed = [...new Set(touched(event, entry.at))].flatMap((id) => {\n const message = sent.get(id)\n const current = items.get(id)\n\n if (message === undefined || current === undefined) return current === undefined ? [] : [current]\n\n const next = itemOf(message, current.at)\n items.set(id, next)\n\n return [next]\n })\n\n const all = [...changed, ...approvalItems(event, decidedBefore)]\n const now = activityOf(activity)\n\n return all.length === 0 &&\n failed === previous.failed &&\n deleted === previous.deleted &&\n sameActivity(previous.activity, now)\n ? undefined\n : { seq, items: all, activity: now, failed, deleted }\n }\n\n return {\n step,\n get snapshot() {\n return { seq, items: [...items.values()], activity: activityOf(activity), failed, deleted }\n }\n }\n}\n\nconst merge = (changes: ReadonlyArray<AnyChange>): AnyChange | undefined => {\n const last = changes.at(-1)\n\n if (last === undefined) return undefined\n\n const items = new Map<string, AnyItem>()\n\n for (const change of changes) for (const item of change.items) items.set(item.id, item)\n\n return {\n seq: last.seq,\n items: [...items.values()],\n activity: last.activity,\n failed: last.failed,\n deleted: last.deleted\n }\n}\n\n/**\n * Represents an assistant's definition as the timeline reads it. An assistant made with\n * `Assistant.make` fits it.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface Definition<Events = never> {\n /** The schemas of the assistant's events. */\n readonly events: ReadonlyArray<AssistantEvent.AppEventSchema>\n /** The schema of the chat's entries, typed with the assistant's events: it types `show`. */\n readonly Entry: Schema.Codec<Assistant.Entry<Events>, Schema.Json>\n}\n\n/**\n * Represents the options accepted by {@link changes}.\n *\n * @category models\n * @since 0.3.0\n */\nexport interface ChangesOptions<\n Show extends ReadonlyArray<AssistantEvent.AppEventSchema> = readonly []\n> extends ShowOptions<Show> {\n /**\n * The `seq` up to which the client already has the timeline. The result contains only the\n * changes after it. Defaults to `0`, every change.\n */\n readonly after?: number | undefined\n}\n\nfunction changesWith<E, R, Events, const Show extends Shown<Events> = readonly []>(\n entries: Stream.Stream<AssistantEvent.AnyEntry, E, R>,\n definition: Definition<Events>,\n options?: ChangesOptions<Show>\n): Stream.Stream<Change<Show[number][\"Type\"]>, E, R>\nfunction changesWith<E, R>(\n entries: Stream.Stream<AssistantEvent.AnyEntry, E, R>,\n _definition: Schemas,\n options?: ChangesOptions<ReadonlyArray<AssistantEvent.AppEventSchema>>\n): Stream.Stream<AnyChange, E, R> {\n const after = options?.after ?? 0\n\n return entries.pipe(\n Stream.mapAccumArray(\n () => folding(options?.show ?? []),\n (timeline, chunk) => {\n const changed = chunk.flatMap((entry) => {\n const change = timeline.step(entry)\n\n return change === undefined || entry.seq <= after ? [] : [change]\n })\n\n const merged = merge(changed)\n\n return [timeline, merged === undefined ? [] : [merged]]\n }\n )\n )\n}\n\n/**\n * Returns the stream of changes to a chat's timeline that its entries produce, for a chat client.\n *\n * **Details**\n *\n * The entries must stream from the start of the chat's log, as `assistant.subscribe(chatId)` does\n * without `after`. The fold reads every entry, because a message depends on earlier entries: its\n * block is answered or closed later. `after` only skips emitting the changes up to that `seq`, so a\n * client that has the timeline up to a `seq` receives only what changed since. Pass `after` here,\n * not to `subscribe`: a subscription that starts after a `seq` gives the fold an incomplete log,\n * and the changes it produces are wrong.\n *\n * Each chunk of entries becomes one change; the stored entries, for example, arrive as one chunk.\n *\n * **Only the app's events in `show` reach the client.** Each one becomes an item between the\n * messages (see {@link EventItem}); the app's other events, like a summary of the chat for the\n * model, are left out. Without `show`, none are shown. The changes are typed with the events shown,\n * so the RPC's success schema names the same ones (`Timeline.Change([...])`), and a handler that\n * shows one the schema doesn't name doesn't compile. A timeline shows a turn as working once it\n * starts, or from the user's input.\n *\n * **Example** (Streaming a chat's timeline to a client, with the one app event its user sees)\n *\n * ```ts\n * import { Assistant, type ChatId, Timeline } from \"effect-assistant\"\n * import * as Schema from \"effect/Schema\"\n * import * as Stream from \"effect/Stream\"\n *\n * class Summary extends Schema.TaggedClass<Summary>()(\"Summary\", { text: Schema.String, throughSeq: Schema.Int }) {}\n * class DailyBudgetReached extends Schema.TaggedClass<DailyBudgetReached>()(\"DailyBudgetReached\", {}) {}\n *\n * const MyAssistant = Assistant.make(\"my-assistant\", { events: [Summary, DailyBudgetReached] })\n *\n * // The summary is for the model: only the budget notice reaches the user.\n * const watch = (chatId: ChatId.ChatId, after: number) =>\n * Stream.unwrap(\n * MyAssistant.useSync((assistant) =>\n * assistant.subscribe(chatId).pipe(Timeline.changes(MyAssistant, { show: [DailyBudgetReached], after }))\n * )\n * )\n * ```\n *\n * @see {@link fold} for folding the changes into a timeline on the client\n *\n * @category utils\n * @since 0.3.0\n */\nexport const changes: {\n <Events, const Show extends Shown<Events> = readonly []>(\n definition: Definition<Events>,\n options?: ChangesOptions<Show>\n ): <E, R>(entries: Stream.Stream<AssistantEvent.AnyEntry, E, R>) => Stream.Stream<Change<Show[number][\"Type\"]>, E, R>\n <E, R, Events, const Show extends Shown<Events> = readonly []>(\n entries: Stream.Stream<AssistantEvent.AnyEntry, E, R>,\n definition: Definition<Events>,\n options?: ChangesOptions<Show>\n ): Stream.Stream<Change<Show[number][\"Type\"]>, E, R>\n} = dual((args) => Stream.isStream(args[0]), changesWith)\n"],"mappings":"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"}
1
+ {"version":3,"file":"Timeline.js","names":["AssistantEvent.Attachment","AssistantEvent.ToolOutcome","AppEvents.tagOf","AssistantActivity.empty","AppEvents.decodeWith","AssistantActivity.apply","AssistantEvent.isLibraryEvent","BlockKit.apply","ToolApproval.apply"],"sources":["../src/Timeline.ts"],"sourcesContent":["/**\n * This module provides a chat's timeline: the chat as its user sees it. A chat client receives the\n * timeline and nothing else.\n *\n * A {@link Timeline} holds the chat's items: the messages with their blocks, the app's own events\n * it shows (each typed with its event, see {@link EventItem}), and the tool calls waiting for the\n * user's approval ({@link ApprovalItem}). It also holds the chat's\n * {@link Activity}, and whether the last turn failed or the chat was deleted. It never contains the\n * model's output, tool calls or results, what the model read, or why a turn failed. The server\n * streams {@link Change}s by piping the assistant's `subscribe` through {@link changes}, and the\n * client folds them with {@link fold} (or {@link apply}, from {@link empty}). On the server,\n * {@link fromEntries} builds a whole timeline from a chat's log, and {@link snapshot} the whole\n * timeline as one change.\n *\n * **Only the app events in `show` reach the timeline**, and none by default: an event that is only\n * for the assistant, like a summary of the chat, stays out. The RPC's schema, `Change([...])`,\n * names the same events and types the client's timeline with them.\n *\n * @since 0.3.0\n */\nimport { dual } from \"effect/Function\"\nimport * as Match from \"effect/Match\"\nimport * as Option from \"effect/Option\"\nimport * as Predicate from \"effect/Predicate\"\nimport * as Schema from \"effect/Schema\"\nimport * as SchemaTransformation from \"effect/SchemaTransformation\"\nimport * as Stream from \"effect/Stream\"\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 { InputId } from \"./InputId.ts\"\nimport * as AppEvents from \"./internal/events.ts\"\nimport * as ToolApproval from \"./ToolApproval.ts\"\nimport { toJson } from \"./internal/schema.ts\"\n\n/**\n * Schema for the status of a block. A block is `\"open\"` while it accepts an answer, `\"answered\"`\n * once the user has used it, and `\"closed\"` when it closed without an answer because the user\n * answered in words or moved on.\n *\n * @category schemas\n * @since 0.1.0\n */\nexport const BlockStatus = Schema.Literals([\"open\", \"answered\", \"closed\"])\n\n/**\n * The status of a block.\n *\n * @category models\n * @since 0.1.0\n */\nexport type BlockStatus = typeof BlockStatus.Type\n\n/**\n * Schema for a block on an assistant's message. It holds the block's kind, its props (JSON, as\n * the block's props schema encodes them), its status, and the user's answer (JSON, as the block's\n * answer schema encodes it).\n *\n * @category schemas\n * @since 0.1.0\n */\nexport const Block = Schema.Struct({\n kind: Schema.String,\n props: Schema.Json,\n status: BlockStatus,\n answer: Schema.optional(Schema.Json),\n /** For a block that shows a tool's result (`Block.fromToolResult`): the result, as the tool's success schema encodes it. */\n result: Schema.optional(Schema.Json)\n})\n\n/**\n * Represents a block on an assistant's message.\n *\n * @category models\n * @since 0.1.0\n */\nexport type Block = typeof Block.Type\n\n/**\n * Schema for a message the user sent. `at` is when it was stored, in epoch\n * milliseconds.\n *\n * @category schemas\n * @since 0.1.0\n */\nexport const UserMessage = Schema.TaggedStruct(\"UserMessage\", {\n id: InputId,\n text: Schema.String,\n attachments: Schema.optional(Schema.Array(AssistantEvent.Attachment)),\n at: Schema.Finite\n})\n\n/**\n * Represents a message the user sent.\n *\n * @category models\n * @since 0.1.0\n */\nexport type UserMessage = typeof UserMessage.Type\n\n/**\n * Schema for a message the assistant sent the user, including its block if it has one.\n *\n * @category schemas\n * @since 0.1.0\n */\nexport const AssistantMessage = Schema.TaggedStruct(\"AssistantMessage\", {\n id: Schema.String,\n text: Schema.String,\n at: Schema.Finite,\n block: Schema.optional(Block)\n})\n\n/**\n * Represents a message the assistant sent the user.\n *\n * @category models\n * @since 0.1.0\n */\nexport type AssistantMessage = typeof AssistantMessage.Type\n\n/**\n * Represents one of the app's own events in the timeline: an item between the messages, in the\n * order the chat stored it.\n *\n * **Details**\n *\n * The item's `_tag` is the event's, so a match over a timeline's items takes a handler per event\n * beside `UserMessage` and `AssistantMessage`. The event, decoded with its schema, is in `event`,\n * and the item's own fields sit beside it rather than spread into it: an event may have an `id` or\n * an `at` of its own, and a class instance keeps its prototype. `id` is fixed per entry of the\n * chat's log, and `at` is when the event was stored, in epoch milliseconds.\n *\n * @category models\n * @since 0.3.0\n */\nexport type EventItem<E> = E extends { readonly _tag: infer Tag extends string }\n ? { readonly _tag: Tag; readonly id: string; readonly at: number; readonly event: E }\n : never\n\n/**\n * Schema for a tool call that waits, or waited, for the user's approval, as an item on the\n * timeline.\n *\n * **Details**\n *\n * It is only the yes or no: the assistant's own message tells the user what it asks them to\n * approve, so the item carries no params. `id` is the call's id, which the decision is for (see\n * `ToolApproval.approve`), and `tool` the tool's name, for a label. `status` is where the call stands\n * (`ToolApproval.Status`); `reason` is the user's, when they rejected it with one; `outcome` is how\n * the approved call went, once it ran. `at` is when the call started waiting, in epoch\n * milliseconds. A change updates the item in place as the call is decided and run.\n *\n * @category schemas\n * @since 0.4.0\n */\nexport const ApprovalItem = Schema.TaggedStruct(\"ToolApproval\", {\n id: Schema.String,\n tool: Schema.String,\n status: Schema.Literals([\"pending\", \"approved\", \"rejected\", \"expired\"]),\n reason: Schema.optional(Schema.String),\n outcome: Schema.optional(AssistantEvent.ToolOutcome),\n at: Schema.Finite\n})\n\n/**\n * Represents a tool call that waits, or waited, for the user's approval, as an item on the\n * timeline.\n *\n * @category models\n * @since 0.4.0\n */\nexport type ApprovalItem = typeof ApprovalItem.Type\n\n/**\n * Represents an item in the chat's timeline: a message the user or the assistant sent, one of the\n * app's own events it shows (see {@link EventItem}), or a tool call waiting for the user's approval\n * (see {@link ApprovalItem}).\n *\n * **Example** (Showing each item of a chat)\n *\n * ```ts\n * import { Timeline } from \"effect-assistant\"\n * import * as Match from \"effect/Match\"\n * import * as Schema from \"effect/Schema\"\n *\n * class DailyBudgetReached extends Schema.TaggedClass<DailyBudgetReached>()(\"DailyBudgetReached\", {}) {}\n *\n * const line = (item: Timeline.Item<DailyBudgetReached>) =>\n * Match.valueTags(item, {\n * UserMessage: (sent) => `You: ${sent.text}`,\n * AssistantMessage: (sent) => `Assistant: ${sent.text}`,\n * DailyBudgetReached: () => \"You've reached today's budget.\"\n * })\n * ```\n *\n * @category models\n * @since 0.3.0\n */\nexport type Item<Events = never> = UserMessage | AssistantMessage | EventItem<Events> | ApprovalItem\n\n/**\n * Schema for a specialist that is working for the assistant. It holds the specialist's name and\n * when it started, in epoch milliseconds.\n *\n * @category schemas\n * @since 0.1.0\n */\nexport const SpecialistWorking = Schema.Struct({\n specialist: Schema.String,\n since: Schema.Finite\n})\n\n/**\n * Represents a specialist that is working for the assistant.\n *\n * @category models\n * @since 0.1.0\n */\nexport type SpecialistWorking = typeof SpecialistWorking.Type\n\n/**\n * Schema for what is working in the chat.\n *\n * `working` is true while the assistant runs a turn, or when a turn is about to start for\n * something that arrived. `specialists` lists the specialists that are working. When the chat is\n * idle, nothing is working, even if a workflow is waiting on a timer or on the user.\n *\n * @category schemas\n * @since 0.1.0\n */\nexport const Activity = Schema.Struct({\n working: Schema.Boolean,\n specialists: Schema.Array(SpecialistWorking)\n})\n\n/**\n * Represents what is working in the chat.\n *\n * @category models\n * @since 0.1.0\n */\nexport type Activity = typeof Activity.Type\n\n/**\n * Represents a chat's timeline: the chat as its user sees it, typed with the app's events it shows.\n * It holds the chat's items in order, what is working in it, whether the last turn failed, and\n * whether the chat was deleted. `sequence` is the position in the chat's log that the timeline is up to.\n *\n * @category models\n * @since 0.3.0\n */\nexport interface Timeline<Events = never> {\n readonly sequence: number\n readonly items: ReadonlyArray<Item<Events>>\n readonly activity: Activity\n /** Whether the last turn failed with no message after it. */\n readonly failed: boolean\n /** Whether the chat was deleted. A deleted chat accepts nothing more, and its stream ends. */\n readonly deleted: boolean\n}\n\n/**\n * Represents a change to a chat's timeline, typed with the app's events it shows.\n *\n * **Details**\n *\n * A change holds each item that is new or changed (for example, a message whose block was\n * answered or closed, or a call the user approved), in full. It also holds the current activity, whether the last turn failed,\n * and whether the chat was deleted. `sequence` is the position in the chat's log that the change is up\n * to.\n *\n * @category models\n * @since 0.3.0\n */\nexport interface Change<Events = never> {\n readonly sequence: number\n readonly items: ReadonlyArray<Item<Events>>\n readonly activity: Activity\n readonly failed: boolean\n readonly deleted: boolean\n}\n\nconst eventItem = (schema: AssistantEvent.AppEventSchema) =>\n Schema.Struct({ _tag: Schema.Literal(AppEvents.tagOf(schema)), id: Schema.String, at: Schema.Finite, event: schema })\n\nfunction makeChange<const Events extends ReadonlyArray<AssistantEvent.AppEventSchema> = readonly []>(\n events?: Events\n): Schema.Codec<Change<Events[number][\"Type\"]>, Schema.Json>\n// The overload states the decoded type: an item per given schema, each typed with its own event.\nfunction makeChange(events: ReadonlyArray<AssistantEvent.AppEventSchema> = []): Schema.Top {\n const AnyItem = Schema.toCodecJson(\n Schema.Union([UserMessage, AssistantMessage, ApprovalItem, ...events.map(eventItem)])\n )\n\n const decodeItem = Schema.decodeUnknownOption(AnyItem)\n const encodeItem = Schema.encodeUnknownSync(AnyItem)\n\n return Schema.Struct({\n sequence: Schema.Int,\n // An item for an event this schema doesn't name, or that doesn't decode (from a newer server, say), is left out\n // rather than failing the whole change.\n items: Schema.Array(Schema.Json).pipe(\n Schema.decodeTo(\n Schema.Array(Schema.toType(AnyItem)),\n SchemaTransformation.transform<ReadonlyArray<typeof AnyItem.Type>, ReadonlyArray<Schema.Json>>({\n decode: (json) => json.flatMap((item) => Option.toArray(decodeItem(item))),\n encode: (items) => items.map((item) => encodeItem(item))\n })\n )\n ),\n activity: Activity,\n failed: Schema.Boolean,\n deleted: Schema.Boolean\n })\n}\n\n/**\n * Creates the schema for the changes to a chat's timeline, with items for the given app events,\n * in JSON.\n *\n * **Details**\n *\n * It is the success schema of the RPC that streams a chat's timeline: name the events the chat's\n * users see, the same ones the handler passes to {@link changes} as `show`. Tool calls waiting for\n * approval are always in it, as {@link ApprovalItem}s. Each item decodes its\n * event with that event's schema, so the client gets `Change<Events>` typed with them and folds it\n * with {@link fold}, no definition needed. `Change()` carries no app events.\n *\n * A handler whose changes show an event the schema doesn't name doesn't compile.\n *\n * **Example** (An RPC that streams a chat's timeline, with the budget notice)\n *\n * ```ts\n * import { ChatId, Timeline } from \"effect-assistant\"\n * import * as Rpc from \"effect/rpc/Rpc\"\n * import * as Schema from \"effect/Schema\"\n *\n * class DailyBudgetReached extends Schema.TaggedClass<DailyBudgetReached>()(\"DailyBudgetReached\", {}) {}\n *\n * const Subscribe = Rpc.make(\"Subscribe\", {\n * payload: { chatId: ChatId.ChatId },\n * success: Timeline.Change([DailyBudgetReached]),\n * stream: true\n * })\n * ```\n *\n * @category schemas\n * @since 0.3.0\n */\nexport const Change: <const Events extends ReadonlyArray<AssistantEvent.AppEventSchema> = readonly []>(\n events?: Events\n) => Schema.Codec<Change<Events[number][\"Type\"]>, Schema.Json> = makeChange\n\n/**\n * The timeline of a chat before anything has happened in it, with no items and nothing working.\n * Use it as the starting point when folding changes with {@link apply}.\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const empty: Timeline = {\n sequence: 0,\n items: [],\n activity: { working: false, specialists: [] },\n failed: false,\n deleted: false\n}\n\n/**\n * Returns the timeline after a change.\n *\n * **Details**\n *\n * Each item in the change replaces the item with the same id, in place, and new items are added\n * after the rest. The change's activity, `failed`, and `deleted` replace the timeline's. Applying a\n * change twice changes nothing, so a client that resubscribes from the start can fold again and get\n * the same timeline.\n *\n * The timeline is typed with the change's events, as its RPC's schema decoded them (see\n * {@link Change}). To fold a stream of changes, use {@link fold}: in\n * `Stream.scan(() => Timeline.empty, Timeline.apply)`, TypeScript fixes the state's type from\n * `empty` before it sees the changes' events, so that form needs the timeline's type written out.\n *\n * **Example** (Folding changes into your own state)\n *\n * ```ts\n * import { Timeline } from \"effect-assistant\"\n *\n * interface State<Events> {\n * readonly timeline: Timeline.Timeline<Events>\n * readonly unread: number\n * }\n *\n * const next = <Events>(state: State<Events>, change: Timeline.Change<Events>): State<Events> => ({\n * timeline: Timeline.apply(state.timeline, change),\n * unread: state.unread + change.items.length\n * })\n * ```\n *\n * @category combinators\n * @since 0.1.0\n */\nexport const apply = <Events>(timeline: Timeline<Events>, change: Change<Events>): Timeline<Events> => {\n const changed = new Map(change.items.map((item) => [item.id, item]))\n const known = new Set(timeline.items.map((item) => item.id))\n\n return {\n sequence: Math.max(timeline.sequence, change.sequence),\n items: [\n ...timeline.items.map((item) => changed.get(item.id) ?? item),\n ...change.items.filter((item) => !known.has(item.id))\n ],\n activity: change.activity,\n failed: change.failed,\n deleted: change.deleted\n }\n}\n\n/**\n * Returns the timelines a stream of changes folds to, from {@link empty}, typed with the changes'\n * events.\n *\n * **Example** (A chat's timeline on the client)\n *\n * ```ts\n * import { Timeline } from \"effect-assistant\"\n * import * as Schema from \"effect/Schema\"\n * import * as Stream from \"effect/Stream\"\n *\n * class DailyBudgetReached extends Schema.TaggedClass<DailyBudgetReached>()(\"DailyBudgetReached\", {}) {}\n *\n * // The stream of an RPC whose success schema is `Timeline.Change([DailyBudgetReached])`.\n * declare const changes: Stream.Stream<Timeline.Change<DailyBudgetReached>>\n *\n * const timelines = changes.pipe(Timeline.fold) // Stream<Timeline.Timeline<DailyBudgetReached>>\n * ```\n *\n * @category combinators\n * @since 0.3.0\n */\nexport const fold = <Events, E, R>(\n changes: Stream.Stream<Change<Events>, E, R>\n): Stream.Stream<Timeline<Events>, E, R> => changes.pipe(Stream.scan((): Timeline<Events> => empty, apply))\n\n/**\n * Returns whether nothing is working in the chat: no turn is running or about to start, and no\n * specialist is working.\n *\n * @category guards\n * @since 0.1.0\n */\nexport const isIdle = <Events>(timeline: Timeline<Events>): boolean =>\n !timeline.activity.working && timeline.activity.specialists.length === 0\n\ninterface AnyEventItem {\n readonly _tag: string\n readonly id: string\n readonly at: number\n readonly event: { readonly _tag: string }\n}\n\ntype AnyItem = UserMessage | AssistantMessage | AnyEventItem | ApprovalItem\n\n// What the fold below builds: the overloads that use it type its items with the events shown.\ninterface AnyChange {\n readonly sequence: number\n readonly items: ReadonlyArray<AnyItem>\n readonly activity: Activity\n readonly failed: boolean\n readonly deleted: boolean\n}\n\n/**\n * Represents the options for building a chat's timeline from its log.\n *\n * @category models\n * @since 0.3.0\n */\nexport interface ShowOptions<Show extends ReadonlyArray<AssistantEvent.AppEventSchema>> {\n /**\n * The schemas of the app's events the chat's users see. Each becomes an item in the timeline;\n * the app's other events are left out. Defaults to none.\n */\n readonly show?: Show | undefined\n}\n\n/**\n * Represents the schemas an assistant's timeline may show: some of its own events.\n *\n * @category utility types\n * @since 0.3.0\n */\nexport type Shown<Events> = ReadonlyArray<AssistantEvent.AppEventSchema & { readonly Type: Events }>\n\nfunction snapshotWith(entries: ReadonlyArray<AssistantEvent.AnyEntry>): Change\nfunction snapshotWith<Events, const Show extends Shown<Events> = readonly []>(\n entries: ReadonlyArray<AssistantEvent.AnyEntry>,\n definition: Definition<Events>,\n options?: ShowOptions<Show>\n): Change<Show[number][\"Type\"]>\nfunction snapshotWith(\n entries: ReadonlyArray<AssistantEvent.AnyEntry>,\n _definition?: Schemas,\n options?: ShowOptions<ReadonlyArray<AssistantEvent.AppEventSchema>>\n): AnyChange {\n const timeline = folding(options?.show ?? [])\n\n for (const entry of entries) timeline.step(entry)\n\n return timeline.snapshot\n}\n\n/**\n * Returns a chat's whole timeline as one change, built from the entries of its log.\n *\n * **When to use**\n *\n * Use it on the server for an RPC that returns a whole chat at once: its success schema is\n * {@link Change} with the same events as `show`, and the client applies it to {@link empty}.\n *\n * **Details**\n *\n * Only the app's events in `show` become items; without it, the timeline has none.\n *\n * @category constructors\n * @since 0.3.0\n */\nexport const snapshot: {\n (entries: ReadonlyArray<AssistantEvent.AnyEntry>): Change\n <Events, const Show extends Shown<Events> = readonly []>(\n entries: ReadonlyArray<AssistantEvent.AnyEntry>,\n definition: Definition<Events>,\n options?: ShowOptions<Show>\n ): Change<Show[number][\"Type\"]>\n} = snapshotWith\n\nfunction fromEntriesWith(entries: ReadonlyArray<AssistantEvent.AnyEntry>): Timeline\nfunction fromEntriesWith<Events, const Show extends Shown<Events> = readonly []>(\n entries: ReadonlyArray<AssistantEvent.AnyEntry>,\n definition: Definition<Events>,\n options?: ShowOptions<Show>\n): Timeline<Show[number][\"Type\"]>\nfunction fromEntriesWith(\n entries: ReadonlyArray<AssistantEvent.AnyEntry>,\n _definition?: Schemas,\n options?: ShowOptions<ReadonlyArray<AssistantEvent.AppEventSchema>>\n): AnyChange {\n const timeline = folding(options?.show ?? [])\n\n for (const entry of entries) timeline.step(entry)\n\n return timeline.snapshot\n}\n\n/**\n * Returns a chat's timeline, built from the entries of its log.\n *\n * **When to use**\n *\n * Use it in server-side code that reads a whole chat. A client gets the timeline as a stream from\n * {@link changes} instead, and an RPC that returns a whole chat sends a {@link snapshot}.\n *\n * **Details**\n *\n * Only the app's events in `show` become items, typed with their schemas; without it, the timeline\n * has none.\n *\n * **Example** (Building a chat's timeline from its log)\n *\n * ```ts\n * import { type ChatId, ChatLog, Timeline } from \"effect-assistant\"\n * import * as Effect from \"effect/Effect\"\n *\n * const timelineOf = (chatId: ChatId.ChatId) =>\n * ChatLog.ChatLog.use((log) => log.read(chatId)).pipe(\n * Effect.map((entries) => Timeline.fromEntries(entries))\n * )\n * ```\n *\n * @see {@link changes} for a stream of changes instead of a whole timeline\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const fromEntries: {\n (entries: ReadonlyArray<AssistantEvent.AnyEntry>): Timeline\n <Events, const Show extends Shown<Events> = readonly []>(\n entries: ReadonlyArray<AssistantEvent.AnyEntry>,\n definition: Definition<Events>,\n options?: ShowOptions<Show>\n ): Timeline<Show[number][\"Type\"]>\n} = fromEntriesWith\n\ninterface Schemas {\n readonly events: ReadonlyArray<AssistantEvent.AppEventSchema>\n}\n\ninterface Folding {\n readonly step: (entry: AssistantEvent.AnyEntry) => AnyChange | undefined\n readonly snapshot: AnyChange\n}\n\nconst activityOf = (activity: AssistantActivity.State): Activity => ({\n working: activity.turnSince !== undefined || activity.queued,\n specialists: [...activity.specialists.values()].map((run) => ({\n specialist: run.specialist,\n since: run.since\n }))\n})\n\nconst sameActivity = (a: Activity, b: Activity) =>\n a.working === b.working &&\n a.specialists.length === b.specialists.length &&\n a.specialists.every((run, i) => {\n const other = b.specialists[i]\n\n return other !== undefined && run.specialist === other.specialist && run.since === other.since\n })\n\nconst folding = (show: ReadonlyArray<AssistantEvent.AppEventSchema>): Folding => {\n const shown = new Map(show.map((schema) => [AppEvents.tagOf(schema), schema]))\n let approvals: ReadonlyMap<string, ToolApproval.State> = new Map()\n let sequence = 0\n let failed = false\n let deleted = false\n let activity = AssistantActivity.empty\n let blocks: ReadonlyMap<string, BlockKit.State> = new Map()\n // The items in order, and the assistant's messages as stored: their blocks change as the chat goes on.\n const items = new Map<string, AnyItem>()\n const sent = new Map<string, AssistantEvent.AssistantMessage>()\n // The results a block can show, by id: a message shows one stored before it.\n const results = new Map<string, Schema.Json>()\n\n const blockOf = (message: AssistantEvent.AssistantMessage): Block | undefined => {\n if (message.block === undefined) return undefined\n\n const state = blocks.get(message.id)\n const status = state === undefined || state.open ? \"open\" : state.answer === undefined ? \"closed\" : \"answered\"\n const block: Block = { kind: message.block.kind, props: toJson(message.block.props), status }\n const { resultId } = message.block\n const result = resultId === undefined ? undefined : results.get(resultId)\n const shown = result === undefined ? block : { ...block, result }\n\n return state?.answer === undefined ? shown : { ...shown, answer: toJson(state.answer.value) }\n }\n\n const itemOf = (message: AssistantEvent.AssistantMessage, at: number): AssistantMessage => {\n const block = blockOf(message)\n\n return block === undefined\n ? AssistantMessage.make({ id: message.id, text: message.text, at })\n : AssistantMessage.make({ id: message.id, text: message.text, at, block })\n }\n\n const touched = (event: AssistantEvent.AssistantEvent, at: number): ReadonlyArray<string> =>\n Match.value(event).pipe(\n Match.tagsExhaustive({\n UserMessage: (message) => {\n items.set(\n message.id,\n message.attachments === undefined\n ? UserMessage.make({ id: message.id, text: message.text, at })\n : UserMessage.make({\n id: message.id,\n text: message.text,\n attachments: message.attachments,\n at\n })\n )\n failed = false\n\n return [message.id]\n },\n AssistantMessage: (message) => {\n sent.set(message.id, message)\n items.set(message.id, itemOf(message, at))\n failed = false\n\n return [message.id, ...(message.closes ?? [])]\n },\n BlockAnswer: (answer) => {\n failed = false\n\n return [answer.messageId]\n },\n BlocksClosed: (closed) => closed.messageIds,\n ApprovalRequested: () => [],\n ApprovalDecision: () => {\n failed = false\n\n return []\n },\n ApprovalsExpired: () => [],\n TurnFailed: () => {\n failed = true\n\n return []\n },\n ModelOutput: () => [],\n ToolStarted: () => [],\n ToolResult: (result) => {\n if (result.resultId !== undefined && result.outcome === \"success\") {\n results.set(result.resultId, result.full ?? result.result)\n }\n\n return []\n },\n TurnStarted: () => [],\n TurnCompleted: () => [],\n SpecialistStarted: () => [],\n SpecialistFinished: () => [],\n ChatDeleted: () => {\n deleted = true\n\n return []\n }\n })\n )\n\n // Decoded as `subscribe` streams it, or as the log stores it; one that doesn't decode is left out.\n const eventItemOf = (entry: AssistantEvent.AnyEntry): Option.Option<AnyEventItem> => {\n const schema = shown.get(entry.event._tag)\n\n if (schema === undefined) return Option.none()\n\n const event = Schema.is(schema)(entry.event) ? Option.some(entry.event) : AppEvents.decodeWith(schema, entry.event)\n\n return Option.map(event, (decoded) => ({\n _tag: decoded._tag,\n id: `app-event:${entry.sequence}`,\n at: entry.at,\n event: decoded\n }))\n }\n\n const callsOf = (event: AssistantEvent.AssistantEvent): ReadonlyArray<string> =>\n Predicate.isTagged(event, \"ApprovalsExpired\")\n ? event.callIds\n : Predicate.isTagged(event, \"ApprovalRequested\") ||\n Predicate.isTagged(event, \"ApprovalDecision\") ||\n Predicate.isTagged(event, \"ToolResult\")\n ? [event.callId]\n : []\n\n // The items of the calls this entry changed.\n const approvalItems = (event: AssistantEvent.AssistantEvent, before: ReadonlyMap<string, ToolApproval.State>) =>\n callsOf(event).flatMap((callId) => {\n const state = approvals.get(callId)\n\n if (state === undefined || state === before.get(callId)) return []\n\n const item = ApprovalItem.make({\n id: callId,\n tool: state.request.tool,\n status: state.status,\n reason: state.reason,\n outcome: state.outcome,\n at: state.at\n })\n\n items.set(callId, item)\n\n return [item]\n })\n\n const step = (entry: AssistantEvent.AnyEntry): AnyChange | undefined => {\n const previous = { activity: activityOf(activity), failed, deleted }\n const { event } = entry\n\n sequence = entry.sequence\n activity = AssistantActivity.apply(activity, entry)\n\n if (!AssistantEvent.isLibraryEvent(event)) {\n const now = activityOf(activity)\n\n return Option.match(eventItemOf(entry), {\n onNone: () =>\n sameActivity(previous.activity, now) ? undefined : { sequence, items: [], activity: now, failed, deleted },\n onSome: (item) => {\n items.set(item.id, item)\n\n return { sequence, items: [item], activity: now, failed, deleted }\n }\n })\n }\n\n blocks = BlockKit.apply(blocks, event)\n\n const decidedBefore = approvals\n approvals = ToolApproval.apply(approvals, entry)\n\n const changed = [...new Set(touched(event, entry.at))].flatMap((id) => {\n const message = sent.get(id)\n const current = items.get(id)\n\n if (message === undefined || current === undefined) return current === undefined ? [] : [current]\n\n const next = itemOf(message, current.at)\n items.set(id, next)\n\n return [next]\n })\n\n const all = [...changed, ...approvalItems(event, decidedBefore)]\n const now = activityOf(activity)\n\n return all.length === 0 &&\n failed === previous.failed &&\n deleted === previous.deleted &&\n sameActivity(previous.activity, now)\n ? undefined\n : { sequence, items: all, activity: now, failed, deleted }\n }\n\n return {\n step,\n get snapshot() {\n return { sequence, items: [...items.values()], activity: activityOf(activity), failed, deleted }\n }\n }\n}\n\nconst merge = (changes: ReadonlyArray<AnyChange>): AnyChange | undefined => {\n const last = changes.at(-1)\n\n if (last === undefined) return undefined\n\n const items = new Map<string, AnyItem>()\n\n for (const change of changes) for (const item of change.items) items.set(item.id, item)\n\n return {\n sequence: last.sequence,\n items: [...items.values()],\n activity: last.activity,\n failed: last.failed,\n deleted: last.deleted\n }\n}\n\n/**\n * Represents an assistant's definition as the timeline reads it. An assistant made with\n * `Assistant.make` fits it.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface Definition<Events = never> {\n /** The schemas of the assistant's events. */\n readonly events: ReadonlyArray<AssistantEvent.AppEventSchema>\n /** The schema of the chat's entries, typed with the assistant's events: it types `show`. */\n readonly Entry: Schema.Codec<Assistant.Entry<Events>, Schema.Json>\n}\n\n/**\n * Represents the options accepted by {@link changes}.\n *\n * @category models\n * @since 0.3.0\n */\nexport interface ChangesOptions<\n Show extends ReadonlyArray<AssistantEvent.AppEventSchema> = readonly []\n> extends ShowOptions<Show> {\n /**\n * The `sequence` up to which the client already has the timeline. The result contains only the\n * changes after it. Defaults to `0`, every change.\n */\n readonly after?: number | undefined\n}\n\nfunction changesWith<E, R, Events, const Show extends Shown<Events> = readonly []>(\n entries: Stream.Stream<AssistantEvent.AnyEntry, E, R>,\n definition: Definition<Events>,\n options?: ChangesOptions<Show>\n): Stream.Stream<Change<Show[number][\"Type\"]>, E, R>\nfunction changesWith<E, R>(\n entries: Stream.Stream<AssistantEvent.AnyEntry, E, R>,\n _definition: Schemas,\n options?: ChangesOptions<ReadonlyArray<AssistantEvent.AppEventSchema>>\n): Stream.Stream<AnyChange, E, R> {\n const after = options?.after ?? 0\n\n return entries.pipe(\n Stream.mapAccumArray(\n () => folding(options?.show ?? []),\n (timeline, chunk) => {\n const changed = chunk.flatMap((entry) => {\n const change = timeline.step(entry)\n\n return change === undefined || entry.sequence <= after ? [] : [change]\n })\n\n const merged = merge(changed)\n\n return [timeline, merged === undefined ? [] : [merged]]\n }\n )\n )\n}\n\n/**\n * Returns the stream of changes to a chat's timeline that its entries produce, for a chat client.\n *\n * **Details**\n *\n * The entries must stream from the start of the chat's log, as `assistant.subscribe(chatId)` does\n * without `after`. The fold reads every entry, because a message depends on earlier entries: its\n * block is answered or closed later. `after` only skips emitting the changes up to that `sequence`, so a\n * client that has the timeline up to a `sequence` receives only what changed since. Pass `after` here,\n * not to `subscribe`: a subscription that starts after a `sequence` gives the fold an incomplete log,\n * and the changes it produces are wrong.\n *\n * Each chunk of entries becomes one change; the stored entries, for example, arrive as one chunk.\n *\n * **Only the app's events in `show` reach the client.** Each one becomes an item between the\n * messages (see {@link EventItem}); the app's other events, like a summary of the chat for the\n * model, are left out. Without `show`, none are shown. The changes are typed with the events shown,\n * so the RPC's success schema names the same ones (`Timeline.Change([...])`), and a handler that\n * shows one the schema doesn't name doesn't compile. A timeline shows a turn as working once it\n * starts, or from the user's input.\n *\n * **Example** (Streaming a chat's timeline to a client, with the one app event its user sees)\n *\n * ```ts\n * import { Assistant, type ChatId, Timeline } from \"effect-assistant\"\n * import * as Schema from \"effect/Schema\"\n * import * as Stream from \"effect/Stream\"\n *\n * class Summary extends Schema.TaggedClass<Summary>()(\"Summary\", { text: Schema.String, throughSequence: Schema.Int }) {}\n * class DailyBudgetReached extends Schema.TaggedClass<DailyBudgetReached>()(\"DailyBudgetReached\", {}) {}\n *\n * const MyAssistant = Assistant.make(\"my-assistant\", { events: [Summary, DailyBudgetReached] })\n *\n * // The summary is for the model: only the budget notice reaches the user.\n * const watch = (chatId: ChatId.ChatId, after: number) =>\n * Stream.unwrap(\n * MyAssistant.useSync((assistant) =>\n * assistant.subscribe(chatId).pipe(Timeline.changes(MyAssistant, { show: [DailyBudgetReached], after }))\n * )\n * )\n * ```\n *\n * @see {@link fold} for folding the changes into a timeline on the client\n *\n * @category utils\n * @since 0.3.0\n */\nexport const changes: {\n <Events, const Show extends Shown<Events> = readonly []>(\n definition: Definition<Events>,\n options?: ChangesOptions<Show>\n ): <E, R>(entries: Stream.Stream<AssistantEvent.AnyEntry, E, R>) => Stream.Stream<Change<Show[number][\"Type\"]>, E, R>\n <E, R, Events, const Show extends Shown<Events> = readonly []>(\n entries: Stream.Stream<AssistantEvent.AnyEntry, E, R>,\n definition: Definition<Events>,\n options?: ChangesOptions<Show>\n ): Stream.Stream<Change<Show[number][\"Type\"]>, E, R>\n} = dual((args) => Stream.isStream(args[0]), changesWith)\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4CA,MAAa,cAAc,OAAO,SAAS;CAAC;CAAQ;CAAY;AAAQ,CAAC;;;;;;;;;AAkBzE,MAAa,QAAQ,OAAO,OAAO;CACjC,MAAM,OAAO;CACb,OAAO,OAAO;CACd,QAAQ;CACR,QAAQ,OAAO,SAAS,OAAO,IAAI;;CAEnC,QAAQ,OAAO,SAAS,OAAO,IAAI;AACrC,CAAC;;;;;;;;AAiBD,MAAa,cAAc,OAAO,aAAa,eAAe;CAC5D,IAAI;CACJ,MAAM,OAAO;CACb,aAAa,OAAO,SAAS,OAAO,MAAMA,UAAyB,CAAC;CACpE,IAAI,OAAO;AACb,CAAC;;;;;;;AAgBD,MAAa,mBAAmB,OAAO,aAAa,oBAAoB;CACtE,IAAI,OAAO;CACX,MAAM,OAAO;CACb,IAAI,OAAO;CACX,OAAO,OAAO,SAAS,KAAK;AAC9B,CAAC;;;;;;;;;;;;;;;;;AA6CD,MAAa,eAAe,OAAO,aAAa,gBAAgB;CAC9D,IAAI,OAAO;CACX,MAAM,OAAO;CACb,QAAQ,OAAO,SAAS;EAAC;EAAW;EAAY;EAAY;CAAS,CAAC;CACtE,QAAQ,OAAO,SAAS,OAAO,MAAM;CACrC,SAAS,OAAO,SAASC,WAA0B;CACnD,IAAI,OAAO;AACb,CAAC;;;;;;;;AA6CD,MAAa,oBAAoB,OAAO,OAAO;CAC7C,YAAY,OAAO;CACnB,OAAO,OAAO;AAChB,CAAC;;;;;;;;;;;AAoBD,MAAa,WAAW,OAAO,OAAO;CACpC,SAAS,OAAO;CAChB,aAAa,OAAO,MAAM,iBAAiB;AAC7C,CAAC;AAiDD,MAAM,aAAa,WACjB,OAAO,OAAO;CAAE,MAAM,OAAO,QAAQC,MAAgB,MAAM,CAAC;CAAG,IAAI,OAAO;CAAQ,IAAI,OAAO;CAAQ,OAAO;AAAO,CAAC;AAMtH,SAAS,WAAW,SAAuD,CAAC,GAAe;CACzF,MAAM,UAAU,OAAO,YACrB,OAAO,MAAM;EAAC;EAAa;EAAkB;EAAc,GAAG,OAAO,IAAI,SAAS;CAAC,CAAC,CACtF;CAEA,MAAM,aAAa,OAAO,oBAAoB,OAAO;CACrD,MAAM,aAAa,OAAO,kBAAkB,OAAO;CAEnD,OAAO,OAAO,OAAO;EACnB,UAAU,OAAO;EAGjB,OAAO,OAAO,MAAM,OAAO,IAAI,CAAC,CAAC,KAC/B,OAAO,SACL,OAAO,MAAM,OAAO,OAAO,OAAO,CAAC,GACnC,qBAAqB,UAA0E;GAC7F,SAAS,SAAS,KAAK,SAAS,SAAS,OAAO,QAAQ,WAAW,IAAI,CAAC,CAAC;GACzE,SAAS,UAAU,MAAM,KAAK,SAAS,WAAW,IAAI,CAAC;EACzD,CAAC,CACH,CACF;EACA,UAAU;EACV,QAAQ,OAAO;EACf,SAAS,OAAO;CAClB,CAAC;AACH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmCA,MAAa,SAEoD;;;;;;;;AASjE,MAAa,QAAkB;CAC7B,UAAU;CACV,OAAO,CAAC;CACR,UAAU;EAAE,SAAS;EAAO,aAAa,CAAC;CAAE;CAC5C,QAAQ;CACR,SAAS;AACX;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoCA,MAAa,SAAiB,UAA4B,WAA6C;CACrG,MAAM,UAAU,IAAI,IAAI,OAAO,MAAM,KAAK,SAAS,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC;CACnE,MAAM,QAAQ,IAAI,IAAI,SAAS,MAAM,KAAK,SAAS,KAAK,EAAE,CAAC;CAE3D,OAAO;EACL,UAAU,KAAK,IAAI,SAAS,UAAU,OAAO,QAAQ;EACrD,OAAO,CACL,GAAG,SAAS,MAAM,KAAK,SAAS,QAAQ,IAAI,KAAK,EAAE,KAAK,IAAI,GAC5D,GAAG,OAAO,MAAM,QAAQ,SAAS,CAAC,MAAM,IAAI,KAAK,EAAE,CAAC,CACtD;EACA,UAAU,OAAO;EACjB,QAAQ,OAAO;EACf,SAAS,OAAO;CAClB;AACF;;;;;;;;;;;;;;;;;;;;;;;AAwBA,MAAa,QACX,YAC0C,QAAQ,KAAK,OAAO,WAA6B,OAAO,KAAK,CAAC;;;;;;;;AAS1G,MAAa,UAAkB,aAC7B,CAAC,SAAS,SAAS,WAAW,SAAS,SAAS,YAAY,WAAW;AAgDzE,SAAS,aACP,SACA,aACA,SACW;CACX,MAAM,WAAW,QAAQ,SAAS,QAAQ,CAAC,CAAC;CAE5C,KAAK,MAAM,SAAS,SAAS,SAAS,KAAK,KAAK;CAEhD,OAAO,SAAS;AAClB;;;;;;;;;;;;;;;;AAiBA,MAAa,WAOT;AAQJ,SAAS,gBACP,SACA,aACA,SACW;CACX,MAAM,WAAW,QAAQ,SAAS,QAAQ,CAAC,CAAC;CAE5C,KAAK,MAAM,SAAS,SAAS,SAAS,KAAK,KAAK;CAEhD,OAAO,SAAS;AAClB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCA,MAAa,cAOT;AAWJ,MAAM,cAAc,cAAiD;CACnE,SAAS,SAAS,cAAc,KAAA,KAAa,SAAS;CACtD,aAAa,CAAC,GAAG,SAAS,YAAY,OAAO,CAAC,CAAC,CAAC,KAAK,SAAS;EAC5D,YAAY,IAAI;EAChB,OAAO,IAAI;CACb,EAAE;AACJ;AAEA,MAAM,gBAAgB,GAAa,MACjC,EAAE,YAAY,EAAE,WAChB,EAAE,YAAY,WAAW,EAAE,YAAY,UACvC,EAAE,YAAY,OAAO,KAAK,MAAM;CAC9B,MAAM,QAAQ,EAAE,YAAY;CAE5B,OAAO,UAAU,KAAA,KAAa,IAAI,eAAe,MAAM,cAAc,IAAI,UAAU,MAAM;AAC3F,CAAC;AAEH,MAAM,WAAW,SAAgE;CAC/E,MAAM,QAAQ,IAAI,IAAI,KAAK,KAAK,WAAW,CAACA,MAAgB,MAAM,GAAG,MAAM,CAAC,CAAC;CAC7E,IAAI,4BAAqD,IAAI,IAAI;CACjE,IAAI,WAAW;CACf,IAAI,SAAS;CACb,IAAI,UAAU;CACd,IAAI,WAAWC;CACf,IAAI,yBAA8C,IAAI,IAAI;CAE1D,MAAM,wBAAQ,IAAI,IAAqB;CACvC,MAAM,uBAAO,IAAI,IAA6C;CAE9D,MAAM,0BAAU,IAAI,IAAyB;CAE7C,MAAM,WAAW,YAAgE;EAC/E,IAAI,QAAQ,UAAU,KAAA,GAAW,OAAO,KAAA;EAExC,MAAM,QAAQ,OAAO,IAAI,QAAQ,EAAE;EACnC,MAAM,SAAS,UAAU,KAAA,KAAa,MAAM,OAAO,SAAS,MAAM,WAAW,KAAA,IAAY,WAAW;EACpG,MAAM,QAAe;GAAE,MAAM,QAAQ,MAAM;GAAM,OAAO,OAAO,QAAQ,MAAM,KAAK;GAAG;EAAO;EAC5F,MAAM,EAAE,aAAa,QAAQ;EAC7B,MAAM,SAAS,aAAa,KAAA,IAAY,KAAA,IAAY,QAAQ,IAAI,QAAQ;EACxE,MAAM,QAAQ,WAAW,KAAA,IAAY,QAAQ;GAAE,GAAG;GAAO;EAAO;EAEhE,OAAO,OAAO,WAAW,KAAA,IAAY,QAAQ;GAAE,GAAG;GAAO,QAAQ,OAAO,MAAM,OAAO,KAAK;EAAE;CAC9F;CAEA,MAAM,UAAU,SAA0C,OAAiC;EACzF,MAAM,QAAQ,QAAQ,OAAO;EAE7B,OAAO,UAAU,KAAA,IACb,iBAAiB,KAAK;GAAE,IAAI,QAAQ;GAAI,MAAM,QAAQ;GAAM;EAAG,CAAC,IAChE,iBAAiB,KAAK;GAAE,IAAI,QAAQ;GAAI,MAAM,QAAQ;GAAM;GAAI;EAAM,CAAC;CAC7E;CAEA,MAAM,WAAW,OAAsC,OACrD,MAAM,MAAM,KAAK,CAAC,CAAC,KACjB,MAAM,eAAe;EACnB,cAAc,YAAY;GACxB,MAAM,IACJ,QAAQ,IACR,QAAQ,gBAAgB,KAAA,IACpB,YAAY,KAAK;IAAE,IAAI,QAAQ;IAAI,MAAM,QAAQ;IAAM;GAAG,CAAC,IAC3D,YAAY,KAAK;IACf,IAAI,QAAQ;IACZ,MAAM,QAAQ;IACd,aAAa,QAAQ;IACrB;GACF,CAAC,CACP;GACA,SAAS;GAET,OAAO,CAAC,QAAQ,EAAE;EACpB;EACA,mBAAmB,YAAY;GAC7B,KAAK,IAAI,QAAQ,IAAI,OAAO;GAC5B,MAAM,IAAI,QAAQ,IAAI,OAAO,SAAS,EAAE,CAAC;GACzC,SAAS;GAET,OAAO,CAAC,QAAQ,IAAI,GAAI,QAAQ,UAAU,CAAC,CAAE;EAC/C;EACA,cAAc,WAAW;GACvB,SAAS;GAET,OAAO,CAAC,OAAO,SAAS;EAC1B;EACA,eAAe,WAAW,OAAO;EACjC,yBAAyB,CAAC;EAC1B,wBAAwB;GACtB,SAAS;GAET,OAAO,CAAC;EACV;EACA,wBAAwB,CAAC;EACzB,kBAAkB;GAChB,SAAS;GAET,OAAO,CAAC;EACV;EACA,mBAAmB,CAAC;EACpB,mBAAmB,CAAC;EACpB,aAAa,WAAW;GACtB,IAAI,OAAO,aAAa,KAAA,KAAa,OAAO,YAAY,WACtD,QAAQ,IAAI,OAAO,UAAU,OAAO,QAAQ,OAAO,MAAM;GAG3D,OAAO,CAAC;EACV;EACA,mBAAmB,CAAC;EACpB,qBAAqB,CAAC;EACtB,yBAAyB,CAAC;EAC1B,0BAA0B,CAAC;EAC3B,mBAAmB;GACjB,UAAU;GAEV,OAAO,CAAC;EACV;CACF,CAAC,CACH;CAGF,MAAM,eAAe,UAAgE;EACnF,MAAM,SAAS,MAAM,IAAI,MAAM,MAAM,IAAI;EAEzC,IAAI,WAAW,KAAA,GAAW,OAAO,OAAO,KAAK;EAE7C,MAAM,QAAQ,OAAO,GAAG,MAAM,CAAC,CAAC,MAAM,KAAK,IAAI,OAAO,KAAK,MAAM,KAAK,IAAIC,WAAqB,QAAQ,MAAM,KAAK;EAElH,OAAO,OAAO,IAAI,QAAQ,aAAa;GACrC,MAAM,QAAQ;GACd,IAAI,aAAa,MAAM;GACvB,IAAI,MAAM;GACV,OAAO;EACT,EAAE;CACJ;CAEA,MAAM,WAAW,UACf,UAAU,SAAS,OAAO,kBAAkB,IACxC,MAAM,UACN,UAAU,SAAS,OAAO,mBAAmB,KAC3C,UAAU,SAAS,OAAO,kBAAkB,KAC5C,UAAU,SAAS,OAAO,YAAY,IACtC,CAAC,MAAM,MAAM,IACb,CAAC;CAGT,MAAM,iBAAiB,OAAsC,WAC3D,QAAQ,KAAK,CAAC,CAAC,SAAS,WAAW;EACjC,MAAM,QAAQ,UAAU,IAAI,MAAM;EAElC,IAAI,UAAU,KAAA,KAAa,UAAU,OAAO,IAAI,MAAM,GAAG,OAAO,CAAC;EAEjE,MAAM,OAAO,aAAa,KAAK;GAC7B,IAAI;GACJ,MAAM,MAAM,QAAQ;GACpB,QAAQ,MAAM;GACd,QAAQ,MAAM;GACd,SAAS,MAAM;GACf,IAAI,MAAM;EACZ,CAAC;EAED,MAAM,IAAI,QAAQ,IAAI;EAEtB,OAAO,CAAC,IAAI;CACd,CAAC;CAEH,MAAM,QAAQ,UAA0D;EACtE,MAAM,WAAW;GAAE,UAAU,WAAW,QAAQ;GAAG;GAAQ;EAAQ;EACnE,MAAM,EAAE,UAAU;EAElB,WAAW,MAAM;EACjB,WAAWC,QAAwB,UAAU,KAAK;EAElD,IAAI,CAACC,eAA8B,KAAK,GAAG;GACzC,MAAM,MAAM,WAAW,QAAQ;GAE/B,OAAO,OAAO,MAAM,YAAY,KAAK,GAAG;IACtC,cACE,aAAa,SAAS,UAAU,GAAG,IAAI,KAAA,IAAY;KAAE;KAAU,OAAO,CAAC;KAAG,UAAU;KAAK;KAAQ;IAAQ;IAC3G,SAAS,SAAS;KAChB,MAAM,IAAI,KAAK,IAAI,IAAI;KAEvB,OAAO;MAAE;MAAU,OAAO,CAAC,IAAI;MAAG,UAAU;MAAK;MAAQ;KAAQ;IACnE;GACF,CAAC;EACH;EAEA,SAASC,QAAe,QAAQ,KAAK;EAErC,MAAM,gBAAgB;EACtB,YAAYC,QAAmB,WAAW,KAAK;EAc/C,MAAM,MAAM,CAAC,GAZG,CAAC,GAAG,IAAI,IAAI,QAAQ,OAAO,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS,OAAO;GACrE,MAAM,UAAU,KAAK,IAAI,EAAE;GAC3B,MAAM,UAAU,MAAM,IAAI,EAAE;GAE5B,IAAI,YAAY,KAAA,KAAa,YAAY,KAAA,GAAW,OAAO,YAAY,KAAA,IAAY,CAAC,IAAI,CAAC,OAAO;GAEhG,MAAM,OAAO,OAAO,SAAS,QAAQ,EAAE;GACvC,MAAM,IAAI,IAAI,IAAI;GAElB,OAAO,CAAC,IAAI;EACd,CAEsB,GAAG,GAAG,cAAc,OAAO,aAAa,CAAC;EAC/D,MAAM,MAAM,WAAW,QAAQ;EAE/B,OAAO,IAAI,WAAW,KACpB,WAAW,SAAS,UACpB,YAAY,SAAS,WACrB,aAAa,SAAS,UAAU,GAAG,IACjC,KAAA,IACA;GAAE;GAAU,OAAO;GAAK,UAAU;GAAK;GAAQ;EAAQ;CAC7D;CAEA,OAAO;EACL;EACA,IAAI,WAAW;GACb,OAAO;IAAE;IAAU,OAAO,CAAC,GAAG,MAAM,OAAO,CAAC;IAAG,UAAU,WAAW,QAAQ;IAAG;IAAQ;GAAQ;EACjG;CACF;AACF;AAEA,MAAM,SAAS,YAA6D;CAC1E,MAAM,OAAO,QAAQ,GAAG,EAAE;CAE1B,IAAI,SAAS,KAAA,GAAW,OAAO,KAAA;CAE/B,MAAM,wBAAQ,IAAI,IAAqB;CAEvC,KAAK,MAAM,UAAU,SAAS,KAAK,MAAM,QAAQ,OAAO,OAAO,MAAM,IAAI,KAAK,IAAI,IAAI;CAEtF,OAAO;EACL,UAAU,KAAK;EACf,OAAO,CAAC,GAAG,MAAM,OAAO,CAAC;EACzB,UAAU,KAAK;EACf,QAAQ,KAAK;EACb,SAAS,KAAK;CAChB;AACF;AAqCA,SAAS,YACP,SACA,aACA,SACgC;CAChC,MAAM,QAAQ,SAAS,SAAS;CAEhC,OAAO,QAAQ,KACb,OAAO,oBACC,QAAQ,SAAS,QAAQ,CAAC,CAAC,IAChC,UAAU,UAAU;EACnB,MAAM,UAAU,MAAM,SAAS,UAAU;GACvC,MAAM,SAAS,SAAS,KAAK,KAAK;GAElC,OAAO,WAAW,KAAA,KAAa,MAAM,YAAY,QAAQ,CAAC,IAAI,CAAC,MAAM;EACvE,CAAC;EAED,MAAM,SAAS,MAAM,OAAO;EAE5B,OAAO,CAAC,UAAU,WAAW,KAAA,IAAY,CAAC,IAAI,CAAC,MAAM,CAAC;CACxD,CACF,CACF;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiDA,MAAa,UAUT,MAAM,SAAS,OAAO,SAAS,KAAK,EAAE,GAAG,WAAW"}
@@ -128,7 +128,7 @@ export declare const staleApprovals: (entries: ReadonlyArray<AnyEntry>, options:
128
128
  * yield* BlockKit.closeStale({ messages: 10 })
129
129
  * yield* ToolApproval.closeStale({ messages: 10 })
130
130
  *
131
- * return turn.loop({ instructions, toolkit: MyTools })
131
+ * return turn.run({ instructions, toolkit: MyTools })
132
132
  * })
133
133
  * )
134
134
  * ```
@@ -180,7 +180,7 @@ const staleApprovals = (entries, options) => {
180
180
  * yield* BlockKit.closeStale({ messages: 10 })
181
181
  * yield* ToolApproval.closeStale({ messages: 10 })
182
182
  *
183
- * return turn.loop({ instructions, toolkit: MyTools })
183
+ * return turn.run({ instructions, toolkit: MyTools })
184
184
  * })
185
185
  * )
186
186
  * ```
@@ -1 +1 @@
1
- {"version":3,"file":"ToolApproval.js","names":["AssistantEvent.isLibraryEvent","InputId.fromKey"],"sources":["../src/ToolApproval.ts"],"sourcesContent":["/**\n * Tool calls that wait for the user's approval.\n *\n * A tool made with Effect's `needsApproval` doesn't run when the model calls it in a turn: the\n * call waits ({@link AssistantEvent.ApprovalRequested}), the turn ends, and the chat's timeline\n * shows it to the user. Their decision, built with {@link approve} or {@link reject} and submitted\n * like a message, wakes a turn: an approved call runs then, with its params and id, and a rejected\n * one doesn't. A call the user moved on from expires ({@link closeStale}, like `BlockKit.closeStale`). On the timeline, every\n * call is an item with its tool's name and where it stands: only the yes or no, since the\n * assistant's message says what it asks.\n *\n * @since 0.4.0\n */\nimport * as Effect from \"effect/Effect\"\nimport * as Predicate from \"effect/Predicate\"\nimport * as AssistantEvent from \"./AssistantEvent.ts\"\nimport { CurrentChat } from \"./CurrentChat.ts\"\nimport * as InputId from \"./InputId.ts\"\n\n/**\n * Represents where a call that waits for approval stands: `\"pending\"` until the user decides,\n * then `\"approved\"` or `\"rejected\"`, or `\"expired\"` when the user moved on.\n *\n * @category models\n * @since 0.4.0\n */\nexport type Status = \"pending\" | \"approved\" | \"rejected\" | \"expired\"\n\n/**\n * Represents a tool call that waited, or waits, for the user's approval.\n *\n * @category models\n * @since 0.4.0\n */\nexport interface State {\n readonly request: AssistantEvent.ApprovalRequested\n /** When the call started waiting, in epoch milliseconds. */\n readonly at: number\n readonly status: Status\n /** The user's reason, when they rejected it with one. */\n readonly reason: string | undefined\n /** How the approved call went, once it ran. */\n readonly outcome: AssistantEvent.ToolOutcome | undefined\n}\n\nconst decided = (state: State, decision: AssistantEvent.ApprovalDecision): State =>\n decision.reason === undefined\n ? { ...state, status: decision.decision }\n : { ...state, status: decision.decision, reason: decision.reason }\n\n/**\n * Returns the approvals after one more entry. Fold a chat's entries in order, or use\n * {@link fromEntries}.\n *\n * **Details**\n *\n * The first of a call's decision and its expiry counts: anything after it changes nothing.\n *\n * @category combinators\n * @since 0.4.0\n */\nexport const apply = (\n states: ReadonlyMap<string, State>,\n entry: AssistantEvent.AnyEntry\n): ReadonlyMap<string, State> => {\n const { event } = entry\n\n if (!AssistantEvent.isLibraryEvent(event)) return states\n\n if (Predicate.isTagged(event, \"ApprovalRequested\")) {\n return states.has(event.callId)\n ? states\n : new Map(states).set(event.callId, {\n request: event,\n at: entry.at,\n status: \"pending\",\n reason: undefined,\n outcome: undefined\n })\n }\n\n if (Predicate.isTagged(event, \"ApprovalDecision\")) {\n const state = states.get(event.callId)\n\n return state === undefined || state.status !== \"pending\"\n ? states\n : new Map(states).set(event.callId, decided(state, event))\n }\n\n if (Predicate.isTagged(event, \"ApprovalsExpired\")) {\n const expired = event.callIds.flatMap((callId) => {\n const state = states.get(callId)\n\n return state === undefined || state.status !== \"pending\" ? [] : [{ ...state, status: \"expired\" as const }]\n })\n\n if (expired.length === 0) return states\n\n const next = new Map(states)\n\n for (const state of expired) next.set(state.request.callId, state)\n\n return next\n }\n\n if (Predicate.isTagged(event, \"ToolResult\")) {\n const state = states.get(event.callId)\n\n return state === undefined || state.status !== \"approved\"\n ? states\n : new Map(states).set(event.callId, { ...state, outcome: event.outcome })\n }\n\n return states\n}\n\n/**\n * Returns every call that waited for approval in a chat, by call id, with where it stands.\n *\n * @category constructors\n * @since 0.4.0\n */\nexport const fromEntries = (entries: ReadonlyArray<AssistantEvent.AnyEntry>): ReadonlyMap<string, State> =>\n entries.reduce(apply, new Map<string, State>())\n\n/**\n * The id of a decision on a call: the same for every decision on it, so the chat stores one.\n *\n * @category utils\n * @since 0.4.0\n */\nexport const decisionId = (callId: string): InputId.InputId => InputId.fromKey(`approval:${callId}`)\n\n/**\n * Returns the input that approves a call waiting for the user's approval, to submit like a\n * message. `approval` is the call's item on the chat's timeline, or anything with the call's id.\n *\n * **Example** (Approve and reject buttons on the client)\n *\n * ```tsx\n * import { ToolApproval } from \"effect-assistant\"\n *\n * const buttons = (approval: { readonly id: string; readonly status: ToolApproval.Status }) =>\n * approval.status === \"pending\" ? (\n * <>\n * <button onClick={() => submit(ToolApproval.approve(approval))}>Approve</button>\n * <button onClick={() => submit(ToolApproval.reject(approval, { reason: \"Not this week\" }))}>Reject</button>\n * </>\n * ) : null\n * ```\n *\n * @category constructors\n * @since 0.4.0\n */\nexport const approve = (approval: { readonly id: string }): AssistantEvent.ApprovalDecision =>\n AssistantEvent.ApprovalDecision.make({ id: decisionId(approval.id), callId: approval.id, decision: \"approved\" })\n\n/**\n * Returns the input that rejects a call waiting for the user's approval, with an optional short\n * reason the model reads.\n *\n * @see {@link approve} for an example\n *\n * @category constructors\n * @since 0.4.0\n */\nexport const reject = (\n approval: { readonly id: string },\n options?: { readonly reason?: string | undefined }\n): AssistantEvent.ApprovalDecision =>\n options?.reason === undefined\n ? AssistantEvent.ApprovalDecision.make({ id: decisionId(approval.id), callId: approval.id, decision: \"rejected\" })\n : AssistantEvent.ApprovalDecision.make({\n id: decisionId(approval.id),\n callId: approval.id,\n decision: \"rejected\",\n reason: options.reason\n })\n\n/**\n * Returns the ids of the calls still waiting for approval after the user typed the given number\n * of messages without deciding them.\n *\n * **Details**\n *\n * These are the calls {@link closeStale} closes.\n *\n * @category utils\n * @since 0.4.0\n */\nexport const staleApprovals = (\n entries: ReadonlyArray<AssistantEvent.AnyEntry>,\n options: { readonly messages: number }\n): ReadonlyArray<string> => {\n const states = fromEntries(entries)\n\n return [...states.values()].flatMap((state) => {\n if (state.status !== \"pending\") return []\n\n const requested = entries.findIndex(\n ({ event }) =>\n AssistantEvent.isLibraryEvent(event) &&\n Predicate.isTagged(event, \"ApprovalRequested\") &&\n event.callId === state.request.callId\n )\n\n const typed = entries.slice(requested + 1).filter(({ event }) => Predicate.isTagged(event, \"UserMessage\")).length\n\n return typed >= options.messages ? [state.request.callId] : []\n })\n}\n\n/**\n * Closes the calls the user moved on from, which still wait for approval after the given number\n * of typed messages: they expire, and won't run.\n *\n * **Details**\n *\n * Run it from a turn, before or after its loop, like `BlockKit.closeStale`. It records an\n * `ApprovalsExpired` event: the timeline shows the calls as expired, the model reads that they\n * weren't approved, and a decision that arrives later isn't stored.\n *\n * **Example** (Closing calls the user moved on from, and stale blocks, every turn)\n *\n * ```ts\n * import { BlockKit, ToolApproval } from \"effect-assistant\"\n * import * as Effect from \"effect/Effect\"\n *\n * const MyAssistantLive = MyAssistant.toLayer(\n * Effect.fn(function* (turn) {\n * yield* BlockKit.closeStale({ messages: 10 })\n * yield* ToolApproval.closeStale({ messages: 10 })\n *\n * return turn.loop({ instructions, toolkit: MyTools })\n * })\n * )\n * ```\n *\n * @see {@link staleApprovals} for the calls it closes\n *\n * @category utils\n * @since 0.4.0\n */\nexport const closeStale = Effect.fn(function* (options: { readonly messages: number }) {\n const chat = yield* CurrentChat\n const stale = staleApprovals(yield* chat.entries, options)\n\n if (stale.length > 0) yield* chat.append(AssistantEvent.ApprovalsExpired.make({ callIds: stale }))\n})\n"],"mappings":"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"}
1
+ {"version":3,"file":"ToolApproval.js","names":["AssistantEvent.isLibraryEvent","InputId.fromKey"],"sources":["../src/ToolApproval.ts"],"sourcesContent":["/**\n * Tool calls that wait for the user's approval.\n *\n * A tool made with Effect's `needsApproval` doesn't run when the model calls it in a turn: the\n * call waits ({@link AssistantEvent.ApprovalRequested}), the turn ends, and the chat's timeline\n * shows it to the user. Their decision, built with {@link approve} or {@link reject} and submitted\n * like a message, wakes a turn: an approved call runs then, with its params and id, and a rejected\n * one doesn't. A call the user moved on from expires ({@link closeStale}, like `BlockKit.closeStale`). On the timeline, every\n * call is an item with its tool's name and where it stands: only the yes or no, since the\n * assistant's message says what it asks.\n *\n * @since 0.4.0\n */\nimport * as Effect from \"effect/Effect\"\nimport * as Predicate from \"effect/Predicate\"\nimport * as AssistantEvent from \"./AssistantEvent.ts\"\nimport { CurrentChat } from \"./CurrentChat.ts\"\nimport * as InputId from \"./InputId.ts\"\n\n/**\n * Represents where a call that waits for approval stands: `\"pending\"` until the user decides,\n * then `\"approved\"` or `\"rejected\"`, or `\"expired\"` when the user moved on.\n *\n * @category models\n * @since 0.4.0\n */\nexport type Status = \"pending\" | \"approved\" | \"rejected\" | \"expired\"\n\n/**\n * Represents a tool call that waited, or waits, for the user's approval.\n *\n * @category models\n * @since 0.4.0\n */\nexport interface State {\n readonly request: AssistantEvent.ApprovalRequested\n /** When the call started waiting, in epoch milliseconds. */\n readonly at: number\n readonly status: Status\n /** The user's reason, when they rejected it with one. */\n readonly reason: string | undefined\n /** How the approved call went, once it ran. */\n readonly outcome: AssistantEvent.ToolOutcome | undefined\n}\n\nconst decided = (state: State, decision: AssistantEvent.ApprovalDecision): State =>\n decision.reason === undefined\n ? { ...state, status: decision.decision }\n : { ...state, status: decision.decision, reason: decision.reason }\n\n/**\n * Returns the approvals after one more entry. Fold a chat's entries in order, or use\n * {@link fromEntries}.\n *\n * **Details**\n *\n * The first of a call's decision and its expiry counts: anything after it changes nothing.\n *\n * @category combinators\n * @since 0.4.0\n */\nexport const apply = (\n states: ReadonlyMap<string, State>,\n entry: AssistantEvent.AnyEntry\n): ReadonlyMap<string, State> => {\n const { event } = entry\n\n if (!AssistantEvent.isLibraryEvent(event)) return states\n\n if (Predicate.isTagged(event, \"ApprovalRequested\")) {\n return states.has(event.callId)\n ? states\n : new Map(states).set(event.callId, {\n request: event,\n at: entry.at,\n status: \"pending\",\n reason: undefined,\n outcome: undefined\n })\n }\n\n if (Predicate.isTagged(event, \"ApprovalDecision\")) {\n const state = states.get(event.callId)\n\n return state === undefined || state.status !== \"pending\"\n ? states\n : new Map(states).set(event.callId, decided(state, event))\n }\n\n if (Predicate.isTagged(event, \"ApprovalsExpired\")) {\n const expired = event.callIds.flatMap((callId) => {\n const state = states.get(callId)\n\n return state === undefined || state.status !== \"pending\" ? [] : [{ ...state, status: \"expired\" as const }]\n })\n\n if (expired.length === 0) return states\n\n const next = new Map(states)\n\n for (const state of expired) next.set(state.request.callId, state)\n\n return next\n }\n\n if (Predicate.isTagged(event, \"ToolResult\")) {\n const state = states.get(event.callId)\n\n return state === undefined || state.status !== \"approved\"\n ? states\n : new Map(states).set(event.callId, { ...state, outcome: event.outcome })\n }\n\n return states\n}\n\n/**\n * Returns every call that waited for approval in a chat, by call id, with where it stands.\n *\n * @category constructors\n * @since 0.4.0\n */\nexport const fromEntries = (entries: ReadonlyArray<AssistantEvent.AnyEntry>): ReadonlyMap<string, State> =>\n entries.reduce(apply, new Map<string, State>())\n\n/**\n * The id of a decision on a call: the same for every decision on it, so the chat stores one.\n *\n * @category utils\n * @since 0.4.0\n */\nexport const decisionId = (callId: string): InputId.InputId => InputId.fromKey(`approval:${callId}`)\n\n/**\n * Returns the input that approves a call waiting for the user's approval, to submit like a\n * message. `approval` is the call's item on the chat's timeline, or anything with the call's id.\n *\n * **Example** (Approve and reject buttons on the client)\n *\n * ```tsx\n * import { ToolApproval } from \"effect-assistant\"\n *\n * const buttons = (approval: { readonly id: string; readonly status: ToolApproval.Status }) =>\n * approval.status === \"pending\" ? (\n * <>\n * <button onClick={() => submit(ToolApproval.approve(approval))}>Approve</button>\n * <button onClick={() => submit(ToolApproval.reject(approval, { reason: \"Not this week\" }))}>Reject</button>\n * </>\n * ) : null\n * ```\n *\n * @category constructors\n * @since 0.4.0\n */\nexport const approve = (approval: { readonly id: string }): AssistantEvent.ApprovalDecision =>\n AssistantEvent.ApprovalDecision.make({ id: decisionId(approval.id), callId: approval.id, decision: \"approved\" })\n\n/**\n * Returns the input that rejects a call waiting for the user's approval, with an optional short\n * reason the model reads.\n *\n * @see {@link approve} for an example\n *\n * @category constructors\n * @since 0.4.0\n */\nexport const reject = (\n approval: { readonly id: string },\n options?: { readonly reason?: string | undefined }\n): AssistantEvent.ApprovalDecision =>\n options?.reason === undefined\n ? AssistantEvent.ApprovalDecision.make({ id: decisionId(approval.id), callId: approval.id, decision: \"rejected\" })\n : AssistantEvent.ApprovalDecision.make({\n id: decisionId(approval.id),\n callId: approval.id,\n decision: \"rejected\",\n reason: options.reason\n })\n\n/**\n * Returns the ids of the calls still waiting for approval after the user typed the given number\n * of messages without deciding them.\n *\n * **Details**\n *\n * These are the calls {@link closeStale} closes.\n *\n * @category utils\n * @since 0.4.0\n */\nexport const staleApprovals = (\n entries: ReadonlyArray<AssistantEvent.AnyEntry>,\n options: { readonly messages: number }\n): ReadonlyArray<string> => {\n const states = fromEntries(entries)\n\n return [...states.values()].flatMap((state) => {\n if (state.status !== \"pending\") return []\n\n const requested = entries.findIndex(\n ({ event }) =>\n AssistantEvent.isLibraryEvent(event) &&\n Predicate.isTagged(event, \"ApprovalRequested\") &&\n event.callId === state.request.callId\n )\n\n const typed = entries.slice(requested + 1).filter(({ event }) => Predicate.isTagged(event, \"UserMessage\")).length\n\n return typed >= options.messages ? [state.request.callId] : []\n })\n}\n\n/**\n * Closes the calls the user moved on from, which still wait for approval after the given number\n * of typed messages: they expire, and won't run.\n *\n * **Details**\n *\n * Run it from a turn, before or after its loop, like `BlockKit.closeStale`. It records an\n * `ApprovalsExpired` event: the timeline shows the calls as expired, the model reads that they\n * weren't approved, and a decision that arrives later isn't stored.\n *\n * **Example** (Closing calls the user moved on from, and stale blocks, every turn)\n *\n * ```ts\n * import { BlockKit, ToolApproval } from \"effect-assistant\"\n * import * as Effect from \"effect/Effect\"\n *\n * const MyAssistantLive = MyAssistant.toLayer(\n * Effect.fn(function* (turn) {\n * yield* BlockKit.closeStale({ messages: 10 })\n * yield* ToolApproval.closeStale({ messages: 10 })\n *\n * return turn.run({ instructions, toolkit: MyTools })\n * })\n * )\n * ```\n *\n * @see {@link staleApprovals} for the calls it closes\n *\n * @category utils\n * @since 0.4.0\n */\nexport const closeStale = Effect.fn(function* (options: { readonly messages: number }) {\n const chat = yield* CurrentChat\n const stale = staleApprovals(yield* chat.entries, options)\n\n if (stale.length > 0) yield* chat.append(AssistantEvent.ApprovalsExpired.make({ callIds: stale }))\n})\n"],"mappings":"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"}
@@ -31,7 +31,7 @@ declare namespace ToolDiscovery_d_exports {
31
31
  * const OfficeDiscoveryLive = ToolDiscovery.layer(OfficeTools).pipe(Layer.provide(OfficeToolsLive))
32
32
  *
33
33
  * const MyAssistantLive = MyAssistant.toLayer((turn) =>
34
- * turn.loop({
34
+ * turn.run({
35
35
  * instructions,
36
36
  * toolkit: Toolkit.merge(CommonTools, OfficeDiscovery)
37
37
  * })
@@ -83,7 +83,7 @@ const requiring = () => (tool) => tool;
83
83
  * const OfficeDiscoveryLive = ToolDiscovery.layer(OfficeTools).pipe(Layer.provide(OfficeToolsLive))
84
84
  *
85
85
  * const MyAssistantLive = MyAssistant.toLayer((turn) =>
86
- * turn.loop({
86
+ * turn.run({
87
87
  * instructions,
88
88
  * toolkit: Toolkit.merge(CommonTools, OfficeDiscovery)
89
89
  * })
@@ -1 +1 @@
1
- {"version":3,"file":"ToolDiscovery.js","names":["needingApproval","Toolkits.needingApproval","Toolkits.runNative"],"sources":["../src/ToolDiscovery.ts"],"sourcesContent":["/**\n * Turns a catalog toolkit into two tools, so the prompt doesn't carry every tool definition.\n *\n * `search_tools` finds tools by what the model needs to do and returns their names, descriptions,\n * and input schemas. `execute_tool` validates an input and runs one of them. {@link make} returns\n * the two tools as a native toolkit, and {@link layer} their handlers.\n *\n * @since 0.1.0\n */\nimport * as Response from \"effect/ai/Response\"\nimport * as Tool from \"effect/ai/Tool\"\nimport * as Toolkit from \"effect/ai/Toolkit\"\nimport * as Effect from \"effect/Effect\"\nimport * as Schema from \"effect/Schema\"\nimport { AssistantToolCall } from \"./AssistantToolCall.ts\"\nimport { toJson } from \"./internal/schema.ts\"\nimport * as Toolkits from \"./internal/toolkit.ts\"\n\nconst words = (text: string): ReadonlySet<string> =>\n new Set(\n text\n .toLowerCase()\n .split(/[^a-z0-9]+/)\n .filter((word) => word.length >= 3)\n )\n\nconst score = (query: ReadonlySet<string>, text: string): number => {\n const target = [...words(text)]\n\n const matches = (word: string) =>\n target.some((t) => t === word || (word.length >= 4 && (t.startsWith(word) || word.startsWith(t))))\n\n return [...query].filter(matches).length\n}\n\n// Everything when nothing matches, so the model can pick or search again.\nconst search = (tools: ReadonlyArray<Tool.Any>, query: string, limit: number): ReadonlyArray<Tool.Any> => {\n const queryWords = words(query)\n\n const ranked = tools\n .flatMap((tool) => {\n const hits = score(queryWords, `${tool.name} ${Tool.getDescription(tool) ?? \"\"}`)\n\n return hits > 0 ? [{ tool, score: hits }] : []\n })\n .sort((a, b) => b.score - a.score)\n\n return (ranked.length > 0 ? ranked.map((hit) => hit.tool) : tools).slice(0, limit)\n}\n\nconst SearchTools = Tool.make(\"search_tools\", {\n description:\n \"Search for more tools by what you need to do. Returns their names, descriptions, and input schemas; call one with execute_tool.\",\n parameters: Schema.Struct({\n query: Schema.String.annotate({ description: \"What you need to do, in a few words.\" })\n }),\n success: Schema.Array(\n Schema.Struct({\n name: Schema.String,\n description: Schema.optional(Schema.String),\n inputSchema: Schema.Json\n })\n )\n})\n\nconst ExecuteTool = Tool.make(\"execute_tool\", {\n description: \"Run a tool found with search_tools. Pass its name and an input matching its input schema.\",\n parameters: Schema.Struct({\n name: Schema.String,\n input: Schema.Record(Schema.String, Schema.Json)\n }),\n success: Schema.Json,\n // The tool's failure is this call's, so the loop sees it failed (a `wait: true` message doesn't\n // end the turn).\n failure: Schema.Json,\n dependencies: [AssistantToolCall]\n})\n\n// A tool's requirements are a covariant phantom type, so one that needs nothing is one that needs\n// any.\nconst requiring =\n <R>() =>\n <N extends string, C extends (Tool.Any extends Tool.Tool<string, infer K, unknown> ? K : never), R0>(\n tool: Tool.Tool<N, C, R0>\n ): Tool.Tool<N, C, R0 | R> =>\n tool\n\n/**\n * Creates the `search_tools` and `execute_tool` tools over a catalog toolkit, as a native toolkit.\n *\n * **Details**\n *\n * Merge the toolkit into the loop's, and provide {@link layer} for the same catalog, which has\n * the tools' handlers. A catalog tool's services are `execute_tool`'s, so the turn provides them\n * as it does for any tool.\n *\n * A catalog tool with Effect's `needsApproval` set fails the layer when it is built: a call\n * through `execute_tool` can't wait for the user's approval. Give such a tool to the loop's toolkit\n * directly (see `ToolApproval`).\n *\n * **Example** (Finding rarely used tools when needed)\n *\n * ```ts\n * import { ToolDiscovery } from \"effect-assistant\"\n * import * as Toolkit from \"effect/ai/Toolkit\"\n * import * as Layer from \"effect/Layer\"\n *\n * // OfficeTools is a big toolkit, found when needed. CommonTools stays in the prompt.\n * const OfficeDiscovery = ToolDiscovery.make(OfficeTools)\n * const OfficeDiscoveryLive = ToolDiscovery.layer(OfficeTools).pipe(Layer.provide(OfficeToolsLive))\n *\n * const MyAssistantLive = MyAssistant.toLayer((turn) =>\n * turn.loop({\n * instructions,\n * toolkit: Toolkit.merge(CommonTools, OfficeDiscovery)\n * })\n * ).pipe(Layer.provide([CommonToolsLive, OfficeDiscoveryLive]))\n * ```\n *\n * @see {@link layer} for the tools' handlers\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const make = <T extends Record<string, Tool.Any>>(\n // Only its type is used here, for `execute_tool`'s services: `layer` runs its tools.\n _catalog: Toolkit.Toolkit<T>\n) => Toolkit.make(SearchTools, requiring<Toolkits.ToolServices<T>>()(ExecuteTool))\n\n/**\n * Creates the layer with the handlers of the `search_tools` and `execute_tool` tools over a\n * catalog toolkit.\n *\n * **Details**\n *\n * The layer needs the catalog's handlers. It provides the handlers of the toolkit that\n * {@link make} returns for the same catalog: handlers are keyed by tool name, so the two calls\n * don't need to share a value. Give both the same catalog.\n *\n * @see {@link make} for the toolkit and an example\n *\n * @category layers\n * @since 0.1.0\n */\nexport const layer = <T extends Record<string, Tool.Any>>(catalog: Toolkit.Toolkit<T>) =>\n make(catalog).toLayer(\n Effect.gen(function* () {\n const resolved = yield* catalog\n const definitions = Object.values(resolved.tools)\n const needingApproval = Toolkits.needingApproval(definitions)\n\n if (needingApproval.length > 0) {\n return yield* Effect.die(\n `${needingApproval.join(\", \")} ${needingApproval.length === 1 ? \"sets\" : \"set\"} needsApproval, and a call through execute_tool can't wait for the user's approval. Give it to the loop's toolkit instead of ToolDiscovery's catalog.`\n )\n }\n\n return {\n search_tools: ({ query }) =>\n Effect.sync(() =>\n search(definitions, query, 5).map((tool) => ({\n name: tool.name,\n description: Tool.getDescription(tool),\n inputSchema: toJson(Tool.getJsonSchema(tool))\n }))\n ),\n execute_tool: Effect.fnUntraced(function* ({ name, input }) {\n // Named after the call that runs it: the same id when a crash runs that call again.\n const outer = yield* AssistantToolCall\n\n const call = Response.makePart(\"tool-call\", {\n id: `${outer.callId}:${name}`,\n name,\n params: input,\n providerExecuted: false\n })\n\n const { outcome, result } = yield* Toolkits.runNative(resolved, call)\n\n return outcome === \"failure\" ? yield* Effect.fail(result) : result\n })\n }\n })\n )\n"],"mappings":"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"}
1
+ {"version":3,"file":"ToolDiscovery.js","names":["needingApproval","Toolkits.needingApproval","Toolkits.runNative"],"sources":["../src/ToolDiscovery.ts"],"sourcesContent":["/**\n * Turns a catalog toolkit into two tools, so the prompt doesn't carry every tool definition.\n *\n * `search_tools` finds tools by what the model needs to do and returns their names, descriptions,\n * and input schemas. `execute_tool` validates an input and runs one of them. {@link make} returns\n * the two tools as a native toolkit, and {@link layer} their handlers.\n *\n * @since 0.1.0\n */\nimport * as Response from \"effect/ai/Response\"\nimport * as Tool from \"effect/ai/Tool\"\nimport * as Toolkit from \"effect/ai/Toolkit\"\nimport * as Effect from \"effect/Effect\"\nimport * as Schema from \"effect/Schema\"\nimport { AssistantToolCall } from \"./AssistantToolCall.ts\"\nimport { toJson } from \"./internal/schema.ts\"\nimport * as Toolkits from \"./internal/toolkit.ts\"\n\nconst words = (text: string): ReadonlySet<string> =>\n new Set(\n text\n .toLowerCase()\n .split(/[^a-z0-9]+/)\n .filter((word) => word.length >= 3)\n )\n\nconst score = (query: ReadonlySet<string>, text: string): number => {\n const target = [...words(text)]\n\n const matches = (word: string) =>\n target.some((t) => t === word || (word.length >= 4 && (t.startsWith(word) || word.startsWith(t))))\n\n return [...query].filter(matches).length\n}\n\n// Everything when nothing matches, so the model can pick or search again.\nconst search = (tools: ReadonlyArray<Tool.Any>, query: string, limit: number): ReadonlyArray<Tool.Any> => {\n const queryWords = words(query)\n\n const ranked = tools\n .flatMap((tool) => {\n const hits = score(queryWords, `${tool.name} ${Tool.getDescription(tool) ?? \"\"}`)\n\n return hits > 0 ? [{ tool, score: hits }] : []\n })\n .sort((a, b) => b.score - a.score)\n\n return (ranked.length > 0 ? ranked.map((hit) => hit.tool) : tools).slice(0, limit)\n}\n\nconst SearchTools = Tool.make(\"search_tools\", {\n description:\n \"Search for more tools by what you need to do. Returns their names, descriptions, and input schemas; call one with execute_tool.\",\n parameters: Schema.Struct({\n query: Schema.String.annotate({ description: \"What you need to do, in a few words.\" })\n }),\n success: Schema.Array(\n Schema.Struct({\n name: Schema.String,\n description: Schema.optional(Schema.String),\n inputSchema: Schema.Json\n })\n )\n})\n\nconst ExecuteTool = Tool.make(\"execute_tool\", {\n description: \"Run a tool found with search_tools. Pass its name and an input matching its input schema.\",\n parameters: Schema.Struct({\n name: Schema.String,\n input: Schema.Record(Schema.String, Schema.Json)\n }),\n success: Schema.Json,\n // The tool's failure is this call's, so the loop sees it failed (a `wait: true` message doesn't\n // end the turn).\n failure: Schema.Json,\n dependencies: [AssistantToolCall]\n})\n\n// A tool's requirements are a covariant phantom type, so one that needs nothing is one that needs\n// any.\nconst requiring =\n <R>() =>\n <N extends string, C extends (Tool.Any extends Tool.Tool<string, infer K, unknown> ? K : never), R0>(\n tool: Tool.Tool<N, C, R0>\n ): Tool.Tool<N, C, R0 | R> =>\n tool\n\n/**\n * Creates the `search_tools` and `execute_tool` tools over a catalog toolkit, as a native toolkit.\n *\n * **Details**\n *\n * Merge the toolkit into the loop's, and provide {@link layer} for the same catalog, which has\n * the tools' handlers. A catalog tool's services are `execute_tool`'s, so the turn provides them\n * as it does for any tool.\n *\n * A catalog tool with Effect's `needsApproval` set fails the layer when it is built: a call\n * through `execute_tool` can't wait for the user's approval. Give such a tool to the loop's toolkit\n * directly (see `ToolApproval`).\n *\n * **Example** (Finding rarely used tools when needed)\n *\n * ```ts\n * import { ToolDiscovery } from \"effect-assistant\"\n * import * as Toolkit from \"effect/ai/Toolkit\"\n * import * as Layer from \"effect/Layer\"\n *\n * // OfficeTools is a big toolkit, found when needed. CommonTools stays in the prompt.\n * const OfficeDiscovery = ToolDiscovery.make(OfficeTools)\n * const OfficeDiscoveryLive = ToolDiscovery.layer(OfficeTools).pipe(Layer.provide(OfficeToolsLive))\n *\n * const MyAssistantLive = MyAssistant.toLayer((turn) =>\n * turn.run({\n * instructions,\n * toolkit: Toolkit.merge(CommonTools, OfficeDiscovery)\n * })\n * ).pipe(Layer.provide([CommonToolsLive, OfficeDiscoveryLive]))\n * ```\n *\n * @see {@link layer} for the tools' handlers\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const make = <T extends Record<string, Tool.Any>>(\n // Only its type is used here, for `execute_tool`'s services: `layer` runs its tools.\n _catalog: Toolkit.Toolkit<T>\n) => Toolkit.make(SearchTools, requiring<Toolkits.ToolServices<T>>()(ExecuteTool))\n\n/**\n * Creates the layer with the handlers of the `search_tools` and `execute_tool` tools over a\n * catalog toolkit.\n *\n * **Details**\n *\n * The layer needs the catalog's handlers. It provides the handlers of the toolkit that\n * {@link make} returns for the same catalog: handlers are keyed by tool name, so the two calls\n * don't need to share a value. Give both the same catalog.\n *\n * @see {@link make} for the toolkit and an example\n *\n * @category layers\n * @since 0.1.0\n */\nexport const layer = <T extends Record<string, Tool.Any>>(catalog: Toolkit.Toolkit<T>) =>\n make(catalog).toLayer(\n Effect.gen(function* () {\n const resolved = yield* catalog\n const definitions = Object.values(resolved.tools)\n const needingApproval = Toolkits.needingApproval(definitions)\n\n if (needingApproval.length > 0) {\n return yield* Effect.die(\n `${needingApproval.join(\", \")} ${needingApproval.length === 1 ? \"sets\" : \"set\"} needsApproval, and a call through execute_tool can't wait for the user's approval. Give it to the loop's toolkit instead of ToolDiscovery's catalog.`\n )\n }\n\n return {\n search_tools: ({ query }) =>\n Effect.sync(() =>\n search(definitions, query, 5).map((tool) => ({\n name: tool.name,\n description: Tool.getDescription(tool),\n inputSchema: toJson(Tool.getJsonSchema(tool))\n }))\n ),\n execute_tool: Effect.fnUntraced(function* ({ name, input }) {\n // Named after the call that runs it: the same id when a crash runs that call again.\n const outer = yield* AssistantToolCall\n\n const call = Response.makePart(\"tool-call\", {\n id: `${outer.callId}:${name}`,\n name,\n params: input,\n providerExecuted: false\n })\n\n const { outcome, result } = yield* Toolkits.runNative(resolved, call)\n\n return outcome === \"failure\" ? yield* Effect.fail(result) : result\n })\n }\n })\n )\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAkBA,MAAM,SAAS,SACb,IAAI,IACF,KACG,YAAY,CAAC,CACb,MAAM,YAAY,CAAC,CACnB,QAAQ,SAAS,KAAK,UAAU,CAAC,CACtC;AAEF,MAAM,SAAS,OAA4B,SAAyB;CAClE,MAAM,SAAS,CAAC,GAAG,MAAM,IAAI,CAAC;CAE9B,MAAM,WAAW,SACf,OAAO,MAAM,MAAM,MAAM,QAAS,KAAK,UAAU,MAAM,EAAE,WAAW,IAAI,KAAK,KAAK,WAAW,CAAC,EAAG;CAEnG,OAAO,CAAC,GAAG,KAAK,CAAC,CAAC,OAAO,OAAO,CAAC,CAAC;AACpC;AAGA,MAAM,UAAU,OAAgC,OAAe,UAA2C;CACxG,MAAM,aAAa,MAAM,KAAK;CAE9B,MAAM,SAAS,MACZ,SAAS,SAAS;EACjB,MAAM,OAAO,MAAM,YAAY,GAAG,KAAK,KAAK,GAAG,KAAK,eAAe,IAAI,KAAK,IAAI;EAEhF,OAAO,OAAO,IAAI,CAAC;GAAE;GAAM,OAAO;EAAK,CAAC,IAAI,CAAC;CAC/C,CAAC,CAAC,CACD,MAAM,GAAG,MAAM,EAAE,QAAQ,EAAE,KAAK;CAEnC,QAAQ,OAAO,SAAS,IAAI,OAAO,KAAK,QAAQ,IAAI,IAAI,IAAI,MAAA,CAAO,MAAM,GAAG,KAAK;AACnF;AAEA,MAAM,cAAc,KAAK,KAAK,gBAAgB;CAC5C,aACE;CACF,YAAY,OAAO,OAAO,EACxB,OAAO,OAAO,OAAO,SAAS,EAAE,aAAa,uCAAuC,CAAC,EACvF,CAAC;CACD,SAAS,OAAO,MACd,OAAO,OAAO;EACZ,MAAM,OAAO;EACb,aAAa,OAAO,SAAS,OAAO,MAAM;EAC1C,aAAa,OAAO;CACtB,CAAC,CACH;AACF,CAAC;AAED,MAAM,cAAc,KAAK,KAAK,gBAAgB;CAC5C,aAAa;CACb,YAAY,OAAO,OAAO;EACxB,MAAM,OAAO;EACb,OAAO,OAAO,OAAO,OAAO,QAAQ,OAAO,IAAI;CACjD,CAAC;CACD,SAAS,OAAO;CAGhB,SAAS,OAAO;CAChB,cAAc,CAAC,iBAAiB;AAClC,CAAC;AAID,MAAM,mBAGF,SAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuCJ,MAAa,QAEX,aACG,QAAQ,KAAK,aAAa,UAAoC,CAAC,CAAC,WAAW,CAAC;;;;;;;;;;;;;;;;AAiBjF,MAAa,SAA6C,YACxD,KAAK,OAAO,CAAC,CAAC,QACZ,OAAO,IAAI,aAAa;CACtB,MAAM,WAAW,OAAO;CACxB,MAAM,cAAc,OAAO,OAAO,SAAS,KAAK;CAChD,MAAMA,oBAAkBC,gBAAyB,WAAW;CAE5D,IAAID,kBAAgB,SAAS,GAC3B,OAAO,OAAO,OAAO,IACnB,GAAGA,kBAAgB,KAAK,IAAI,EAAE,GAAGA,kBAAgB,WAAW,IAAI,SAAS,MAAM,sJACjF;CAGF,OAAO;EACL,eAAe,EAAE,YACf,OAAO,WACL,OAAO,aAAa,OAAO,CAAC,CAAC,CAAC,KAAK,UAAU;GAC3C,MAAM,KAAK;GACX,aAAa,KAAK,eAAe,IAAI;GACrC,aAAa,OAAO,KAAK,cAAc,IAAI,CAAC;EAC9C,EAAE,CACJ;EACF,cAAc,OAAO,WAAW,WAAW,EAAE,MAAM,SAAS;GAE1D,MAAM,QAAQ,OAAO;GAErB,MAAM,OAAO,SAAS,SAAS,aAAa;IAC1C,IAAI,GAAG,MAAM,OAAO,GAAG;IACvB;IACA,QAAQ;IACR,kBAAkB;GACpB,CAAC;GAED,MAAM,EAAE,SAAS,WAAW,OAAOE,UAAmB,UAAU,IAAI;GAEpE,OAAO,YAAY,YAAY,OAAO,OAAO,KAAK,MAAM,IAAI;EAC9D,CAAC;CACH;AACF,CAAC,CACH"}
@@ -44,7 +44,7 @@ const deletedChat = (entries) => ({
44
44
  submit: () => Effect.void,
45
45
  submitInBackground: () => Effect.void,
46
46
  resume: Effect.void,
47
- stream: (after) => Stream.fromIterable(entries.filter((entry) => entry.seq > after)),
47
+ stream: (after) => Stream.fromIterable(entries.filter((entry) => entry.sequence > after)),
48
48
  delete: Effect.void
49
49
  });
50
50
  const refused = (input, why) => Effect.logDebug(`assistant chat ${why}: ${input._tag} not stored`);
@@ -165,10 +165,10 @@ const make = Effect.fnUntraced(function* (options) {
165
165
  stream: (after) => Stream.unwrap(Effect.gen(function* () {
166
166
  const subscription = yield* PubSub.subscribe(channel);
167
167
  const all = yield* chat.entries;
168
- const stored = all.filter((entry) => entry.seq > after);
169
- const last = stored.at(-1)?.seq ?? after;
168
+ const stored = all.filter((entry) => entry.sequence > after);
169
+ const last = stored.at(-1)?.sequence ?? after;
170
170
  if (all.some(isChatDeleted)) return Stream.fromIterable(stored);
171
- return Stream.concat(Stream.fromIterable(stored), Stream.fromSubscription(subscription).pipe(Stream.filter((entry) => entry.seq > last))).pipe(Stream.takeUntil(isChatDeleted));
171
+ return Stream.concat(Stream.fromIterable(stored), Stream.fromSubscription(subscription).pipe(Stream.filter((entry) => entry.sequence > last))).pipe(Stream.takeUntil(isChatDeleted));
172
172
  })),
173
173
  delete: remove
174
174
  };
@@ -1 +1 @@
1
- {"version":3,"file":"chat.js","names":["AssistantEvent.isLibraryEvent","AssistantEvent.isInput","Chats.Chats","AppEvents.isRegistered","AppEvents.encode","AppEvents.decodeEntries","ToolApproval.fromEntries","AppEvents.markInput","History.needsTurn","Turns.run","Specialists.makeRunner","SpecialistService.Runner"],"sources":["../../src/internal/chat.ts"],"sourcesContent":["import * as Cause from \"effect/Cause\"\nimport * as Context from \"effect/Context\"\nimport * as Deferred from \"effect/Deferred\"\nimport * as Effect from \"effect/Effect\"\nimport * as Exit from \"effect/Exit\"\nimport * as Fiber from \"effect/Fiber\"\nimport * as FiberSet from \"effect/FiberSet\"\nimport * as Predicate from \"effect/Predicate\"\nimport * as PubSub from \"effect/PubSub\"\nimport * as Ref from \"effect/Ref\"\nimport * as Scope from \"effect/Scope\"\nimport * as Semaphore from \"effect/Semaphore\"\nimport * as Stream from \"effect/Stream\"\nimport * as SynchronizedRef from \"effect/SynchronizedRef\"\nimport * as AssistantEvent from \"../AssistantEvent.ts\"\nimport type { ChatId } from \"../ChatId.ts\"\nimport { ChatLog } from \"../ChatLog.ts\"\nimport { CurrentChat } from \"../CurrentChat.ts\"\nimport * as Chats from \"../Chats.ts\"\nimport * as SpecialistService from \"../Specialist.ts\"\nimport * as ToolApproval from \"../ToolApproval.ts\"\nimport * as AppEvents from \"./events.ts\"\nimport * as History from \"./history.ts\"\nimport * as Specialists from \"./specialists.ts\"\nimport * as Turns from \"./turns.ts\"\nimport type { Writer } from \"./writer.ts\"\n\nconst idOf = (event: { readonly _tag: string }): string | undefined =>\n AssistantEvent.isLibraryEvent(event)\n ? AssistantEvent.isInput(event)\n ? event.id\n : undefined\n : Predicate.hasProperty(event, \"id\") && Predicate.isString(event.id)\n ? event.id\n : undefined\n\nconst makeChannelPubSub = Effect.acquireRelease(PubSub.unbounded<AssistantEvent.Entry>(), PubSub.shutdown)\n\nconst makeHolding = Effect.fnUntraced(function* (keepAlive: (enabled: boolean) => Effect.Effect<void>) {\n const holds = yield* SynchronizedRef.make(0)\n\n const acquire = SynchronizedRef.updateEffect(\n holds,\n Effect.fnUntraced(function* (n: number) {\n if (n !== 0) return n + 1\n\n yield* keepAlive(true)\n\n return 1\n })\n )\n\n const release = SynchronizedRef.updateEffect(\n holds,\n Effect.fnUntraced(function* (n: number) {\n if (n !== 1) return n - 1\n\n yield* keepAlive(false)\n\n return 0\n })\n )\n\n return (effect: Effect.Effect<void>) =>\n Effect.acquireUseRelease(\n acquire,\n () => effect,\n // An interrupted effect (a shutdown) keeps its hold, so the chat starts again and picks up where it was cut off.\n (_, exit) => (Exit.hasInterrupts(exit) ? Effect.void : release)\n )\n})\n\n/** @internal */\nexport interface Chat {\n readonly submit: (input: AssistantEvent.Input | AssistantEvent.StoredAppEvent) => Effect.Effect<void>\n readonly submitInBackground: (input: AssistantEvent.Input | AssistantEvent.StoredAppEvent) => Effect.Effect<void>\n // For inputs no turn has read, like a report stored during a shutdown.\n readonly resume: Effect.Effect<void>\n readonly stream: (after: number) => Stream.Stream<AssistantEvent.Entry>\n // Stops what works in the chat and ends its subscribers' streams; its log stays.\n readonly delete: Effect.Effect<void>\n}\n\nconst isChatDeleted = (entry: AssistantEvent.Entry) => Predicate.isTagged(entry.event, \"ChatDeleted\")\n\nconst deletedChat = (entries: ReadonlyArray<AssistantEvent.Entry>): Chat => ({\n submit: () => Effect.void,\n submitInBackground: () => Effect.void,\n resume: Effect.void,\n stream: (after) => Stream.fromIterable(entries.filter((entry) => entry.seq > after)),\n delete: Effect.void\n})\n\nconst refused = (input: { readonly _tag: string }, why: string) =>\n Effect.logDebug(`assistant chat ${why}: ${input._tag} not stored`)\n\n/** @internal */\nexport const make = Effect.fnUntraced(function* (options: {\n readonly registration: Turns.Registration\n readonly chatId: ChatId\n readonly keepAlive: (enabled: boolean) => Effect.Effect<void>\n}) {\n const { registration, chatId } = options\n const { app } = registration.definition\n const storage = yield* ChatLog\n const chats = yield* Chats.Chats\n const initial = yield* storage.read(chatId)\n\n if (initial.some(isChatDeleted)) return deletedChat(initial)\n\n const channel = yield* makeChannelPubSub\n const writes = yield* Semaphore.make(1)\n const turnsLock = yield* Semaphore.make(1)\n const holding = yield* makeHolding(options.keepAlive)\n const deleted = yield* Deferred.make<undefined>()\n // So deleting the chat stops all its work: its turns, runs, and the builds' resources.\n const working = yield* Scope.fork(yield* Effect.scope)\n const inWorking = Effect.provideService(Scope.Scope, working)\n const background = yield* inWorking(FiberSet.make())\n const requests = yield* inWorking(FiberSet.make())\n\n const isDeleted = Deferred.isDone(deleted)\n\n // The chat is its log's only writer, so it reads the log once and turns read memory.\n const entries = yield* Ref.make(initial)\n\n // Every writer goes through here, one at a time, so each append is numbered after the last. After a delete it\n // stores nothing.\n const write = Effect.fnUntraced(function* (\n events: ReadonlyArray<AssistantEvent.AssistantEvent | AssistantEvent.StoredAppEvent>\n ) {\n const unknown = events.find((event) => !AssistantEvent.isLibraryEvent(event) && !AppEvents.isRegistered(app, event))\n\n if (unknown !== undefined) {\n return yield* Effect.die(`${unknown._tag} isn't one of the ${registration.definition.name} assistant's events.`)\n }\n\n if (yield* isDeleted) return []\n\n return yield* stored(events)\n })\n\n // Uninterruptible: a stored append must reach memory and the subscribers too, or a turn misses it and replies twice.\n const stored = Effect.fnUntraced(function* (\n events: ReadonlyArray<AssistantEvent.AssistantEvent | AssistantEvent.StoredAppEvent>\n ) {\n const stored = yield* storage.append(chatId, events)\n\n yield* Ref.update(entries, (all) => [...all, ...stored])\n yield* PubSub.publishAll(channel, stored)\n\n return stored\n }, Effect.uninterruptible)\n\n const chat: Writer = {\n chatId,\n entries: Ref.get(entries),\n append: (events) => Semaphore.withPermit(writes)(write(events))\n }\n\n const toStored = (event: {\n readonly _tag: string\n }): Effect.Effect<AssistantEvent.AssistantEvent | AssistantEvent.StoredAppEvent> =>\n AssistantEvent.isLibraryEvent(event)\n ? Effect.succeed(event)\n : AppEvents.isRegistered(app, event)\n ? AppEvents.encode(app, event)\n : Effect.die(`${event._tag} isn't one of the ${registration.definition.name} assistant's events.`)\n\n // What handlers, tools, and runs see: the assistant's own events decoded, and encoded when appended.\n const handle: Turns.TypedChat = {\n chatId,\n entries: Effect.map(chat.entries, (all) => AppEvents.decodeEntries(app, all)),\n append: (...events) => Effect.asVoid(Effect.flatMap(Effect.forEach(events, toStored), chat.append))\n }\n\n // A chat the app deleted is deleted here too, which stops what works in it: a turn that checks stops there.\n const checkAvailable: Effect.Effect<boolean> = chats.get(chatId).pipe(\n Effect.as(true),\n Effect.catchTag(\"GetChatError\", (unavailable) =>\n unavailable.reason === \"Deleted\"\n ? Effect.as(\n Effect.suspend(() => remove),\n false\n )\n : Effect.succeed(false)\n )\n )\n\n const store = Effect.fnUntraced(function* (input: AssistantEvent.Input | AssistantEvent.StoredAppEvent) {\n if (yield* isDeleted) return yield* refused(input, \"is deleted\")\n\n if (!(yield* checkAvailable)) return yield* refused(input, \"is unavailable\")\n\n // Checked under the write lock: a retry that arrives while the first submit is storing is stored once.\n yield* Semaphore.withPermit(writes)(\n Effect.gen(function* () {\n if (yield* isDeleted) return yield* refused(input, \"is deleted\")\n\n const id = idOf(input)\n const all = yield* Ref.get(entries)\n\n if (id !== undefined && all.some(({ event }) => idOf(event) === id)) return\n\n // A decision counts only for a call that still waits: not one decided, or expired, or unknown.\n if (\n Predicate.isTagged(input, \"ApprovalDecision\") &&\n ToolApproval.fromEntries(all).get(input.callId)?.status !== \"pending\"\n ) {\n return yield* refused(input, \"has no call waiting for that decision\")\n }\n\n // Stored as an input: it came in from outside a turn, so it wakes one, also after a restart.\n yield* write([AssistantEvent.isLibraryEvent(input) ? input : AppEvents.markInput(input)])\n })\n )\n })\n\n // The handlers' builds run in the chat's working scope: what they set up lives until the chat closes or is deleted.\n const handler = yield* inWorking(Effect.provideService(registration.open(handle), CurrentChat, handle))\n\n const specialists = yield* Effect.forEach(\n registration.specialists,\n Effect.fnUntraced(function* ({ specialist, built }) {\n const handler = yield* inWorking(Effect.provideService(built.open, CurrentChat, handle))\n\n return { specialist, handler }\n })\n )\n\n const inBackground = (effect: Effect.Effect<void>) => FiberSet.run(background, holding(effect)).pipe(Effect.asVoid)\n\n // A request's turns run in the working scope too, so a delete stops them; the request ends quietly then.\n const inRequest = Effect.fnUntraced(function* (effect: Effect.Effect<void>) {\n const fiber = yield* FiberSet.run(requests, effect)\n const exit = yield* Fiber.await(fiber).pipe(Effect.onInterrupt(() => Fiber.interrupt(fiber)))\n\n if (Exit.isFailure(exit) && !Cause.hasInterruptsOnly(exit.cause)) {\n return yield* Effect.failCause(exit.cause)\n }\n }, Effect.orDie)\n\n const drain: Effect.Effect<void> = Semaphore.withPermit(turnsLock)(\n Effect.gen(function* () {\n while (!(yield* isDeleted) && History.needsTurn(yield* chat.entries)) {\n yield* Turns.run({ registration, handler, chat, services, checkOpen: Effect.asVoid(checkAvailable) })\n }\n })\n )\n\n const runner = yield* inWorking(\n Specialists.makeRunner({\n chat,\n handle,\n specialists,\n model: registration.model,\n holding,\n report: Effect.fnUntraced(function* (finished) {\n yield* store(finished)\n yield* inBackground(drain)\n })\n })\n )\n\n const services = Context.make(CurrentChat, handle).pipe(\n Context.add(SpecialistService.Runner, runner),\n Context.merge(registration.model)\n )\n\n const remove = Effect.gen(function* () {\n // Under the write lock: an append in flight is stored before the `ChatDeleted`; none comes after.\n const first = yield* Semaphore.withPermit(writes)(\n Effect.gen(function* () {\n if (yield* isDeleted) return false\n\n yield* stored([AssistantEvent.ChatDeleted.make({})])\n\n return yield* Deferred.succeed(deleted, undefined)\n })\n ).pipe(Effect.uninterruptible)\n\n if (!first) return\n\n // Its own fiber: a tool that deletes its own chat is in the working scope, and closing it interrupts that tool.\n // Interrupted work keeps its hold (see `makeHolding`), so the chat lets go of it here.\n const closing = yield* Effect.forkDetach(\n Effect.gen(function* () {\n yield* Scope.close(working, Exit.void)\n yield* options.keepAlive(false)\n })\n )\n\n yield* Fiber.await(closing)\n })\n\n // A chat the app deleted while it was closed is deleted now, so its subscribers hear it.\n yield* checkAvailable\n\n const chatApi: Chat = {\n submit: Effect.fnUntraced(function* (input) {\n yield* store(input)\n yield* inRequest(drain)\n }),\n submitInBackground: Effect.fnUntraced(function* (input) {\n yield* store(input)\n yield* inBackground(drain)\n }),\n resume: Effect.gen(function* () {\n if (History.needsTurn(yield* chat.entries)) yield* inBackground(drain)\n }),\n stream: (after) =>\n Stream.unwrap(\n Effect.gen(function* () {\n // Subscribes before it reads, so nothing appended in between is missed.\n const subscription = yield* PubSub.subscribe(channel)\n const all = yield* chat.entries\n const stored = all.filter((entry) => entry.seq > after)\n const last = stored.at(-1)?.seq ?? after\n\n // Deleted: what the reader hasn't seen, then the end. Otherwise the stream ends with the `ChatDeleted`.\n if (all.some(isChatDeleted)) return Stream.fromIterable(stored)\n\n return Stream.concat(\n Stream.fromIterable(stored),\n Stream.fromSubscription(subscription).pipe(Stream.filter((entry) => entry.seq > last))\n ).pipe(Stream.takeUntil(isChatDeleted))\n })\n ),\n delete: remove\n }\n\n return chatApi\n})\n"],"mappings":"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"}
1
+ {"version":3,"file":"chat.js","names":["AssistantEvent.isLibraryEvent","AssistantEvent.isInput","Chats.Chats","AppEvents.isRegistered","AppEvents.encode","AppEvents.decodeEntries","ToolApproval.fromEntries","AppEvents.markInput","History.needsTurn","Turns.run","Specialists.makeRunner","SpecialistService.Runner"],"sources":["../../src/internal/chat.ts"],"sourcesContent":["import * as Cause from \"effect/Cause\"\nimport * as Context from \"effect/Context\"\nimport * as Deferred from \"effect/Deferred\"\nimport * as Effect from \"effect/Effect\"\nimport * as Exit from \"effect/Exit\"\nimport * as Fiber from \"effect/Fiber\"\nimport * as FiberSet from \"effect/FiberSet\"\nimport * as Predicate from \"effect/Predicate\"\nimport * as PubSub from \"effect/PubSub\"\nimport * as Ref from \"effect/Ref\"\nimport * as Scope from \"effect/Scope\"\nimport * as Semaphore from \"effect/Semaphore\"\nimport * as Stream from \"effect/Stream\"\nimport * as SynchronizedRef from \"effect/SynchronizedRef\"\nimport * as AssistantEvent from \"../AssistantEvent.ts\"\nimport type { ChatId } from \"../ChatId.ts\"\nimport { ChatLog } from \"../ChatLog.ts\"\nimport { CurrentChat } from \"../CurrentChat.ts\"\nimport * as Chats from \"../Chats.ts\"\nimport * as SpecialistService from \"../Specialist.ts\"\nimport * as ToolApproval from \"../ToolApproval.ts\"\nimport * as AppEvents from \"./events.ts\"\nimport * as History from \"./history.ts\"\nimport * as Specialists from \"./specialists.ts\"\nimport * as Turns from \"./turns.ts\"\nimport type { Writer } from \"./writer.ts\"\n\nconst idOf = (event: { readonly _tag: string }): string | undefined =>\n AssistantEvent.isLibraryEvent(event)\n ? AssistantEvent.isInput(event)\n ? event.id\n : undefined\n : Predicate.hasProperty(event, \"id\") && Predicate.isString(event.id)\n ? event.id\n : undefined\n\nconst makeChannelPubSub = Effect.acquireRelease(PubSub.unbounded<AssistantEvent.Entry>(), PubSub.shutdown)\n\nconst makeHolding = Effect.fnUntraced(function* (keepAlive: (enabled: boolean) => Effect.Effect<void>) {\n const holds = yield* SynchronizedRef.make(0)\n\n const acquire = SynchronizedRef.updateEffect(\n holds,\n Effect.fnUntraced(function* (n: number) {\n if (n !== 0) return n + 1\n\n yield* keepAlive(true)\n\n return 1\n })\n )\n\n const release = SynchronizedRef.updateEffect(\n holds,\n Effect.fnUntraced(function* (n: number) {\n if (n !== 1) return n - 1\n\n yield* keepAlive(false)\n\n return 0\n })\n )\n\n return (effect: Effect.Effect<void>) =>\n Effect.acquireUseRelease(\n acquire,\n () => effect,\n // An interrupted effect (a shutdown) keeps its hold, so the chat starts again and picks up where it was cut off.\n (_, exit) => (Exit.hasInterrupts(exit) ? Effect.void : release)\n )\n})\n\n/** @internal */\nexport interface Chat {\n readonly submit: (input: AssistantEvent.Input | AssistantEvent.StoredAppEvent) => Effect.Effect<void>\n readonly submitInBackground: (input: AssistantEvent.Input | AssistantEvent.StoredAppEvent) => Effect.Effect<void>\n // For inputs no turn has read, like a report stored during a shutdown.\n readonly resume: Effect.Effect<void>\n readonly stream: (after: number) => Stream.Stream<AssistantEvent.Entry>\n // Stops what works in the chat and ends its subscribers' streams; its log stays.\n readonly delete: Effect.Effect<void>\n}\n\nconst isChatDeleted = (entry: AssistantEvent.Entry) => Predicate.isTagged(entry.event, \"ChatDeleted\")\n\nconst deletedChat = (entries: ReadonlyArray<AssistantEvent.Entry>): Chat => ({\n submit: () => Effect.void,\n submitInBackground: () => Effect.void,\n resume: Effect.void,\n stream: (after) => Stream.fromIterable(entries.filter((entry) => entry.sequence > after)),\n delete: Effect.void\n})\n\nconst refused = (input: { readonly _tag: string }, why: string) =>\n Effect.logDebug(`assistant chat ${why}: ${input._tag} not stored`)\n\n/** @internal */\nexport const make = Effect.fnUntraced(function* (options: {\n readonly registration: Turns.Registration\n readonly chatId: ChatId\n readonly keepAlive: (enabled: boolean) => Effect.Effect<void>\n}) {\n const { registration, chatId } = options\n const { app } = registration.definition\n const storage = yield* ChatLog\n const chats = yield* Chats.Chats\n const initial = yield* storage.read(chatId)\n\n if (initial.some(isChatDeleted)) return deletedChat(initial)\n\n const channel = yield* makeChannelPubSub\n const writes = yield* Semaphore.make(1)\n const turnsLock = yield* Semaphore.make(1)\n const holding = yield* makeHolding(options.keepAlive)\n const deleted = yield* Deferred.make<undefined>()\n // So deleting the chat stops all its work: its turns, runs, and the builds' resources.\n const working = yield* Scope.fork(yield* Effect.scope)\n const inWorking = Effect.provideService(Scope.Scope, working)\n const background = yield* inWorking(FiberSet.make())\n const requests = yield* inWorking(FiberSet.make())\n\n const isDeleted = Deferred.isDone(deleted)\n\n // The chat is its log's only writer, so it reads the log once and turns read memory.\n const entries = yield* Ref.make(initial)\n\n // Every writer goes through here, one at a time, so each append is numbered after the last. After a delete it\n // stores nothing.\n const write = Effect.fnUntraced(function* (\n events: ReadonlyArray<AssistantEvent.AssistantEvent | AssistantEvent.StoredAppEvent>\n ) {\n const unknown = events.find((event) => !AssistantEvent.isLibraryEvent(event) && !AppEvents.isRegistered(app, event))\n\n if (unknown !== undefined) {\n return yield* Effect.die(`${unknown._tag} isn't one of the ${registration.definition.name} assistant's events.`)\n }\n\n if (yield* isDeleted) return []\n\n return yield* stored(events)\n })\n\n // Uninterruptible: a stored append must reach memory and the subscribers too, or a turn misses it and replies twice.\n const stored = Effect.fnUntraced(function* (\n events: ReadonlyArray<AssistantEvent.AssistantEvent | AssistantEvent.StoredAppEvent>\n ) {\n const stored = yield* storage.append(chatId, events)\n\n yield* Ref.update(entries, (all) => [...all, ...stored])\n yield* PubSub.publishAll(channel, stored)\n\n return stored\n }, Effect.uninterruptible)\n\n const chat: Writer = {\n chatId,\n entries: Ref.get(entries),\n append: (events) => Semaphore.withPermit(writes)(write(events))\n }\n\n const toStored = (event: {\n readonly _tag: string\n }): Effect.Effect<AssistantEvent.AssistantEvent | AssistantEvent.StoredAppEvent> =>\n AssistantEvent.isLibraryEvent(event)\n ? Effect.succeed(event)\n : AppEvents.isRegistered(app, event)\n ? AppEvents.encode(app, event)\n : Effect.die(`${event._tag} isn't one of the ${registration.definition.name} assistant's events.`)\n\n // What handlers, tools, and runs see: the assistant's own events decoded, and encoded when appended.\n const handle: Turns.TypedChat = {\n chatId,\n entries: Effect.map(chat.entries, (all) => AppEvents.decodeEntries(app, all)),\n append: (...events) => Effect.asVoid(Effect.flatMap(Effect.forEach(events, toStored), chat.append))\n }\n\n // A chat the app deleted is deleted here too, which stops what works in it: a turn that checks stops there.\n const checkAvailable: Effect.Effect<boolean> = chats.get(chatId).pipe(\n Effect.as(true),\n Effect.catchTag(\"GetChatError\", (unavailable) =>\n unavailable.reason === \"Deleted\"\n ? Effect.as(\n Effect.suspend(() => remove),\n false\n )\n : Effect.succeed(false)\n )\n )\n\n const store = Effect.fnUntraced(function* (input: AssistantEvent.Input | AssistantEvent.StoredAppEvent) {\n if (yield* isDeleted) return yield* refused(input, \"is deleted\")\n\n if (!(yield* checkAvailable)) return yield* refused(input, \"is unavailable\")\n\n // Checked under the write lock: a retry that arrives while the first submit is storing is stored once.\n yield* Semaphore.withPermit(writes)(\n Effect.gen(function* () {\n if (yield* isDeleted) return yield* refused(input, \"is deleted\")\n\n const id = idOf(input)\n const all = yield* Ref.get(entries)\n\n if (id !== undefined && all.some(({ event }) => idOf(event) === id)) return\n\n // A decision counts only for a call that still waits: not one decided, or expired, or unknown.\n if (\n Predicate.isTagged(input, \"ApprovalDecision\") &&\n ToolApproval.fromEntries(all).get(input.callId)?.status !== \"pending\"\n ) {\n return yield* refused(input, \"has no call waiting for that decision\")\n }\n\n // Stored as an input: it came in from outside a turn, so it wakes one, also after a restart.\n yield* write([AssistantEvent.isLibraryEvent(input) ? input : AppEvents.markInput(input)])\n })\n )\n })\n\n // The handlers' builds run in the chat's working scope: what they set up lives until the chat closes or is deleted.\n const handler = yield* inWorking(Effect.provideService(registration.open(handle), CurrentChat, handle))\n\n const specialists = yield* Effect.forEach(\n registration.specialists,\n Effect.fnUntraced(function* ({ specialist, built }) {\n const handler = yield* inWorking(Effect.provideService(built.open, CurrentChat, handle))\n\n return { specialist, handler }\n })\n )\n\n const inBackground = (effect: Effect.Effect<void>) => FiberSet.run(background, holding(effect)).pipe(Effect.asVoid)\n\n // A request's turns run in the working scope too, so a delete stops them; the request ends quietly then.\n const inRequest = Effect.fnUntraced(function* (effect: Effect.Effect<void>) {\n const fiber = yield* FiberSet.run(requests, effect)\n const exit = yield* Fiber.await(fiber).pipe(Effect.onInterrupt(() => Fiber.interrupt(fiber)))\n\n if (Exit.isFailure(exit) && !Cause.hasInterruptsOnly(exit.cause)) {\n return yield* Effect.failCause(exit.cause)\n }\n }, Effect.orDie)\n\n const drain: Effect.Effect<void> = Semaphore.withPermit(turnsLock)(\n Effect.gen(function* () {\n while (!(yield* isDeleted) && History.needsTurn(yield* chat.entries)) {\n yield* Turns.run({ registration, handler, chat, services, checkOpen: Effect.asVoid(checkAvailable) })\n }\n })\n )\n\n const runner = yield* inWorking(\n Specialists.makeRunner({\n chat,\n handle,\n specialists,\n model: registration.model,\n holding,\n report: Effect.fnUntraced(function* (finished) {\n yield* store(finished)\n yield* inBackground(drain)\n })\n })\n )\n\n const services = Context.make(CurrentChat, handle).pipe(\n Context.add(SpecialistService.Runner, runner),\n Context.merge(registration.model)\n )\n\n const remove = Effect.gen(function* () {\n // Under the write lock: an append in flight is stored before the `ChatDeleted`; none comes after.\n const first = yield* Semaphore.withPermit(writes)(\n Effect.gen(function* () {\n if (yield* isDeleted) return false\n\n yield* stored([AssistantEvent.ChatDeleted.make({})])\n\n return yield* Deferred.succeed(deleted, undefined)\n })\n ).pipe(Effect.uninterruptible)\n\n if (!first) return\n\n // Its own fiber: a tool that deletes its own chat is in the working scope, and closing it interrupts that tool.\n // Interrupted work keeps its hold (see `makeHolding`), so the chat lets go of it here.\n const closing = yield* Effect.forkDetach(\n Effect.gen(function* () {\n yield* Scope.close(working, Exit.void)\n yield* options.keepAlive(false)\n })\n )\n\n yield* Fiber.await(closing)\n })\n\n // A chat the app deleted while it was closed is deleted now, so its subscribers hear it.\n yield* checkAvailable\n\n const chatApi: Chat = {\n submit: Effect.fnUntraced(function* (input) {\n yield* store(input)\n yield* inRequest(drain)\n }),\n submitInBackground: Effect.fnUntraced(function* (input) {\n yield* store(input)\n yield* inBackground(drain)\n }),\n resume: Effect.gen(function* () {\n if (History.needsTurn(yield* chat.entries)) yield* inBackground(drain)\n }),\n stream: (after) =>\n Stream.unwrap(\n Effect.gen(function* () {\n // Subscribes before it reads, so nothing appended in between is missed.\n const subscription = yield* PubSub.subscribe(channel)\n const all = yield* chat.entries\n const stored = all.filter((entry) => entry.sequence > after)\n const last = stored.at(-1)?.sequence ?? after\n\n // Deleted: what the reader hasn't seen, then the end. Otherwise the stream ends with the `ChatDeleted`.\n if (all.some(isChatDeleted)) return Stream.fromIterable(stored)\n\n return Stream.concat(\n Stream.fromIterable(stored),\n Stream.fromSubscription(subscription).pipe(Stream.filter((entry) => entry.sequence > last))\n ).pipe(Stream.takeUntil(isChatDeleted))\n })\n ),\n delete: remove\n }\n\n return chatApi\n})\n"],"mappings":"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"}
@@ -57,7 +57,7 @@ const decodeEntry = (app, entry) => {
57
57
  const decodeEntries = (app, entries) => entries.flatMap((entry) => Option.toArray(decodeEntry(app, entry)));
58
58
  const toEntry = (entry) => ({
59
59
  chatId: entry.chatId,
60
- seq: entry.seq,
60
+ sequence: entry.sequence,
61
61
  at: entry.at
62
62
  });
63
63
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"events.js","names":["AssistantEvent.isLibraryEvent","AssistantEvent.StoredAppEvent"],"sources":["../../src/internal/events.ts"],"sourcesContent":["import * as Effect from \"effect/Effect\"\nimport * as Option from \"effect/Option\"\nimport * as Predicate from \"effect/Predicate\"\nimport * as Record from \"effect/Record\"\nimport * as Schema from \"effect/Schema\"\nimport * as SchemaAST from \"effect/SchemaAST\"\nimport type * as Assistant from \"../Assistant.ts\"\nimport * as AssistantEvent from \"../AssistantEvent.ts\"\nimport { inputKey } from \"./input.ts\"\n\n/** @internal */\nexport interface AppEvents {\n readonly byTag: ReadonlyMap<string, AssistantEvent.AppEventSchema>\n}\n\n/** @internal */\nexport const tagOf = (schema: AssistantEvent.AppEventSchema): string => {\n const ast = schema.fields._tag.ast\n\n if (SchemaAST.isLiteral(ast) && Predicate.isString(ast.literal)) return ast.literal\n\n throw new Error(\"An app event is a tagged struct or class.\")\n}\n\n/** @internal */\nexport const make = (events: ReadonlyArray<AssistantEvent.AppEventSchema>): AppEvents => {\n const byTag = new Map(events.map((schema): [string, AssistantEvent.AppEventSchema] => [tagOf(schema), schema]))\n\n for (const tag of byTag.keys()) {\n if (AssistantEvent.isLibraryEvent({ _tag: tag })) throw new Error(`${tag} is one of the library's events.`)\n }\n\n if (byTag.size !== events.length) throw new Error(\"An assistant's events have different tags.\")\n\n return { byTag }\n}\n\n/** @internal */\nexport const encode = Effect.fnUntraced(function* (app: AppEvents, event: { readonly _tag: string }) {\n const schema = app.byTag.get(event._tag)\n\n // The types only let registered events through, so one with no schema here is a defect.\n if (schema === undefined) return yield* Effect.die(`${event._tag} isn't one of this assistant's events.`)\n\n const json = yield* Schema.encodeUnknownEffect(Schema.toCodecJson(schema))(event)\n\n return yield* Schema.decodeUnknownEffect(AssistantEvent.StoredAppEvent)(json)\n}, Effect.orDie)\n\n/** @internal */\nexport const isRegistered = (app: AppEvents, event: { readonly _tag: string }): boolean => app.byTag.has(event._tag)\n\n// The app's schema decodes the event as the app wrote it.\nconst unmarked = (event: { readonly _tag: string }) =>\n Predicate.hasProperty(event, inputKey) ? Record.remove(event, inputKey) : event\n\n// An event as the log stores it, decoded with its schema.\n/** @internal */\nexport const decodeWith = (\n schema: AssistantEvent.AppEventSchema,\n event: { readonly _tag: string }\n): Option.Option<{ readonly _tag: string }> => Schema.decodeUnknownOption(Schema.toCodecJson(schema))(unmarked(event))\n\nconst decodeEvent = (app: AppEvents, event: { readonly _tag: string }): Option.Option<{ readonly _tag: string }> => {\n const schema = app.byTag.get(event._tag)\n\n return schema === undefined ? Option.none() : decodeWith(schema, event)\n}\n\n// What came in through `submit`, from outside a turn, is an input: it wakes a turn.\n/** @internal */\nexport const markInput = (event: AssistantEvent.StoredAppEvent): AssistantEvent.StoredAppEvent => ({\n ...event,\n [inputKey]: true\n})\n\n// An app event that doesn't decode (say, one from an older version of its schema) is left out.\n/** @internal */\nexport const decodeEntry = (app: AppEvents, entry: AssistantEvent.AnyEntry): Option.Option<Assistant.Entry<any>> => {\n const { event } = entry\n\n if (AssistantEvent.isLibraryEvent(event)) return Option.some({ ...toEntry(entry), event })\n\n return Option.map(decodeEvent(app, event), (decoded) => ({ ...toEntry(entry), event: decoded }))\n}\n\n/** @internal */\nexport const decodeEntries = (\n app: AppEvents,\n entries: ReadonlyArray<AssistantEvent.AnyEntry>\n): ReadonlyArray<Assistant.Entry<any>> => entries.flatMap((entry) => Option.toArray(decodeEntry(app, entry)))\n\n/** @internal */\nexport type DecodedEntries = ReturnType<typeof decodeEntries>\n\nconst toEntry = (entry: AssistantEvent.AnyEntry) => ({\n chatId: entry.chatId,\n seq: entry.seq,\n at: entry.at\n})\n"],"mappings":";;;;;;;;;;AAgBA,MAAa,SAAS,WAAkD;CACtE,MAAM,MAAM,OAAO,OAAO,KAAK;CAE/B,IAAI,UAAU,UAAU,GAAG,KAAK,UAAU,SAAS,IAAI,OAAO,GAAG,OAAO,IAAI;CAE5E,MAAM,IAAI,MAAM,2CAA2C;AAC7D;;AAGA,MAAa,QAAQ,WAAoE;CACvF,MAAM,QAAQ,IAAI,IAAI,OAAO,KAAK,WAAoD,CAAC,MAAM,MAAM,GAAG,MAAM,CAAC,CAAC;CAE9G,KAAK,MAAM,OAAO,MAAM,KAAK,GAC3B,IAAIA,eAA8B,EAAE,MAAM,IAAI,CAAC,GAAG,MAAM,IAAI,MAAM,GAAG,IAAI,iCAAiC;CAG5G,IAAI,MAAM,SAAS,OAAO,QAAQ,MAAM,IAAI,MAAM,4CAA4C;CAE9F,OAAO,EAAE,MAAM;AACjB;;AAGA,MAAa,SAAS,OAAO,WAAW,WAAW,KAAgB,OAAkC;CACnG,MAAM,SAAS,IAAI,MAAM,IAAI,MAAM,IAAI;CAGvC,IAAI,WAAW,KAAA,GAAW,OAAO,OAAO,OAAO,IAAI,GAAG,MAAM,KAAK,uCAAuC;CAExG,MAAM,OAAO,OAAO,OAAO,oBAAoB,OAAO,YAAY,MAAM,CAAC,CAAC,CAAC,KAAK;CAEhF,OAAO,OAAO,OAAO,oBAAoBC,cAA6B,CAAC,CAAC,IAAI;AAC9E,GAAG,OAAO,KAAK;;AAGf,MAAa,gBAAgB,KAAgB,UAA8C,IAAI,MAAM,IAAI,MAAM,IAAI;AAGnH,MAAM,YAAY,UAChB,UAAU,YAAY,OAAA,yBAAe,IAAI,OAAO,OAAO,OAAO,QAAQ,IAAI;;AAI5E,MAAa,cACX,QACA,UAC6C,OAAO,oBAAoB,OAAO,YAAY,MAAM,CAAC,CAAC,CAAC,SAAS,KAAK,CAAC;AAErH,MAAM,eAAe,KAAgB,UAA+E;CAClH,MAAM,SAAS,IAAI,MAAM,IAAI,MAAM,IAAI;CAEvC,OAAO,WAAW,KAAA,IAAY,OAAO,KAAK,IAAI,WAAW,QAAQ,KAAK;AACxE;;AAIA,MAAa,aAAa,WAAyE;CACjG,GAAG;EACF,WAAW;AACd;;AAIA,MAAa,eAAe,KAAgB,UAAwE;CAClH,MAAM,EAAE,UAAU;CAElB,IAAID,eAA8B,KAAK,GAAG,OAAO,OAAO,KAAK;EAAE,GAAG,QAAQ,KAAK;EAAG;CAAM,CAAC;CAEzF,OAAO,OAAO,IAAI,YAAY,KAAK,KAAK,IAAI,aAAa;EAAE,GAAG,QAAQ,KAAK;EAAG,OAAO;CAAQ,EAAE;AACjG;;AAGA,MAAa,iBACX,KACA,YACwC,QAAQ,SAAS,UAAU,OAAO,QAAQ,YAAY,KAAK,KAAK,CAAC,CAAC;AAK5G,MAAM,WAAW,WAAoC;CACnD,QAAQ,MAAM;CACd,KAAK,MAAM;CACX,IAAI,MAAM;AACZ"}
1
+ {"version":3,"file":"events.js","names":["AssistantEvent.isLibraryEvent","AssistantEvent.StoredAppEvent"],"sources":["../../src/internal/events.ts"],"sourcesContent":["import * as Effect from \"effect/Effect\"\nimport * as Option from \"effect/Option\"\nimport * as Predicate from \"effect/Predicate\"\nimport * as Record from \"effect/Record\"\nimport * as Schema from \"effect/Schema\"\nimport * as SchemaAST from \"effect/SchemaAST\"\nimport type * as Assistant from \"../Assistant.ts\"\nimport * as AssistantEvent from \"../AssistantEvent.ts\"\nimport { inputKey } from \"./input.ts\"\n\n/** @internal */\nexport interface AppEvents {\n readonly byTag: ReadonlyMap<string, AssistantEvent.AppEventSchema>\n}\n\n/** @internal */\nexport const tagOf = (schema: AssistantEvent.AppEventSchema): string => {\n const ast = schema.fields._tag.ast\n\n if (SchemaAST.isLiteral(ast) && Predicate.isString(ast.literal)) return ast.literal\n\n throw new Error(\"An app event is a tagged struct or class.\")\n}\n\n/** @internal */\nexport const make = (events: ReadonlyArray<AssistantEvent.AppEventSchema>): AppEvents => {\n const byTag = new Map(events.map((schema): [string, AssistantEvent.AppEventSchema] => [tagOf(schema), schema]))\n\n for (const tag of byTag.keys()) {\n if (AssistantEvent.isLibraryEvent({ _tag: tag })) throw new Error(`${tag} is one of the library's events.`)\n }\n\n if (byTag.size !== events.length) throw new Error(\"An assistant's events have different tags.\")\n\n return { byTag }\n}\n\n/** @internal */\nexport const encode = Effect.fnUntraced(function* (app: AppEvents, event: { readonly _tag: string }) {\n const schema = app.byTag.get(event._tag)\n\n // The types only let registered events through, so one with no schema here is a defect.\n if (schema === undefined) return yield* Effect.die(`${event._tag} isn't one of this assistant's events.`)\n\n const json = yield* Schema.encodeUnknownEffect(Schema.toCodecJson(schema))(event)\n\n return yield* Schema.decodeUnknownEffect(AssistantEvent.StoredAppEvent)(json)\n}, Effect.orDie)\n\n/** @internal */\nexport const isRegistered = (app: AppEvents, event: { readonly _tag: string }): boolean => app.byTag.has(event._tag)\n\n// The app's schema decodes the event as the app wrote it.\nconst unmarked = (event: { readonly _tag: string }) =>\n Predicate.hasProperty(event, inputKey) ? Record.remove(event, inputKey) : event\n\n// An event as the log stores it, decoded with its schema.\n/** @internal */\nexport const decodeWith = (\n schema: AssistantEvent.AppEventSchema,\n event: { readonly _tag: string }\n): Option.Option<{ readonly _tag: string }> => Schema.decodeUnknownOption(Schema.toCodecJson(schema))(unmarked(event))\n\nconst decodeEvent = (app: AppEvents, event: { readonly _tag: string }): Option.Option<{ readonly _tag: string }> => {\n const schema = app.byTag.get(event._tag)\n\n return schema === undefined ? Option.none() : decodeWith(schema, event)\n}\n\n// What came in through `submit`, from outside a turn, is an input: it wakes a turn.\n/** @internal */\nexport const markInput = (event: AssistantEvent.StoredAppEvent): AssistantEvent.StoredAppEvent => ({\n ...event,\n [inputKey]: true\n})\n\n// An app event that doesn't decode (say, one from an older version of its schema) is left out.\n/** @internal */\nexport const decodeEntry = (app: AppEvents, entry: AssistantEvent.AnyEntry): Option.Option<Assistant.Entry<any>> => {\n const { event } = entry\n\n if (AssistantEvent.isLibraryEvent(event)) return Option.some({ ...toEntry(entry), event })\n\n return Option.map(decodeEvent(app, event), (decoded) => ({ ...toEntry(entry), event: decoded }))\n}\n\n/** @internal */\nexport const decodeEntries = (\n app: AppEvents,\n entries: ReadonlyArray<AssistantEvent.AnyEntry>\n): ReadonlyArray<Assistant.Entry<any>> => entries.flatMap((entry) => Option.toArray(decodeEntry(app, entry)))\n\n/** @internal */\nexport type DecodedEntries = ReturnType<typeof decodeEntries>\n\nconst toEntry = (entry: AssistantEvent.AnyEntry) => ({\n chatId: entry.chatId,\n sequence: entry.sequence,\n at: entry.at\n})\n"],"mappings":";;;;;;;;;;AAgBA,MAAa,SAAS,WAAkD;CACtE,MAAM,MAAM,OAAO,OAAO,KAAK;CAE/B,IAAI,UAAU,UAAU,GAAG,KAAK,UAAU,SAAS,IAAI,OAAO,GAAG,OAAO,IAAI;CAE5E,MAAM,IAAI,MAAM,2CAA2C;AAC7D;;AAGA,MAAa,QAAQ,WAAoE;CACvF,MAAM,QAAQ,IAAI,IAAI,OAAO,KAAK,WAAoD,CAAC,MAAM,MAAM,GAAG,MAAM,CAAC,CAAC;CAE9G,KAAK,MAAM,OAAO,MAAM,KAAK,GAC3B,IAAIA,eAA8B,EAAE,MAAM,IAAI,CAAC,GAAG,MAAM,IAAI,MAAM,GAAG,IAAI,iCAAiC;CAG5G,IAAI,MAAM,SAAS,OAAO,QAAQ,MAAM,IAAI,MAAM,4CAA4C;CAE9F,OAAO,EAAE,MAAM;AACjB;;AAGA,MAAa,SAAS,OAAO,WAAW,WAAW,KAAgB,OAAkC;CACnG,MAAM,SAAS,IAAI,MAAM,IAAI,MAAM,IAAI;CAGvC,IAAI,WAAW,KAAA,GAAW,OAAO,OAAO,OAAO,IAAI,GAAG,MAAM,KAAK,uCAAuC;CAExG,MAAM,OAAO,OAAO,OAAO,oBAAoB,OAAO,YAAY,MAAM,CAAC,CAAC,CAAC,KAAK;CAEhF,OAAO,OAAO,OAAO,oBAAoBC,cAA6B,CAAC,CAAC,IAAI;AAC9E,GAAG,OAAO,KAAK;;AAGf,MAAa,gBAAgB,KAAgB,UAA8C,IAAI,MAAM,IAAI,MAAM,IAAI;AAGnH,MAAM,YAAY,UAChB,UAAU,YAAY,OAAA,yBAAe,IAAI,OAAO,OAAO,OAAO,QAAQ,IAAI;;AAI5E,MAAa,cACX,QACA,UAC6C,OAAO,oBAAoB,OAAO,YAAY,MAAM,CAAC,CAAC,CAAC,SAAS,KAAK,CAAC;AAErH,MAAM,eAAe,KAAgB,UAA+E;CAClH,MAAM,SAAS,IAAI,MAAM,IAAI,MAAM,IAAI;CAEvC,OAAO,WAAW,KAAA,IAAY,OAAO,KAAK,IAAI,WAAW,QAAQ,KAAK;AACxE;;AAIA,MAAa,aAAa,WAAyE;CACjG,GAAG;EACF,WAAW;AACd;;AAIA,MAAa,eAAe,KAAgB,UAAwE;CAClH,MAAM,EAAE,UAAU;CAElB,IAAID,eAA8B,KAAK,GAAG,OAAO,OAAO,KAAK;EAAE,GAAG,QAAQ,KAAK;EAAG;CAAM,CAAC;CAEzF,OAAO,OAAO,IAAI,YAAY,KAAK,KAAK,IAAI,aAAa;EAAE,GAAG,QAAQ,KAAK;EAAG,OAAO;CAAQ,EAAE;AACjG;;AAGA,MAAa,iBACX,KACA,YACwC,QAAQ,SAAS,UAAU,OAAO,QAAQ,YAAY,KAAK,KAAK,CAAC,CAAC;AAK5G,MAAM,WAAW,WAAoC;CACnD,QAAQ,MAAM;CACd,UAAU,MAAM;CAChB,IAAI,MAAM;AACZ"}