@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.
- package/LICENSE +21 -0
- package/README.md +204 -0
- package/dist/action-registry.d.ts +59 -0
- package/dist/action-registry.d.ts.map +1 -0
- package/dist/action-registry.js +217 -0
- package/dist/action-registry.test.d.ts +2 -0
- package/dist/action-registry.test.d.ts.map +1 -0
- package/dist/action-registry.test.js +115 -0
- package/dist/action-store.d.ts +21 -0
- package/dist/action-store.d.ts.map +1 -0
- package/dist/action-store.js +23 -0
- package/dist/action-store.test.d.ts +2 -0
- package/dist/action-store.test.d.ts.map +1 -0
- package/dist/action-store.test.js +27 -0
- package/dist/codec.d.ts +25 -0
- package/dist/codec.d.ts.map +1 -0
- package/dist/codec.js +1 -0
- package/dist/commands.d.ts +73 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/commands.js +38 -0
- package/dist/commands.test.d.ts +2 -0
- package/dist/commands.test.d.ts.map +1 -0
- package/dist/commands.test.js +23 -0
- package/dist/create-bot.d.ts +171 -0
- package/dist/create-bot.d.ts.map +1 -0
- package/dist/create-bot.foundations.test.d.ts +2 -0
- package/dist/create-bot.foundations.test.d.ts.map +1 -0
- package/dist/create-bot.foundations.test.js +147 -0
- package/dist/create-bot.js +576 -0
- package/dist/create-bot.test.d.ts +2 -0
- package/dist/create-bot.test.d.ts.map +1 -0
- package/dist/create-bot.test.js +936 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +32 -0
- package/dist/mint-id.d.ts +3 -0
- package/dist/mint-id.d.ts.map +1 -0
- package/dist/mint-id.js +20 -0
- package/dist/mint-id.test.d.ts +2 -0
- package/dist/mint-id.test.d.ts.map +1 -0
- package/dist/mint-id.test.js +16 -0
- package/dist/modal-submit.test.d.ts +2 -0
- package/dist/modal-submit.test.d.ts.map +1 -0
- package/dist/modal-submit.test.js +68 -0
- package/dist/platform-adapter.d.ts +312 -0
- package/dist/platform-adapter.d.ts.map +1 -0
- package/dist/platform-adapter.js +1 -0
- package/dist/platform-adapter.test.d.ts +2 -0
- package/dist/platform-adapter.test.d.ts.map +1 -0
- package/dist/platform-adapter.test.js +40 -0
- package/dist/reactions.test.d.ts +2 -0
- package/dist/reactions.test.d.ts.map +1 -0
- package/dist/reactions.test.js +197 -0
- package/dist/run-loop.d.ts +41 -0
- package/dist/run-loop.d.ts.map +1 -0
- package/dist/run-loop.js +102 -0
- package/dist/run-loop.test.d.ts +2 -0
- package/dist/run-loop.test.d.ts.map +1 -0
- package/dist/run-loop.test.js +125 -0
- package/dist/standard-schema.d.ts +52 -0
- package/dist/standard-schema.d.ts.map +1 -0
- package/dist/standard-schema.js +66 -0
- package/dist/state/duration.d.ts +3 -0
- package/dist/state/duration.d.ts.map +1 -0
- package/dist/state/duration.js +16 -0
- package/dist/state/duration.test.d.ts +2 -0
- package/dist/state/duration.test.d.ts.map +1 -0
- package/dist/state/duration.test.js +13 -0
- package/dist/state/kv-action-store.d.ts +7 -0
- package/dist/state/kv-action-store.d.ts.map +1 -0
- package/dist/state/kv-action-store.js +9 -0
- package/dist/state/kv-action-store.test.d.ts +2 -0
- package/dist/state/kv-action-store.test.d.ts.map +1 -0
- package/dist/state/kv-action-store.test.js +21 -0
- package/dist/state/memory-store.d.ts +41 -0
- package/dist/state/memory-store.d.ts.map +1 -0
- package/dist/state/memory-store.js +110 -0
- package/dist/state/memory-store.test.d.ts +2 -0
- package/dist/state/memory-store.test.d.ts.map +1 -0
- package/dist/state/memory-store.test.js +22 -0
- package/dist/state/state-conversation-store.d.ts +14 -0
- package/dist/state/state-conversation-store.d.ts.map +1 -0
- package/dist/state/state-conversation-store.js +22 -0
- package/dist/state/state-conversation-store.test.d.ts +2 -0
- package/dist/state/state-conversation-store.test.d.ts.map +1 -0
- package/dist/state/state-conversation-store.test.js +58 -0
- package/dist/state/state-store.d.ts +63 -0
- package/dist/state/state-store.d.ts.map +1 -0
- package/dist/state/state-store.js +1 -0
- package/dist/telemetry/bot-telemetry.d.ts +31 -0
- package/dist/telemetry/bot-telemetry.d.ts.map +1 -0
- package/dist/telemetry/bot-telemetry.js +70 -0
- package/dist/telemetry/bot-telemetry.test.d.ts +2 -0
- package/dist/telemetry/bot-telemetry.test.d.ts.map +1 -0
- package/dist/telemetry/bot-telemetry.test.js +56 -0
- package/dist/telemetry/create-bot-telemetry.test.d.ts +2 -0
- package/dist/telemetry/create-bot-telemetry.test.d.ts.map +1 -0
- package/dist/telemetry/create-bot-telemetry.test.js +71 -0
- package/dist/telemetry/e2e-telemetry.test.d.ts +2 -0
- package/dist/telemetry/e2e-telemetry.test.d.ts.map +1 -0
- package/dist/telemetry/e2e-telemetry.test.js +56 -0
- package/dist/telemetry/events-catalog.test.d.ts +2 -0
- package/dist/telemetry/events-catalog.test.d.ts.map +1 -0
- package/dist/telemetry/events-catalog.test.js +8 -0
- package/dist/telemetry/install-id.d.ts +8 -0
- package/dist/telemetry/install-id.d.ts.map +1 -0
- package/dist/telemetry/install-id.js +43 -0
- package/dist/telemetry/install-id.test.d.ts +2 -0
- package/dist/telemetry/install-id.test.d.ts.map +1 -0
- package/dist/telemetry/install-id.test.js +43 -0
- package/dist/telemetry/platform-allowlist-coverage.test.d.ts +2 -0
- package/dist/telemetry/platform-allowlist-coverage.test.d.ts.map +1 -0
- package/dist/telemetry/platform-allowlist-coverage.test.js +45 -0
- package/dist/telemetry/sanitize-error.d.ts +3 -0
- package/dist/telemetry/sanitize-error.d.ts.map +1 -0
- package/dist/telemetry/sanitize-error.js +31 -0
- package/dist/telemetry/sanitize-error.test.d.ts +2 -0
- package/dist/telemetry/sanitize-error.test.d.ts.map +1 -0
- package/dist/telemetry/sanitize-error.test.js +33 -0
- package/dist/testing/fake-adapter.d.ts +137 -0
- package/dist/testing/fake-adapter.d.ts.map +1 -0
- package/dist/testing/fake-adapter.js +273 -0
- package/dist/testing/fake-agent.d.ts +28 -0
- package/dist/testing/fake-agent.d.ts.map +1 -0
- package/dist/testing/fake-agent.js +36 -0
- package/dist/testing/state-store-conformance.d.ts +4 -0
- package/dist/testing/state-store-conformance.d.ts.map +1 -0
- package/dist/testing/state-store-conformance.js +117 -0
- package/dist/thread-capabilities.test.d.ts +2 -0
- package/dist/thread-capabilities.test.d.ts.map +1 -0
- package/dist/thread-capabilities.test.js +77 -0
- package/dist/thread-ephemeral.test.d.ts +2 -0
- package/dist/thread-ephemeral.test.d.ts.map +1 -0
- package/dist/thread-ephemeral.test.js +45 -0
- package/dist/thread-reactions.test.d.ts +2 -0
- package/dist/thread-reactions.test.d.ts.map +1 -0
- package/dist/thread-reactions.test.js +38 -0
- package/dist/thread.d.ts +152 -0
- package/dist/thread.d.ts.map +1 -0
- package/dist/thread.js +329 -0
- package/dist/thread.test.d.ts +2 -0
- package/dist/thread.test.d.ts.map +1 -0
- package/dist/thread.test.js +83 -0
- package/dist/tools.d.ts +67 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +38 -0
- package/dist/tools.test.d.ts +2 -0
- package/dist/tools.test.d.ts.map +1 -0
- package/dist/tools.test.js +31 -0
- package/dist/transcripts.d.ts +52 -0
- package/dist/transcripts.d.ts.map +1 -0
- package/dist/transcripts.js +67 -0
- package/dist/transcripts.test.d.ts +2 -0
- package/dist/transcripts.test.d.ts.map +1 -0
- package/dist/transcripts.test.js +108 -0
- 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 @@
|
|
|
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"}
|