@copilotkit/channels 0.1.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 (156) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +204 -0
  3. package/dist/action-registry.d.ts +59 -0
  4. package/dist/action-registry.d.ts.map +1 -0
  5. package/dist/action-registry.js +217 -0
  6. package/dist/action-registry.test.d.ts +2 -0
  7. package/dist/action-registry.test.d.ts.map +1 -0
  8. package/dist/action-registry.test.js +115 -0
  9. package/dist/action-store.d.ts +21 -0
  10. package/dist/action-store.d.ts.map +1 -0
  11. package/dist/action-store.js +23 -0
  12. package/dist/action-store.test.d.ts +2 -0
  13. package/dist/action-store.test.d.ts.map +1 -0
  14. package/dist/action-store.test.js +27 -0
  15. package/dist/codec.d.ts +25 -0
  16. package/dist/codec.d.ts.map +1 -0
  17. package/dist/codec.js +1 -0
  18. package/dist/commands.d.ts +73 -0
  19. package/dist/commands.d.ts.map +1 -0
  20. package/dist/commands.js +38 -0
  21. package/dist/commands.test.d.ts +2 -0
  22. package/dist/commands.test.d.ts.map +1 -0
  23. package/dist/commands.test.js +23 -0
  24. package/dist/create-bot.d.ts +171 -0
  25. package/dist/create-bot.d.ts.map +1 -0
  26. package/dist/create-bot.foundations.test.d.ts +2 -0
  27. package/dist/create-bot.foundations.test.d.ts.map +1 -0
  28. package/dist/create-bot.foundations.test.js +147 -0
  29. package/dist/create-bot.js +576 -0
  30. package/dist/create-bot.test.d.ts +2 -0
  31. package/dist/create-bot.test.d.ts.map +1 -0
  32. package/dist/create-bot.test.js +936 -0
  33. package/dist/index.d.ts +26 -0
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +32 -0
  36. package/dist/mint-id.d.ts +3 -0
  37. package/dist/mint-id.d.ts.map +1 -0
  38. package/dist/mint-id.js +20 -0
  39. package/dist/mint-id.test.d.ts +2 -0
  40. package/dist/mint-id.test.d.ts.map +1 -0
  41. package/dist/mint-id.test.js +16 -0
  42. package/dist/modal-submit.test.d.ts +2 -0
  43. package/dist/modal-submit.test.d.ts.map +1 -0
  44. package/dist/modal-submit.test.js +68 -0
  45. package/dist/platform-adapter.d.ts +312 -0
  46. package/dist/platform-adapter.d.ts.map +1 -0
  47. package/dist/platform-adapter.js +1 -0
  48. package/dist/platform-adapter.test.d.ts +2 -0
  49. package/dist/platform-adapter.test.d.ts.map +1 -0
  50. package/dist/platform-adapter.test.js +40 -0
  51. package/dist/reactions.test.d.ts +2 -0
  52. package/dist/reactions.test.d.ts.map +1 -0
  53. package/dist/reactions.test.js +197 -0
  54. package/dist/run-loop.d.ts +41 -0
  55. package/dist/run-loop.d.ts.map +1 -0
  56. package/dist/run-loop.js +102 -0
  57. package/dist/run-loop.test.d.ts +2 -0
  58. package/dist/run-loop.test.d.ts.map +1 -0
  59. package/dist/run-loop.test.js +125 -0
  60. package/dist/standard-schema.d.ts +52 -0
  61. package/dist/standard-schema.d.ts.map +1 -0
  62. package/dist/standard-schema.js +66 -0
  63. package/dist/state/duration.d.ts +3 -0
  64. package/dist/state/duration.d.ts.map +1 -0
  65. package/dist/state/duration.js +16 -0
  66. package/dist/state/duration.test.d.ts +2 -0
  67. package/dist/state/duration.test.d.ts.map +1 -0
  68. package/dist/state/duration.test.js +13 -0
  69. package/dist/state/kv-action-store.d.ts +7 -0
  70. package/dist/state/kv-action-store.d.ts.map +1 -0
  71. package/dist/state/kv-action-store.js +9 -0
  72. package/dist/state/kv-action-store.test.d.ts +2 -0
  73. package/dist/state/kv-action-store.test.d.ts.map +1 -0
  74. package/dist/state/kv-action-store.test.js +21 -0
  75. package/dist/state/memory-store.d.ts +41 -0
  76. package/dist/state/memory-store.d.ts.map +1 -0
  77. package/dist/state/memory-store.js +110 -0
  78. package/dist/state/memory-store.test.d.ts +2 -0
  79. package/dist/state/memory-store.test.d.ts.map +1 -0
  80. package/dist/state/memory-store.test.js +22 -0
  81. package/dist/state/state-conversation-store.d.ts +14 -0
  82. package/dist/state/state-conversation-store.d.ts.map +1 -0
  83. package/dist/state/state-conversation-store.js +22 -0
  84. package/dist/state/state-conversation-store.test.d.ts +2 -0
  85. package/dist/state/state-conversation-store.test.d.ts.map +1 -0
  86. package/dist/state/state-conversation-store.test.js +58 -0
  87. package/dist/state/state-store.d.ts +63 -0
  88. package/dist/state/state-store.d.ts.map +1 -0
  89. package/dist/state/state-store.js +1 -0
  90. package/dist/telemetry/bot-telemetry.d.ts +31 -0
  91. package/dist/telemetry/bot-telemetry.d.ts.map +1 -0
  92. package/dist/telemetry/bot-telemetry.js +70 -0
  93. package/dist/telemetry/bot-telemetry.test.d.ts +2 -0
  94. package/dist/telemetry/bot-telemetry.test.d.ts.map +1 -0
  95. package/dist/telemetry/bot-telemetry.test.js +56 -0
  96. package/dist/telemetry/create-bot-telemetry.test.d.ts +2 -0
  97. package/dist/telemetry/create-bot-telemetry.test.d.ts.map +1 -0
  98. package/dist/telemetry/create-bot-telemetry.test.js +71 -0
  99. package/dist/telemetry/e2e-telemetry.test.d.ts +2 -0
  100. package/dist/telemetry/e2e-telemetry.test.d.ts.map +1 -0
  101. package/dist/telemetry/e2e-telemetry.test.js +56 -0
  102. package/dist/telemetry/events-catalog.test.d.ts +2 -0
  103. package/dist/telemetry/events-catalog.test.d.ts.map +1 -0
  104. package/dist/telemetry/events-catalog.test.js +8 -0
  105. package/dist/telemetry/install-id.d.ts +8 -0
  106. package/dist/telemetry/install-id.d.ts.map +1 -0
  107. package/dist/telemetry/install-id.js +43 -0
  108. package/dist/telemetry/install-id.test.d.ts +2 -0
  109. package/dist/telemetry/install-id.test.d.ts.map +1 -0
  110. package/dist/telemetry/install-id.test.js +43 -0
  111. package/dist/telemetry/platform-allowlist-coverage.test.d.ts +2 -0
  112. package/dist/telemetry/platform-allowlist-coverage.test.d.ts.map +1 -0
  113. package/dist/telemetry/platform-allowlist-coverage.test.js +45 -0
  114. package/dist/telemetry/sanitize-error.d.ts +3 -0
  115. package/dist/telemetry/sanitize-error.d.ts.map +1 -0
  116. package/dist/telemetry/sanitize-error.js +31 -0
  117. package/dist/telemetry/sanitize-error.test.d.ts +2 -0
  118. package/dist/telemetry/sanitize-error.test.d.ts.map +1 -0
  119. package/dist/telemetry/sanitize-error.test.js +33 -0
  120. package/dist/testing/fake-adapter.d.ts +137 -0
  121. package/dist/testing/fake-adapter.d.ts.map +1 -0
  122. package/dist/testing/fake-adapter.js +273 -0
  123. package/dist/testing/fake-agent.d.ts +28 -0
  124. package/dist/testing/fake-agent.d.ts.map +1 -0
  125. package/dist/testing/fake-agent.js +36 -0
  126. package/dist/testing/state-store-conformance.d.ts +4 -0
  127. package/dist/testing/state-store-conformance.d.ts.map +1 -0
  128. package/dist/testing/state-store-conformance.js +117 -0
  129. package/dist/thread-capabilities.test.d.ts +2 -0
  130. package/dist/thread-capabilities.test.d.ts.map +1 -0
  131. package/dist/thread-capabilities.test.js +77 -0
  132. package/dist/thread-ephemeral.test.d.ts +2 -0
  133. package/dist/thread-ephemeral.test.d.ts.map +1 -0
  134. package/dist/thread-ephemeral.test.js +45 -0
  135. package/dist/thread-reactions.test.d.ts +2 -0
  136. package/dist/thread-reactions.test.d.ts.map +1 -0
  137. package/dist/thread-reactions.test.js +38 -0
  138. package/dist/thread.d.ts +152 -0
  139. package/dist/thread.d.ts.map +1 -0
  140. package/dist/thread.js +329 -0
  141. package/dist/thread.test.d.ts +2 -0
  142. package/dist/thread.test.d.ts.map +1 -0
  143. package/dist/thread.test.js +83 -0
  144. package/dist/tools.d.ts +67 -0
  145. package/dist/tools.d.ts.map +1 -0
  146. package/dist/tools.js +38 -0
  147. package/dist/tools.test.d.ts +2 -0
  148. package/dist/tools.test.d.ts.map +1 -0
  149. package/dist/tools.test.js +31 -0
  150. package/dist/transcripts.d.ts +52 -0
  151. package/dist/transcripts.d.ts.map +1 -0
  152. package/dist/transcripts.js +67 -0
  153. package/dist/transcripts.test.d.ts +2 -0
  154. package/dist/transcripts.test.d.ts.map +1 -0
  155. package/dist/transcripts.test.js +108 -0
  156. package/package.json +72 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License
2
+
3
+ Copyright (c) Atai Barkai
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,204 @@
1
+ # @copilotkit/channels
2
+
3
+ The **platform-agnostic channel engine**. It owns everything between an incoming
4
+ message and a rendered reply — handler registration, the agent
5
+ run/tool/interrupt loop, JSX action binding, and the `PlatformAdapter`
6
+ contract — without knowing anything about Slack (or any other surface). A
7
+ platform adapter plugs in at the boundary; `@copilotkit/channels-slack` is the
8
+ concrete Slack one.
9
+
10
+ It builds on `@copilotkit/channels-ui` (the JSX runtime + component vocabulary,
11
+ re-exported from here for convenience) and AG-UI (`@ag-ui/client`,
12
+ `@ag-ui/core`).
13
+
14
+ ## Install
15
+
16
+ ```sh
17
+ pnpm add @copilotkit/channels @copilotkit/channels-ui
18
+ # plus a platform adapter, e.g.
19
+ pnpm add @copilotkit/channels-slack
20
+ ```
21
+
22
+ ## Quickstart
23
+
24
+ ```ts
25
+ import { createBot } from "@copilotkit/channels";
26
+ import { slack } from "@copilotkit/channels-slack"; // a concrete PlatformAdapter
27
+
28
+ const bot = createBot({
29
+ adapters: [slack({ botToken, appToken })],
30
+ agent: (threadId) => makeAgent(threadId), // AbstractAgent or (threadId) => AbstractAgent
31
+ tools: [...myTools], // BotTool[] forwarded on every runAgent
32
+ context: [...myContext], // ContextEntry[] forwarded on every runAgent
33
+ });
34
+
35
+ bot.onMention(({ thread }) => thread.runAgent());
36
+ bot.onMessage(({ thread }) => thread.runAgent());
37
+
38
+ await bot.start();
39
+ ```
40
+
41
+ `createBot(opts)` returns a `Bot`:
42
+
43
+ - `onMention(handler)` / `onMessage(handler)` — turn handlers receiving
44
+ `{ thread, message }`. (Routing is mention-preferred: if any mention
45
+ handler is registered, all turns route to it; otherwise message handlers
46
+ fire.)
47
+ - `onThreadStarted(handler)` — a conversation surface opened (e.g. the Slack
48
+ assistant pane); receives `{ thread, user? }`. Greet, set suggested prompts
49
+ or a title, or run the agent. Adapters without the concept never fire it.
50
+ - `onInteraction<TValue>(id, handler)` — explicit escape-hatch handler for a
51
+ known action id, bypassing the registry; `ctx.action.value` is typed `TValue`.
52
+ - `onInterrupt<TPayload>(eventName, handler)` — handle a captured agent
53
+ interrupt (LangGraph-style `on_interrupt`); receives `{ payload, thread }`
54
+ with `payload` typed `TPayload`.
55
+ - `onCommand(command)` / `onCommand(name, handler)` — register a slash command.
56
+ The handler gets `{ thread, command, text, options, user }`. `text` is the
57
+ raw args (Slack); `options` is the typed, parsed form (`defineBotCommand`
58
+ with an `options` Standard Schema) for surfaces with native structured args
59
+ (e.g. Discord). Forwarded to adapters that support commands and ignored
60
+ elsewhere — also pass them up front via `commands` in `CreateBotOptions`.
61
+ - `tool(t)` — register a `BotTool` (alternative to `opts.tools`); must be
62
+ added before `start()`.
63
+ - `start()` / `stop()` — bring adapters up / down.
64
+
65
+ `agent` is optional. If omitted, calling `thread.runAgent()` throws; supply
66
+ an `AbstractAgent` or a `(threadId) => AbstractAgent` factory.
67
+
68
+ ## `Thread`
69
+
70
+ A `Thread` is the per-conversation handle handed to your handlers and tool
71
+ contexts. It accepts any `Renderable` (JSX or a string) for posting.
72
+
73
+ ```ts
74
+ interface Thread {
75
+ readonly platform: string;
76
+ post(ui: Renderable): Promise<MessageRef>;
77
+ update(ref: MessageRef, ui: Renderable): Promise<MessageRef>;
78
+ delete(ref: MessageRef): Promise<void>;
79
+ stream(src: string | AsyncIterable<string>): Promise<MessageRef>;
80
+ runAgent(input?: {
81
+ context?: ContextEntry[];
82
+ tools?: BotTool[];
83
+ }): Promise<MessageRef | undefined>;
84
+ resume(value: unknown): Promise<MessageRef | undefined>;
85
+ awaitChoice<T = unknown>(ui: Renderable): Promise<T>;
86
+ // Capability-gated (return { ok: false } on surfaces without support):
87
+ setSuggestedPrompts(
88
+ prompts: ReadonlyArray<{ title: string; message: string }>,
89
+ opts?: { title?: string },
90
+ ): Promise<{ ok: boolean; error?: string }>;
91
+ setTitle(title: string): Promise<{ ok: boolean; error?: string }>;
92
+ }
93
+ ```
94
+
95
+ - `post` / `update` render the JSX to IR, **bind** every event-prop handler
96
+ in the tree (mint a content-stable id, snapshot it, rewrite the prop to
97
+ `{ id }`), then hand the IR to the adapter.
98
+ - `runAgent` resolves the conversation's agent session, creates the adapter's
99
+ `RunRenderer`, and drives the run/tool/interrupt loop. Per-run `tools` /
100
+ `context` are merged on top of the bot-level defaults for that run only.
101
+ - `resume(value)` re-enters a paused interrupt run with
102
+ `forwardedProps.command`.
103
+ - `awaitChoice<T>(ui)` posts a picker and blocks until an interaction in this
104
+ conversation resolves it to the clicked control's value (HITL); pass `T` to
105
+ type the returned value.
106
+
107
+ ## Tools & context
108
+
109
+ A `BotTool` is forwarded to the agent as a frontend tool; its handler runs in
110
+ the bot when the agent calls it. The handler `ctx` carries the `thread`, so a
111
+ tool can render JSX (`ctx.thread.post(<Card .../>)`) or run the agent further.
112
+
113
+ ```ts
114
+ interface BotTool<Schema extends ObjectSchema = ObjectSchema> {
115
+ name: string;
116
+ description: string;
117
+ parameters: Schema; // any Standard Schema (Zod/Valibot/ArkType/…)
118
+ handler(args, ctx: BotToolContext): Promise<unknown> | unknown;
119
+ }
120
+ ```
121
+
122
+ Define one with the non-curried `defineBotTool`, which infers the arg types
123
+ from `parameters`:
124
+
125
+ ```ts
126
+ defineBotTool({
127
+ name: "read_thread",
128
+ description: "Read the messages in the current conversation.",
129
+ parameters: z.object({}),
130
+ async handler(_args, { thread }) {
131
+ return await thread.getMessages();
132
+ },
133
+ });
134
+ ```
135
+
136
+ `parameters` (a Standard Schema) is converted to JSON Schema for the LLM and
137
+ validated on the way back. `BotToolContext` is `{ thread, message?, user?,
138
+ signal?, platform }` — a single shared type with no per-adapter generic.
139
+ Platform-specific power is reached only through capability-gated `thread`
140
+ methods (e.g. `thread.getMessages()`, `thread.lookupUser(query)`,
141
+ `thread.postFile(...)`), so a tool stays portable across surfaces.
142
+
143
+ A `ContextEntry` is `{ description: string; value: string }` — knowledge
144
+ folded into the agent's system context on each `runAgent`.
145
+
146
+ ## ActionStore
147
+
148
+ Inline JSX handlers are bound by content. Each interactive node gets a
149
+ **content-stable, opaque** minted id — `mintId(componentName, path, props)`
150
+ = `"ck:" + sha1(name | path | stableStringify(props)).slice(0,16)`. Only the
151
+ opaque id (plus any small `bind()` args) is stamped on the native token; no
152
+ props, PII, or secrets go over the wire.
153
+
154
+ On a click, the `ActionRegistry` resolves the handler from a hot in-memory
155
+ cache; on a miss it **rehydrates** by loading the snapshot from the
156
+ `ActionStore`, re-rendering the named component with the frozen props, and
157
+ re-walking to the handler's path.
158
+
159
+ The default `ActionStore` is `InMemoryActionStore` (a `Map` with optional
160
+ TTL). It is lost on restart: after a restart an old button click degrades to
161
+ an `ActionExpiredError` ("this action expired"), which `createBot` swallows.
162
+ **Durable actions require an external store (Redis / DB) — not shipped in
163
+ v1.** Implement the `ActionStore` interface (`put` / `get` / `delete`) and
164
+ pass it as `actionStore` to make actions survive restarts.
165
+
166
+ ## Writing a `PlatformAdapter`
167
+
168
+ To target a new surface, implement `PlatformAdapter` from this package. The
169
+ engine drives ingress through the `IngressSink` you receive in `start(sink)`
170
+ (`sink.onTurn(IncomingTurn)` / `sink.onInteraction(InteractionEvent)` /
171
+ `sink.onCommand(IncomingCommand)` / `sink.onThreadStarted(IncomingThreadStart)`)
172
+ and egress through your `post` / `update` / `stream` / `delete` (which receive
173
+ `BotNode[]` to translate to a native payload via `render`). You also provide
174
+ `createRunRenderer(target)` (an AG-UI `RunRenderer`: the subscriber to stream
175
+ into, plus accessors for captured tool calls and interrupts that the run-loop
176
+ reads after each `runAgent`), `decodeInteraction(raw)` (native event → opaque
177
+ `InteractionEvent`), `lookupUser`, a `conversationStore`
178
+ (`getOrCreate` → `AgentSession`), and the surface `capabilities` /
179
+ `ackDeadlineMs`. Optional capability methods like `getMessages(target)` and
180
+ `postFile(target, args)` back the matching `thread` methods when the surface
181
+ supports them — likewise `setSuggestedPrompts(target, prompts, opts?)` and
182
+ `setThreadTitle(target, title)` back `thread.setSuggestedPrompts` /
183
+ `thread.setTitle`, and `sink.onThreadStarted(...)` emits the "conversation
184
+ opened" lifecycle event. Slash commands are also capability-gated: an adapter forwards
185
+ invocations via `sink.onCommand(IncomingCommand)`, and may implement
186
+ `registerCommands(specs)` to publish the bot's declared commands up front
187
+ (e.g. Discord's application-command API); adapters that omit it are skipped.
188
+ See `@copilotkit/channels-slack` for a complete implementation.
189
+
190
+ ## Exports
191
+
192
+ `createBot`, `Bot`, `CreateBotOptions`, `BotHandler`, `ThreadStartHandler`;
193
+ `Thread`; the `PlatformAdapter` boundary types (`RunRenderer`, `IngressSink`,
194
+ `IncomingTurn`, `InteractionEvent`, `IncomingCommand`, `IncomingThreadStart`,
195
+ `SurfaceCapabilities`,
196
+ `ReplyTarget`, `ConversationStore`, `AgentSession`, `CapturedToolCall`,
197
+ `CapturedInterrupt`, `UserQuery`); `ActionStore` / `InMemoryActionStore` /
198
+ `ActionSnapshot` / `ActionRegistry` / `ActionExpiredError`; `BotTool` /
199
+ `BotToolContext` / `defineBotTool` / `BotCommand` / `CommandContext` /
200
+ `CommandSpec` / `defineBotCommand` / `ContextEntry` /
201
+ `AgentToolDescriptor` / `ObjectSchema` and the tool helpers
202
+ (`toAgentToolDescriptors`, `parseToolArgs`, `stringifyHandlerResult`);
203
+ `mintId` / `stableStringify`; `runAgentLoop`; plus the re-exported
204
+ `@copilotkit/channels-ui` vocabulary.
@@ -0,0 +1,59 @@
1
+ import type { BotNode, InteractionContext, ComponentFn, Renderable, MessageReactionHandler } from "@copilotkit/channels-ui";
2
+ import type { ActionStore } from "./action-store.js";
3
+ export declare class ActionExpiredError extends Error {
4
+ constructor(id: string);
5
+ }
6
+ export declare class ActionRegistry {
7
+ private store;
8
+ private components;
9
+ private hot;
10
+ private messageReactions;
11
+ constructor(opts: {
12
+ store: ActionStore;
13
+ });
14
+ /** Cache a `<Message onReaction>` handler for the posted message (same-process). */
15
+ registerMessageReaction(messageId: string, handler: MessageReactionHandler): void;
16
+ /**
17
+ * Persist the message's reaction handler as a `{ component, props }` snapshot
18
+ * keyed by `messageId`, so a reaction after a restart re-renders the component
19
+ * and re-derives the handler — durable exactly like a registered-component
20
+ * `onClick` (and degrading the same way for inline/anonymous components).
21
+ */
22
+ persistMessageReaction(messageId: string, snap: {
23
+ component: string;
24
+ props: Record<string, unknown>;
25
+ conversationKey: string;
26
+ }): Promise<void>;
27
+ /**
28
+ * Resolve the `onReaction` handler for `messageId`: the hot cache first, then
29
+ * the durable snapshot (re-rendering the named component and re-plucking the
30
+ * root's handler). Returns `undefined` when neither resolves — e.g. an inline
31
+ * handler whose closure can't be re-derived after a restart.
32
+ */
33
+ resolveMessageReaction(messageId: string): Promise<MessageReactionHandler | undefined>;
34
+ registerComponent(name: string, fn: ComponentFn): void;
35
+ clearHotCache(): void;
36
+ bindTree(componentName: string, props: Record<string, unknown>, conversationKey: string): Promise<BotNode[]>;
37
+ bindRenderable(ui: Renderable, conversationKey: string): Promise<{
38
+ root: BotNode[];
39
+ onReaction?: MessageReactionHandler;
40
+ /**
41
+ * The component + props to persist for durable reaction routing, present
42
+ * only when `ui` was a component element with an `onReaction` (an inline IR
43
+ * tree has no component to re-render, so its handler stays in-memory).
44
+ */
45
+ reactionComponent?: {
46
+ component: string;
47
+ props: Record<string, unknown>;
48
+ };
49
+ }>;
50
+ private walk;
51
+ /**
52
+ * Run the click handler for `id` and return the clicked element's `value`
53
+ * (so callers can resolve a HITL `awaitChoice` waiter even when the platform
54
+ * couldn't carry the value in its callback payload). Returns `undefined` when
55
+ * the element has no `value`.
56
+ */
57
+ dispatch(id: string, ctx: InteractionContext): Promise<unknown>;
58
+ }
59
+ //# sourceMappingURL=action-registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"action-registry.d.ts","sourceRoot":"","sources":["../src/action-registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,OAAO,EAEP,kBAAkB,EAClB,WAAW,EACX,UAAU,EACV,sBAAsB,EACvB,MAAM,yBAAyB,CAAC;AAGjC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD,qBAAa,kBAAmB,SAAQ,KAAK;gBAC/B,EAAE,EAAE,MAAM;CAIvB;AAcD,qBAAa,cAAc;IACzB,OAAO,CAAC,KAAK,CAAc;IAC3B,OAAO,CAAC,UAAU,CAAkC;IAKpD,OAAO,CAAC,GAAG,CAAgE;IAI3E,OAAO,CAAC,gBAAgB,CAA6C;gBAEzD,IAAI,EAAE;QAAE,KAAK,EAAE,WAAW,CAAA;KAAE;IAIxC,oFAAoF;IACpF,uBAAuB,CACrB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,sBAAsB,GAC9B,IAAI;IAIP;;;;;OAKG;IACG,sBAAsB,CAC1B,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE;QACJ,SAAS,EAAE,MAAM,CAAC;QAClB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAC/B,eAAe,EAAE,MAAM,CAAC;KACzB,GACA,OAAO,CAAC,IAAI,CAAC;IAShB;;;;;OAKG;IACG,sBAAsB,CAC1B,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,sBAAsB,GAAG,SAAS,CAAC;IAa9C,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,WAAW,GAAG,IAAI;IAItD,aAAa,IAAI,IAAI;IAMf,QAAQ,CACZ,aAAa,EAAE,MAAM,EACrB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,eAAe,EAAE,MAAM,GACtB,OAAO,CAAC,OAAO,EAAE,CAAC;IAcf,cAAc,CAClB,EAAE,EAAE,UAAU,EACd,eAAe,EAAE,MAAM,GACtB,OAAO,CAAC;QACT,IAAI,EAAE,OAAO,EAAE,CAAC;QAChB,UAAU,CAAC,EAAE,sBAAsB,CAAC;QACpC;;;;WAIG;QACH,iBAAiB,CAAC,EAAE;YAAE,SAAS,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;SAAE,CAAC;KAC3E,CAAC;YAuBY,IAAI;IA0ClB;;;;;OAKG;IACG,QAAQ,CAAC,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,kBAAkB,GAAG,OAAO,CAAC,OAAO,CAAC;CAsBtE"}
@@ -0,0 +1,217 @@
1
+ import { isBound, getBoundArgs, renderToIR } from "@copilotkit/channels-ui";
2
+ import { mintId } from "./mint-id.js";
3
+ export class ActionExpiredError extends Error {
4
+ constructor(id) {
5
+ super(`Action "${id}" has expired or is no longer available.`);
6
+ this.name = "ActionExpiredError";
7
+ }
8
+ }
9
+ const EVENT_PROPS = ["onClick", "onSelect", "onSubmit"];
10
+ function isComponentElement(ui) {
11
+ return (typeof ui === "object" &&
12
+ ui !== null &&
13
+ typeof ui.type === "function");
14
+ }
15
+ export class ActionRegistry {
16
+ store;
17
+ components = new Map();
18
+ // Cache the handler AND the element's `value` per minted id. The value is
19
+ // needed to resolve HITL `awaitChoice` waiters on platforms whose callback
20
+ // payload can't carry it (e.g. Telegram's 64-byte callback_data only holds
21
+ // the action id), where `evt.value` arrives undefined.
22
+ hot = new Map();
23
+ // Same-process fast path for `<Message onReaction>` handlers, keyed by the
24
+ // posted message's id. Mirrors the `hot` action cache; the durable snapshot
25
+ // (below) is the cross-restart counterpart, exactly like onClick.
26
+ messageReactions = new Map();
27
+ constructor(opts) {
28
+ this.store = opts.store;
29
+ }
30
+ /** Cache a `<Message onReaction>` handler for the posted message (same-process). */
31
+ registerMessageReaction(messageId, handler) {
32
+ this.messageReactions.set(messageId, handler);
33
+ }
34
+ /**
35
+ * Persist the message's reaction handler as a `{ component, props }` snapshot
36
+ * keyed by `messageId`, so a reaction after a restart re-renders the component
37
+ * and re-derives the handler — durable exactly like a registered-component
38
+ * `onClick` (and degrading the same way for inline/anonymous components).
39
+ */
40
+ async persistMessageReaction(messageId, snap) {
41
+ await this.store.put(reactionKey(messageId), {
42
+ component: snap.component,
43
+ props: snap.props,
44
+ path: [],
45
+ conversationKey: snap.conversationKey,
46
+ });
47
+ }
48
+ /**
49
+ * Resolve the `onReaction` handler for `messageId`: the hot cache first, then
50
+ * the durable snapshot (re-rendering the named component and re-plucking the
51
+ * root's handler). Returns `undefined` when neither resolves — e.g. an inline
52
+ * handler whose closure can't be re-derived after a restart.
53
+ */
54
+ async resolveMessageReaction(messageId) {
55
+ const hot = this.messageReactions.get(messageId);
56
+ if (hot)
57
+ return hot;
58
+ const snap = await this.store.get(reactionKey(messageId));
59
+ if (!snap?.component)
60
+ return undefined;
61
+ const fn = this.components.get(snap.component);
62
+ if (!fn)
63
+ return undefined;
64
+ const root = renderToIR(fn(snap.props));
65
+ return takeMessageReaction(root);
66
+ }
67
+ registerComponent(name, fn) {
68
+ this.components.set(name, fn);
69
+ }
70
+ clearHotCache() {
71
+ this.hot.clear();
72
+ }
73
+ // Renders the named component, binds all event-prop handlers in the tree
74
+ // (mint id, hot-cache + ActionStore snapshot, rewrite prop to { id }), returns the bound IR.
75
+ async bindTree(componentName, props, conversationKey) {
76
+ const fn = this.components.get(componentName);
77
+ const root = renderToIR((fn ? fn(props) : props));
78
+ await this.walk(root, [], componentName, props, conversationKey);
79
+ return root;
80
+ }
81
+ // Binds an arbitrary Renderable for posting. If `ui` is a component element
82
+ // (`{ type: fn, props }`), it is registered + bound by name (cold-path
83
+ // re-render supported). Otherwise the IR is bound inline with `component:""`,
84
+ // meaning a cold-cache dispatch throws ActionExpiredError (intended
85
+ // degradation for inline handlers that can't be re-derived). A top-level
86
+ // `<Message onReaction>` handler is pulled off the IR (so it never reaches the
87
+ // adapter) and returned for the caller to associate with the posted message.
88
+ async bindRenderable(ui, conversationKey) {
89
+ let root;
90
+ let component;
91
+ let props;
92
+ if (isComponentElement(ui)) {
93
+ const fn = ui.type;
94
+ component = fn.name || "anonymous";
95
+ props = (ui.props ?? {});
96
+ this.registerComponent(component, fn);
97
+ root = await this.bindTree(component, props, conversationKey);
98
+ }
99
+ else {
100
+ root = renderToIR(ui);
101
+ await this.walk(root, [], "", undefined, conversationKey);
102
+ }
103
+ const onReaction = takeMessageReaction(root);
104
+ return {
105
+ root,
106
+ onReaction,
107
+ reactionComponent: onReaction && component && props ? { component, props } : undefined,
108
+ };
109
+ }
110
+ async walk(nodes, base, comp, props, conv) {
111
+ for (let i = 0; i < nodes.length; i++) {
112
+ const node = nodes[i];
113
+ const path = [...base, i];
114
+ for (const ep of EVENT_PROPS) {
115
+ const handler = node.props[ep];
116
+ if (typeof handler === "function") {
117
+ const fullPath = [...path, ep];
118
+ const id = mintId(comp, fullPath, props);
119
+ this.hot.set(id, {
120
+ handler: handler,
121
+ value: node.props.value,
122
+ });
123
+ await this.store.put(id, {
124
+ component: comp,
125
+ props,
126
+ path: fullPath,
127
+ conversationKey: conv,
128
+ boundArgs: isBound(handler) ? getBoundArgs(handler) : undefined,
129
+ });
130
+ node.props[ep] = { id };
131
+ }
132
+ }
133
+ const children = node.props.children;
134
+ if (Array.isArray(children)) {
135
+ await this.walk(children, [...path, "children"], comp, props, conv);
136
+ }
137
+ }
138
+ }
139
+ /**
140
+ * Run the click handler for `id` and return the clicked element's `value`
141
+ * (so callers can resolve a HITL `awaitChoice` waiter even when the platform
142
+ * couldn't carry the value in its callback payload). Returns `undefined` when
143
+ * the element has no `value`.
144
+ */
145
+ async dispatch(id, ctx) {
146
+ let handler;
147
+ let value;
148
+ const hot = this.hot.get(id);
149
+ if (hot) {
150
+ handler = hot.handler;
151
+ value = hot.value;
152
+ }
153
+ else {
154
+ const snap = await this.store.get(id);
155
+ if (!snap || !snap.component)
156
+ throw new ActionExpiredError(id);
157
+ const fn = this.components.get(snap.component);
158
+ if (!fn)
159
+ throw new ActionExpiredError(id);
160
+ const tree = renderToIR(fn(snap.props));
161
+ handler = pluck(tree, snap.path);
162
+ value = pluckValue(tree, snap.path);
163
+ if (!handler)
164
+ throw new ActionExpiredError(id);
165
+ }
166
+ await handler({ ...ctx, action: { ...ctx.action, id } });
167
+ return value;
168
+ }
169
+ }
170
+ /** Store key for a message's durable reaction snapshot (distinct from minted action ids). */
171
+ function reactionKey(messageId) {
172
+ return `reaction:${messageId}`;
173
+ }
174
+ /**
175
+ * Pull a top-level `<Message onReaction>` handler off the IR, deleting the prop
176
+ * so it never reaches the adapter (a function can't be serialized to a native
177
+ * payload). Returns the handler when the single root node is a `message`.
178
+ */
179
+ function takeMessageReaction(root) {
180
+ const node = root.length === 1 ? root[0] : undefined;
181
+ if (!node || node.type !== "message" || !("onReaction" in node.props)) {
182
+ return undefined;
183
+ }
184
+ const handler = node.props.onReaction;
185
+ delete node.props.onReaction;
186
+ return typeof handler === "function"
187
+ ? handler
188
+ : undefined;
189
+ }
190
+ /** Navigate to the node owning the event-prop at `path` and read its `value`. */
191
+ function pluckValue(tree, path) {
192
+ let cur = tree;
193
+ for (const seg of path.slice(0, -1)) {
194
+ if (Array.isArray(cur))
195
+ cur = cur[seg];
196
+ else if (cur && typeof cur === "object")
197
+ cur = cur.props?.[seg];
198
+ else
199
+ return undefined;
200
+ }
201
+ return cur?.props?.value;
202
+ }
203
+ function pluck(tree, path) {
204
+ let cur = tree;
205
+ for (const seg of path.slice(0, -1)) {
206
+ if (Array.isArray(cur))
207
+ cur = cur[seg];
208
+ else if (cur && typeof cur === "object")
209
+ cur = cur.props?.[seg];
210
+ else
211
+ return undefined;
212
+ }
213
+ const ep = path[path.length - 1];
214
+ const node = cur;
215
+ const h = node?.props?.[ep];
216
+ return typeof h === "function" ? h : undefined;
217
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=action-registry.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"action-registry.test.d.ts","sourceRoot":"","sources":["../src/action-registry.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,115 @@
1
+ import { describe, it, expect, beforeEach } from "vitest";
2
+ import { ActionRegistry, ActionExpiredError } from "./action-registry.js";
3
+ import { InMemoryActionStore } from "./action-store.js";
4
+ import { MemoryStore } from "./state/memory-store.js";
5
+ import { kvActionStore } from "./state/kv-action-store.js";
6
+ // Records each click so a test can assert the handler ran — dispatch() now
7
+ // returns the clicked element's `value` (needed to resolve HITL waiters on
8
+ // platforms whose callback payload can't carry it), not the handler's return.
9
+ const clicks = [];
10
+ function Confirm(props) {
11
+ return {
12
+ type: "actions",
13
+ props: {
14
+ children: [
15
+ {
16
+ type: "button",
17
+ props: {
18
+ value: { ok: props.action },
19
+ onClick: ({ action }) => {
20
+ clicks.push(`ok:${props.action}:${action.id}`);
21
+ },
22
+ children: "Yes",
23
+ },
24
+ },
25
+ ],
26
+ },
27
+ };
28
+ }
29
+ const ctx = {};
30
+ describe("ActionRegistry", () => {
31
+ beforeEach(() => {
32
+ clicks.length = 0;
33
+ });
34
+ it("dispatches via hot cache, runs the handler, and returns the element value", async () => {
35
+ const reg = new ActionRegistry({ store: new InMemoryActionStore() });
36
+ reg.registerComponent("Confirm", Confirm);
37
+ const ir = await reg.bindTree("Confirm", { action: "write" }, "conv1");
38
+ const button = ir[0].props.children[0];
39
+ const id = button.props.onClick.id;
40
+ expect(typeof id).toBe("string");
41
+ const value = await reg.dispatch(id, ctx);
42
+ expect(value).toEqual({ ok: "write" });
43
+ expect(clicks[0]).toContain("ok:write:");
44
+ });
45
+ it("cold path re-renders from snapshot when hot cache is cleared, still returning the value", async () => {
46
+ const reg = new ActionRegistry({ store: new InMemoryActionStore() });
47
+ reg.registerComponent("Confirm", Confirm);
48
+ const ir = await reg.bindTree("Confirm", { action: "write" }, "conv1");
49
+ const id = ir[0].props.children[0].props.onClick.id;
50
+ reg.clearHotCache();
51
+ const value = await reg.dispatch(id, ctx);
52
+ expect(value).toEqual({ ok: "write" });
53
+ expect(clicks[0]).toContain("ok:write:");
54
+ });
55
+ it("throws ActionExpiredError on full miss", async () => {
56
+ const reg = new ActionRegistry({ store: new InMemoryActionStore() });
57
+ await expect(reg.dispatch("ck:missing", ctx)).rejects.toBeInstanceOf(ActionExpiredError);
58
+ });
59
+ it("dispatches from a cold registry via a shared store (restart survival)", async () => {
60
+ const state = new MemoryStore();
61
+ // registryA: bind a tree and persist snapshot to shared state
62
+ const regA = new ActionRegistry({ store: kvActionStore(state) });
63
+ regA.registerComponent("Confirm", Confirm);
64
+ const ir = await regA.bindTree("Confirm", { action: "approve" }, "conv-cold");
65
+ const id = ir[0].props.children[0].props.onClick.id;
66
+ // registryB: fresh registry with no hot cache but sharing the same store
67
+ const regB = new ActionRegistry({ store: kvActionStore(state) });
68
+ regB.registerComponent("Confirm", Confirm);
69
+ const value = await regB.dispatch(id, ctx);
70
+ expect(value).toEqual({ ok: "approve" });
71
+ expect(clicks[0]).toContain("ok:approve:");
72
+ });
73
+ it("throws ActionExpiredError when the snapshot is absent (missing id)", async () => {
74
+ const reg = new ActionRegistry({
75
+ store: kvActionStore(new MemoryStore()),
76
+ });
77
+ await expect(reg.dispatch("ck:missing", ctx)).rejects.toBeInstanceOf(ActionExpiredError);
78
+ });
79
+ describe("components-seeded registry (createBot components option)", () => {
80
+ it("enables cold dispatch after simulated restart when component is pre-registered", async () => {
81
+ // Shared store survives the "restart" (like Redis across process restarts).
82
+ const sharedState = new MemoryStore();
83
+ // Registry A: bind a named component and persist its snapshot.
84
+ const regA = new ActionRegistry({ store: kvActionStore(sharedState) });
85
+ regA.registerComponent("Confirm", Confirm);
86
+ const ir = await regA.bindTree("Confirm", { action: "restart-test" }, "conv-restart");
87
+ const id = ir[0].props.children[0].props.onClick.id;
88
+ // Registry B: fresh process — no hot cache, but component is seeded via
89
+ // the createBot `components` option equivalent (registerComponent at startup).
90
+ const regB = new ActionRegistry({ store: kvActionStore(sharedState) });
91
+ regB.registerComponent("Confirm", Confirm);
92
+ // Cold dispatch must succeed and fire the handler.
93
+ const handlerFired = [];
94
+ // Wrap the existing Confirm component so we can assert the specific call.
95
+ const value = await regB.dispatch(id, ctx);
96
+ expect(value).toEqual({ ok: "restart-test" });
97
+ // The shared `clicks` array is populated by Confirm's onClick.
98
+ expect(clicks.some((c) => c.includes("ok:restart-test:"))).toBe(true);
99
+ });
100
+ it("throws ActionExpiredError when component is NOT pre-registered (no-registration degradation)", async () => {
101
+ const sharedState = new MemoryStore();
102
+ // Registry A: bind and persist.
103
+ const regA = new ActionRegistry({ store: kvActionStore(sharedState) });
104
+ regA.registerComponent("Confirm", Confirm);
105
+ const ir = await regA.bindTree("Confirm", { action: "no-reg" }, "conv-no-reg");
106
+ const id = ir[0].props.children[0].props.onClick.id;
107
+ // Registry B': fresh process, shared store, but component NOT registered.
108
+ const regBPrime = new ActionRegistry({
109
+ store: kvActionStore(sharedState),
110
+ });
111
+ // Without registration the cold path cannot re-render → ActionExpiredError.
112
+ await expect(regBPrime.dispatch(id, ctx)).rejects.toBeInstanceOf(ActionExpiredError);
113
+ });
114
+ });
115
+ });
@@ -0,0 +1,21 @@
1
+ export interface ActionSnapshot {
2
+ component?: string;
3
+ props?: unknown;
4
+ path: (string | number)[];
5
+ boundArgs?: unknown;
6
+ conversationKey: string;
7
+ }
8
+ /** @deprecated Configure `createBot({ state })` instead. Action snapshots are stored via `StateStore.kv`. */
9
+ export interface ActionStore {
10
+ put(id: string, snap: ActionSnapshot, ttlMs?: number): Promise<void>;
11
+ get(id: string): Promise<ActionSnapshot | undefined>;
12
+ delete(id: string): Promise<void>;
13
+ }
14
+ /** @deprecated Configure `createBot({ state })` instead. Action snapshots are stored via `StateStore.kv`. */
15
+ export declare class InMemoryActionStore implements ActionStore {
16
+ private map;
17
+ put(id: string, snap: ActionSnapshot, ttlMs?: number): Promise<void>;
18
+ get(id: string): Promise<ActionSnapshot | undefined>;
19
+ delete(id: string): Promise<void>;
20
+ }
21
+ //# sourceMappingURL=action-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"action-store.d.ts","sourceRoot":"","sources":["../src/action-store.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,cAAc;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,IAAI,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC;IAC1B,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;CACzB;AACD,6GAA6G;AAC7G,MAAM,WAAW,WAAW;IAC1B,GAAG,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,cAAc,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrE,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC,CAAC;IACrD,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACnC;AACD,6GAA6G;AAC7G,qBAAa,mBAAoB,YAAW,WAAW;IACrD,OAAO,CAAC,GAAG,CAAmE;IACxE,GAAG,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,cAAc,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAMpE,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC;IASpD,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAGxC"}