@copilotkit/channels-core 0.5.0 → 0.5.1-canary.1785633429
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/README.md +35 -14
- package/dist/action-registry.d.ts +15 -3
- package/dist/action-registry.d.ts.map +1 -1
- package/dist/action-registry.js +61 -9
- package/dist/action-registry.test.js +107 -2
- package/dist/action-store.d.ts +23 -0
- package/dist/action-store.d.ts.map +1 -1
- package/dist/action-store.js +11 -0
- package/dist/canonical-run-loop.test.js +6 -1
- package/dist/channel-identity.test.d.ts +2 -0
- package/dist/channel-identity.test.d.ts.map +1 -0
- package/dist/channel-identity.test.js +297 -0
- package/dist/channel-memory.test.d.ts +2 -0
- package/dist/channel-memory.test.d.ts.map +1 -0
- package/dist/channel-memory.test.js +199 -0
- package/dist/commands.d.ts +5 -3
- package/dist/commands.d.ts.map +1 -1
- package/dist/create-channel.d.ts +30 -56
- package/dist/create-channel.d.ts.map +1 -1
- package/dist/create-channel.foundations.test.js +30 -7
- package/dist/create-channel.js +209 -72
- package/dist/create-channel.test.js +293 -66
- package/dist/hitl-continuation.test.d.ts +2 -0
- package/dist/hitl-continuation.test.d.ts.map +1 -0
- package/dist/hitl-continuation.test.js +393 -0
- package/dist/identity.d.ts +51 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/identity.js +56 -0
- package/dist/identity.test.d.ts +2 -0
- package/dist/identity.test.d.ts.map +1 -0
- package/dist/identity.test.js +109 -0
- package/dist/index.d.ts +10 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/managed-v1-await-choice-guard.test.js +4 -1
- package/dist/memory.d.ts +23 -0
- package/dist/memory.d.ts.map +1 -0
- package/dist/memory.js +25 -0
- package/dist/modal-submit.test.js +21 -6
- package/dist/open-modal.test.js +16 -4
- package/dist/platform-adapter.d.ts +29 -6
- package/dist/platform-adapter.d.ts.map +1 -1
- package/dist/platform-adapter.test.js +2 -0
- package/dist/reactions.test.js +49 -11
- package/dist/run-loop.test.js +30 -5
- package/dist/sanitize-agent-events.test.js +2 -0
- package/dist/source-platform.test.js +24 -9
- package/dist/state/kv-action-store.d.ts.map +1 -1
- package/dist/state/kv-action-store.js +1 -0
- package/dist/state/kv-action-store.test.js +11 -0
- package/dist/state/memory-store.d.ts +1 -0
- package/dist/state/memory-store.d.ts.map +1 -1
- package/dist/state/memory-store.js +9 -0
- package/dist/state/state-store.d.ts +2 -0
- package/dist/state/state-store.d.ts.map +1 -1
- package/dist/telemetry/create-channel-telemetry.test.js +4 -2
- package/dist/telemetry/e2e-telemetry.test.js +1 -0
- package/dist/telemetry/install-id.test.js +5 -0
- package/dist/testing/fake-adapter.d.ts +26 -9
- package/dist/testing/fake-adapter.d.ts.map +1 -1
- package/dist/testing/fake-adapter.js +65 -21
- package/dist/testing/state-store-conformance.d.ts.map +1 -1
- package/dist/testing/state-store-conformance.js +16 -0
- package/dist/thread-capabilities.test.js +29 -9
- package/dist/thread-ephemeral.test.js +8 -5
- package/dist/thread-promise-contract.test.js +4 -0
- package/dist/thread-reactions.test.js +8 -2
- package/dist/thread.d.ts +50 -7
- package/dist/thread.d.ts.map +1 -1
- package/dist/thread.js +163 -17
- package/dist/thread.test.js +4 -0
- package/dist/tools.d.ts +5 -3
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +2 -1
- package/dist/transcripts.d.ts +6 -13
- package/dist/transcripts.d.ts.map +1 -1
- package/dist/transcripts.js +12 -12
- package/dist/transcripts.test.js +28 -29
- package/dist/welcome.test.d.ts +2 -0
- package/dist/welcome.test.d.ts.map +1 -0
- package/dist/welcome.test.js +53 -0
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -32,23 +32,28 @@ import { slack } from "@copilotkit/channels-slack";
|
|
|
32
32
|
`createChannel(opts)` returns a `Channel`:
|
|
33
33
|
|
|
34
34
|
- `onMention(handler)` / `onMessage(handler)` — turn handlers receiving
|
|
35
|
-
`{ thread, message }`.
|
|
36
|
-
|
|
37
|
-
fire.)
|
|
35
|
+
`{ thread, message }`. Bot mentions select `onMention` when registered and
|
|
36
|
+
otherwise fall back to `onMessage`; other content selects `onMessage`.
|
|
38
37
|
- `onThreadStarted(handler)` — a conversation surface opened (e.g. the Slack
|
|
39
|
-
assistant pane); receives `{ thread, user
|
|
38
|
+
assistant pane); receives `{ thread, user, actor }`. Greet, set suggested prompts
|
|
40
39
|
or a title, or run the agent. Adapters without the concept never fire it.
|
|
40
|
+
- `onWelcome(handler)` — a provider installation or conversation activation;
|
|
41
|
+
receives `{ thread, user?, platform }`. Adapters without a reliable
|
|
42
|
+
activation event never fire it.
|
|
41
43
|
- `onInteraction<TValue>(id, handler)` — explicit escape-hatch handler for a
|
|
42
44
|
known action id, bypassing the registry; `ctx.action.value` is typed `TValue`.
|
|
43
45
|
- `onInterrupt<TPayload>(eventName, handler)` — handle a captured agent
|
|
44
|
-
interrupt (LangGraph-style `on_interrupt`); receives `{ payload, thread }`
|
|
46
|
+
interrupt (LangGraph-style `on_interrupt`); receives `{ payload, thread, user, actor }`
|
|
45
47
|
with `payload` typed `TPayload`.
|
|
46
48
|
- `onCommand(command)` / `onCommand(name, handler)` — register a slash command.
|
|
47
|
-
The handler gets `{ thread, command, text, options, user }`. `text` is the
|
|
49
|
+
The handler gets `{ thread, command, text, options, user, actor }`. `text` is the
|
|
48
50
|
raw args (Slack); `options` is the typed, parsed form (`defineChannelCommand`
|
|
49
51
|
with an `options` Standard Schema) for surfaces with native structured args
|
|
50
|
-
(e.g. Discord).
|
|
51
|
-
|
|
52
|
+
(e.g. Discord). This is a direct-adapter capability: managed Slack and Teams
|
|
53
|
+
treat leading-slash text as ordinary message content and do not register
|
|
54
|
+
provider slash commands. Forwarded to direct adapters that support commands
|
|
55
|
+
and ignored elsewhere — also pass them up front via `commands` in
|
|
56
|
+
`CreateChannelOptions`.
|
|
52
57
|
- `tool(t)` — register a `ChannelTool` (alternative to `opts.tools`); must be
|
|
53
58
|
added before the runtime activates the channel.
|
|
54
59
|
|
|
@@ -73,6 +78,7 @@ import { createCopilotNodeListener } from "@copilotkit/runtime/v2/node";
|
|
|
73
78
|
|
|
74
79
|
const channel = createChannel({
|
|
75
80
|
name: "support-bot", // project-unique Intelligence Channel name
|
|
81
|
+
identifyUser: "platform",
|
|
76
82
|
adapters: [slack({ botToken, appToken })],
|
|
77
83
|
});
|
|
78
84
|
|
|
@@ -82,7 +88,6 @@ const runtime = new CopilotRuntime({
|
|
|
82
88
|
// both together only for a self-hosted deployment.
|
|
83
89
|
apiKey: process.env.COPILOTKIT_INTELLIGENCE_API_KEY!, // free tier available
|
|
84
90
|
}),
|
|
85
|
-
identifyUser: async () => ({ id: "support-bot", name: "Support Bot" }),
|
|
86
91
|
channels: [channel],
|
|
87
92
|
});
|
|
88
93
|
|
|
@@ -93,6 +98,12 @@ await listener.channels.ready();
|
|
|
93
98
|
// await listener.channels.stop(); // tears them down
|
|
94
99
|
```
|
|
95
100
|
|
|
101
|
+
The runtime also declares managed Slack and Teams for the same Channel name.
|
|
102
|
+
Developer-owned adapters stay in the adapter array and may run alongside that
|
|
103
|
+
managed adapter. If managed setup is incomplete, configured direct adapters
|
|
104
|
+
still start while `listener.channels.status()` reports `setup_required` for the
|
|
105
|
+
managed path.
|
|
106
|
+
|
|
96
107
|
## `Thread`
|
|
97
108
|
|
|
98
109
|
A `Thread` is the per-conversation handle handed to your handlers and tool
|
|
@@ -108,8 +119,15 @@ interface Thread {
|
|
|
108
119
|
runAgent(input?: {
|
|
109
120
|
context?: ContextEntry[];
|
|
110
121
|
tools?: ChannelTool[];
|
|
122
|
+
memory?: MemoryGrant;
|
|
111
123
|
}): Promise<MessageRef | undefined>;
|
|
112
|
-
resume(
|
|
124
|
+
resume(
|
|
125
|
+
value: unknown,
|
|
126
|
+
options?: {
|
|
127
|
+
memory?: MemoryGrant;
|
|
128
|
+
subject?: "initiator" | "actor";
|
|
129
|
+
},
|
|
130
|
+
): Promise<MessageRef | undefined>;
|
|
113
131
|
awaitChoice<T = unknown>(ui: Renderable): Promise<T>;
|
|
114
132
|
// Capability-gated (return { ok: false } on surfaces without support):
|
|
115
133
|
setSuggestedPrompts(
|
|
@@ -208,8 +226,9 @@ reads after each `runAgent`), `decodeInteraction(raw)` (native event → opaque
|
|
|
208
226
|
`postFile(target, args)` back the matching `thread` methods when the surface
|
|
209
227
|
supports them — likewise `setSuggestedPrompts(target, prompts, opts?)` and
|
|
210
228
|
`setThreadTitle(target, title)` back `thread.setSuggestedPrompts` /
|
|
211
|
-
`thread.setTitle`,
|
|
212
|
-
|
|
229
|
+
`thread.setTitle`, `sink.onWelcome(...)` emits a reliable installation or
|
|
230
|
+
activation event, and `sink.onThreadStarted(...)` emits the "conversation
|
|
231
|
+
opened" lifecycle event. Direct-adapter slash commands are also capability-gated: an adapter forwards
|
|
213
232
|
invocations via `sink.onCommand(IncomingCommand)`, and may implement
|
|
214
233
|
`registerCommands(specs)` to publish the channel's declared commands up front
|
|
215
234
|
(e.g. Discord's application-command API); adapters that omit it are skipped.
|
|
@@ -217,9 +236,11 @@ See `@copilotkit/channels-slack` for a complete implementation.
|
|
|
217
236
|
|
|
218
237
|
## Exports
|
|
219
238
|
|
|
220
|
-
`createChannel`, `Channel`, `CreateChannelOptions`, `ChannelHandler`,
|
|
239
|
+
`createChannel`, `Channel`, `CreateChannelOptions`, `ChannelHandler`,
|
|
240
|
+
`WelcomeHandler`, `ThreadStartHandler`;
|
|
221
241
|
`Thread`; the `PlatformAdapter` boundary types (`RunRenderer`, `IngressSink`,
|
|
222
|
-
`IncomingTurn`, `InteractionEvent`, `IncomingCommand`, `
|
|
242
|
+
`IncomingTurn`, `InteractionEvent`, `IncomingCommand`, `IncomingWelcome`,
|
|
243
|
+
`IncomingThreadStart`,
|
|
223
244
|
`SurfaceCapabilities`,
|
|
224
245
|
`ReplyTarget`, `ConversationStore`, `AgentSession`, `CapturedToolCall`,
|
|
225
246
|
`CapturedInterrupt`, `UserQuery`); `ActionStore` / `InMemoryActionStore` /
|
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
import type { ChannelNode, InteractionContext, ComponentFn, Renderable, MessageReactionHandler } from "@copilotkit/channels-ui";
|
|
2
|
-
import type { ActionStore } from "./action-store.js";
|
|
2
|
+
import type { ActionContinuationContext, ActionContinuationBinding, ActionContinuationSnapshot, ActionStore } from "./action-store.js";
|
|
3
3
|
export declare class ActionExpiredError extends Error {
|
|
4
|
+
readonly code = "channel_action_expired";
|
|
4
5
|
constructor(id: string);
|
|
5
6
|
}
|
|
7
|
+
/** A continuation capability was presented outside its trusted Channel binding. */
|
|
8
|
+
export declare class ActionContinuationMismatchError extends Error {
|
|
9
|
+
readonly code = "channel_continuation_mismatch";
|
|
10
|
+
constructor();
|
|
11
|
+
}
|
|
6
12
|
export declare class ActionRegistry {
|
|
7
13
|
private store;
|
|
8
14
|
private components;
|
|
9
15
|
private hot;
|
|
10
16
|
private messageReactions;
|
|
17
|
+
private readonly retentionMs?;
|
|
11
18
|
constructor(opts: {
|
|
12
19
|
store: ActionStore;
|
|
20
|
+
retentionMs?: number;
|
|
13
21
|
});
|
|
14
22
|
/** Cache a `<Message onReaction>` handler for the posted message (same-process). */
|
|
15
23
|
registerMessageReaction(messageId: string, handler: MessageReactionHandler): void;
|
|
@@ -33,8 +41,8 @@ export declare class ActionRegistry {
|
|
|
33
41
|
resolveMessageReaction(messageId: string): Promise<MessageReactionHandler | undefined>;
|
|
34
42
|
registerComponent(name: string, fn: ComponentFn): void;
|
|
35
43
|
clearHotCache(): void;
|
|
36
|
-
bindTree(componentName: string, props: Record<string, unknown>, conversationKey: string): Promise<ChannelNode[]>;
|
|
37
|
-
bindRenderable(ui: Renderable, conversationKey: string): Promise<{
|
|
44
|
+
bindTree(componentName: string, props: Record<string, unknown>, conversationKey: string, continuation?: ActionContinuationContext): Promise<ChannelNode[]>;
|
|
45
|
+
bindRenderable(ui: Renderable, conversationKey: string, continuation?: ActionContinuationContext): Promise<{
|
|
38
46
|
root: ChannelNode[];
|
|
39
47
|
onReaction?: MessageReactionHandler;
|
|
40
48
|
/**
|
|
@@ -55,5 +63,9 @@ export declare class ActionRegistry {
|
|
|
55
63
|
* the element has no `value`.
|
|
56
64
|
*/
|
|
57
65
|
dispatch(id: string, ctx: InteractionContext): Promise<unknown>;
|
|
66
|
+
/** Read and validate one continuation capability without consuming it. */
|
|
67
|
+
getContinuation(id: string, expected: ActionContinuationBinding): Promise<ActionContinuationSnapshot>;
|
|
68
|
+
/** Validate and atomically consume one continuation capability. */
|
|
69
|
+
claimContinuation(id: string, expected: ActionContinuationBinding): Promise<ActionContinuationSnapshot>;
|
|
58
70
|
}
|
|
59
71
|
//# sourceMappingURL=action-registry.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"action-registry.d.ts","sourceRoot":"","sources":["../src/action-registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,WAAW,EAEX,kBAAkB,EAClB,WAAW,EACX,UAAU,EACV,sBAAsB,EACvB,MAAM,yBAAyB,CAAC;AAGjC,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"action-registry.d.ts","sourceRoot":"","sources":["../src/action-registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,WAAW,EAEX,kBAAkB,EAClB,WAAW,EACX,UAAU,EACV,sBAAsB,EACvB,MAAM,yBAAyB,CAAC;AAGjC,OAAO,KAAK,EACV,yBAAyB,EACzB,yBAAyB,EACzB,0BAA0B,EAC1B,WAAW,EACZ,MAAM,mBAAmB,CAAC;AAE3B,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,QAAQ,CAAC,IAAI,4BAA4B;gBAE7B,EAAE,EAAE,MAAM;CAIvB;AAED,mFAAmF;AACnF,qBAAa,+BAAgC,SAAQ,KAAK;IACxD,QAAQ,CAAC,IAAI,mCAAmC;;CAMjD;AAcD,qBAAa,cAAc;IACzB,OAAO,CAAC,KAAK,CAAc;IAC3B,OAAO,CAAC,UAAU,CAAkC;IAKpD,OAAO,CAAC,GAAG,CAGP;IAIJ,OAAO,CAAC,gBAAgB,CAA6C;IAErE,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAS;gBAE1B,IAAI,EAAE;QAAE,KAAK,EAAE,WAAW,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE;IAK9D,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,EACvB,YAAY,CAAC,EAAE,yBAAyB,GACvC,OAAO,CAAC,WAAW,EAAE,CAAC;IAqBnB,cAAc,CAClB,EAAE,EAAE,UAAU,EACd,eAAe,EAAE,MAAM,EACvB,YAAY,CAAC,EAAE,yBAAyB,GACvC,OAAO,CAAC;QACT,IAAI,EAAE,WAAW,EAAE,CAAC;QACpB,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;YA4BY,IAAI;IAsDlB;;;;;OAKG;IACG,QAAQ,CAAC,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,kBAAkB,GAAG,OAAO,CAAC,OAAO,CAAC;IA2BrE,0EAA0E;IACpE,eAAe,CACnB,EAAE,EAAE,MAAM,EACV,QAAQ,EAAE,yBAAyB,GAClC,OAAO,CAAC,0BAA0B,CAAC;IAOtC,mEAAmE;IAC7D,iBAAiB,CACrB,EAAE,EAAE,MAAM,EACV,QAAQ,EAAE,yBAAyB,GAClC,OAAO,CAAC,0BAA0B,CAAC;CASvC"}
|
package/dist/action-registry.js
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
import { isBound, getBoundArgs, renderToIR } from "@copilotkit/channels-ui";
|
|
2
2
|
import { mintId } from "./mint-id.js";
|
|
3
3
|
export class ActionExpiredError extends Error {
|
|
4
|
+
code = "channel_action_expired";
|
|
4
5
|
constructor(id) {
|
|
5
6
|
super(`Action "${id}" has expired or is no longer available.`);
|
|
6
7
|
this.name = "ActionExpiredError";
|
|
7
8
|
}
|
|
8
9
|
}
|
|
10
|
+
/** A continuation capability was presented outside its trusted Channel binding. */
|
|
11
|
+
export class ActionContinuationMismatchError extends Error {
|
|
12
|
+
code = "channel_continuation_mismatch";
|
|
13
|
+
constructor() {
|
|
14
|
+
super("The Channel continuation does not match this interaction");
|
|
15
|
+
this.name = "ActionContinuationMismatchError";
|
|
16
|
+
}
|
|
17
|
+
}
|
|
9
18
|
const EVENT_PROPS = ["onClick", "onSelect", "onSubmit"];
|
|
10
19
|
function isComponentElement(ui) {
|
|
11
20
|
return (typeof ui === "object" &&
|
|
@@ -24,8 +33,10 @@ export class ActionRegistry {
|
|
|
24
33
|
// posted message's id. Mirrors the `hot` action cache; the durable snapshot
|
|
25
34
|
// (below) is the cross-restart counterpart, exactly like onClick.
|
|
26
35
|
messageReactions = new Map();
|
|
36
|
+
retentionMs;
|
|
27
37
|
constructor(opts) {
|
|
28
38
|
this.store = opts.store;
|
|
39
|
+
this.retentionMs = opts.retentionMs;
|
|
29
40
|
}
|
|
30
41
|
/** Cache a `<Message onReaction>` handler for the posted message (same-process). */
|
|
31
42
|
registerMessageReaction(messageId, handler) {
|
|
@@ -72,10 +83,10 @@ export class ActionRegistry {
|
|
|
72
83
|
}
|
|
73
84
|
// Renders the named component, binds all event-prop handlers in the tree
|
|
74
85
|
// (mint id, hot-cache + ActionStore snapshot, rewrite prop to { id }), returns the bound IR.
|
|
75
|
-
async bindTree(componentName, props, conversationKey) {
|
|
86
|
+
async bindTree(componentName, props, conversationKey, continuation) {
|
|
76
87
|
const fn = this.components.get(componentName);
|
|
77
88
|
const root = renderToIR((fn ? fn(props) : props));
|
|
78
|
-
await this.walk(root, [], componentName, props, conversationKey);
|
|
89
|
+
await this.walk(root, [], componentName, props, conversationKey, continuation);
|
|
79
90
|
return root;
|
|
80
91
|
}
|
|
81
92
|
// Binds an arbitrary Renderable for posting. If `ui` is a component element
|
|
@@ -85,7 +96,7 @@ export class ActionRegistry {
|
|
|
85
96
|
// degradation for inline handlers that can't be re-derived). A top-level
|
|
86
97
|
// `<Message onReaction>` handler is pulled off the IR (so it never reaches the
|
|
87
98
|
// adapter) and returned for the caller to associate with the posted message.
|
|
88
|
-
async bindRenderable(ui, conversationKey) {
|
|
99
|
+
async bindRenderable(ui, conversationKey, continuation) {
|
|
89
100
|
let root;
|
|
90
101
|
let component;
|
|
91
102
|
let props;
|
|
@@ -94,11 +105,11 @@ export class ActionRegistry {
|
|
|
94
105
|
component = fn.name || "anonymous";
|
|
95
106
|
props = (ui.props ?? {});
|
|
96
107
|
this.registerComponent(component, fn);
|
|
97
|
-
root = await this.bindTree(component, props, conversationKey);
|
|
108
|
+
root = await this.bindTree(component, props, conversationKey, continuation);
|
|
98
109
|
}
|
|
99
110
|
else {
|
|
100
111
|
root = renderToIR(ui);
|
|
101
|
-
await this.walk(root, [], "", undefined, conversationKey);
|
|
112
|
+
await this.walk(root, [], "", undefined, conversationKey, continuation);
|
|
102
113
|
}
|
|
103
114
|
const onReaction = takeMessageReaction(root);
|
|
104
115
|
return {
|
|
@@ -107,7 +118,7 @@ export class ActionRegistry {
|
|
|
107
118
|
reactionComponent: onReaction && component && props ? { component, props } : undefined,
|
|
108
119
|
};
|
|
109
120
|
}
|
|
110
|
-
async walk(nodes, base, comp, props, conv) {
|
|
121
|
+
async walk(nodes, base, comp, props, conv, continuation) {
|
|
111
122
|
for (let i = 0; i < nodes.length; i++) {
|
|
112
123
|
const node = nodes[i];
|
|
113
124
|
const path = [...base, i];
|
|
@@ -115,10 +126,13 @@ export class ActionRegistry {
|
|
|
115
126
|
const handler = node.props[ep];
|
|
116
127
|
if (typeof handler === "function") {
|
|
117
128
|
const fullPath = [...path, ep];
|
|
118
|
-
const id =
|
|
129
|
+
const id = continuation
|
|
130
|
+
? `ck:${globalThis.crypto.randomUUID()}`
|
|
131
|
+
: mintId(comp, fullPath, props);
|
|
119
132
|
this.hot.set(id, {
|
|
120
133
|
handler: handler,
|
|
121
134
|
value: node.props.value,
|
|
135
|
+
...(continuation ? { continuation: true } : {}),
|
|
122
136
|
});
|
|
123
137
|
await this.store.put(id, {
|
|
124
138
|
component: comp,
|
|
@@ -126,13 +140,16 @@ export class ActionRegistry {
|
|
|
126
140
|
path: fullPath,
|
|
127
141
|
conversationKey: conv,
|
|
128
142
|
boundArgs: isBound(handler) ? getBoundArgs(handler) : undefined,
|
|
129
|
-
|
|
143
|
+
...(continuation
|
|
144
|
+
? { continuation: { ...continuation, actionId: id } }
|
|
145
|
+
: {}),
|
|
146
|
+
}, continuation ? this.retentionMs : undefined);
|
|
130
147
|
node.props[ep] = { id };
|
|
131
148
|
}
|
|
132
149
|
}
|
|
133
150
|
const children = node.props.children;
|
|
134
151
|
if (Array.isArray(children)) {
|
|
135
|
-
await this.walk(children, [...path, "children"], comp, props, conv);
|
|
152
|
+
await this.walk(children, [...path, "children"], comp, props, conv, continuation);
|
|
136
153
|
}
|
|
137
154
|
}
|
|
138
155
|
}
|
|
@@ -147,6 +164,10 @@ export class ActionRegistry {
|
|
|
147
164
|
let value;
|
|
148
165
|
const hot = this.hot.get(id);
|
|
149
166
|
if (hot) {
|
|
167
|
+
if (hot.continuation && !(await this.store.get(id))) {
|
|
168
|
+
this.hot.delete(id);
|
|
169
|
+
throw new ActionExpiredError(id);
|
|
170
|
+
}
|
|
150
171
|
handler = hot.handler;
|
|
151
172
|
value = hot.value;
|
|
152
173
|
}
|
|
@@ -166,6 +187,37 @@ export class ActionRegistry {
|
|
|
166
187
|
await handler({ ...ctx, action: { ...ctx.action, id } });
|
|
167
188
|
return value;
|
|
168
189
|
}
|
|
190
|
+
/** Read and validate one continuation capability without consuming it. */
|
|
191
|
+
async getContinuation(id, expected) {
|
|
192
|
+
const available = await this.store.get(id);
|
|
193
|
+
if (!available?.continuation)
|
|
194
|
+
throw new ActionExpiredError(id);
|
|
195
|
+
assertContinuationBinding(available.continuation, id, expected);
|
|
196
|
+
return available.continuation;
|
|
197
|
+
}
|
|
198
|
+
/** Validate and atomically consume one continuation capability. */
|
|
199
|
+
async claimContinuation(id, expected) {
|
|
200
|
+
await this.getContinuation(id, expected);
|
|
201
|
+
const claimed = await this.store.consume(id);
|
|
202
|
+
if (!claimed?.continuation)
|
|
203
|
+
throw new ActionExpiredError(id);
|
|
204
|
+
assertContinuationBinding(claimed.continuation, id, expected);
|
|
205
|
+
this.hot.delete(id);
|
|
206
|
+
return claimed.continuation;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
function assertContinuationBinding(actual, actionId, expected) {
|
|
210
|
+
if (actual.actionId !== actionId ||
|
|
211
|
+
actual.channelName !== expected.channelName ||
|
|
212
|
+
actual.conversationKey !== expected.conversationKey ||
|
|
213
|
+
actual.threadId !== expected.threadId ||
|
|
214
|
+
typeof actual.runChainId !== "string" ||
|
|
215
|
+
actual.runChainId.length === 0 ||
|
|
216
|
+
typeof actual.initiator?.actor?.id !== "string" ||
|
|
217
|
+
(actual.initiator.user !== null &&
|
|
218
|
+
typeof actual.initiator.user?.id !== "string")) {
|
|
219
|
+
throw new ActionContinuationMismatchError();
|
|
220
|
+
}
|
|
169
221
|
}
|
|
170
222
|
/** Store key for a message's durable reaction snapshot (distinct from minted action ids). */
|
|
171
223
|
function reactionKey(messageId) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
-
import { ActionRegistry, ActionExpiredError } from "./action-registry.js";
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
2
|
+
import { ActionRegistry, ActionContinuationMismatchError, ActionExpiredError, } from "./action-registry.js";
|
|
3
3
|
import { InMemoryActionStore } from "./action-store.js";
|
|
4
4
|
import { MemoryStore } from "./state/memory-store.js";
|
|
5
5
|
import { kvActionStore } from "./state/kv-action-store.js";
|
|
@@ -112,4 +112,109 @@ describe("ActionRegistry", () => {
|
|
|
112
112
|
await expect(regBPrime.dispatch(id, ctx)).rejects.toBeInstanceOf(ActionExpiredError);
|
|
113
113
|
});
|
|
114
114
|
});
|
|
115
|
+
describe("one-use HITL continuations", () => {
|
|
116
|
+
const continuation = {
|
|
117
|
+
channelName: "approvals",
|
|
118
|
+
conversationKey: "conv1",
|
|
119
|
+
threadId: "thread1",
|
|
120
|
+
runChainId: "run-chain-1",
|
|
121
|
+
initiator: {
|
|
122
|
+
user: { id: "user-1", name: "Alice" },
|
|
123
|
+
actor: { id: "actor-1", kind: "human" },
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
it("mints random capabilities and stores the trusted binding in the action snapshot", async () => {
|
|
127
|
+
const store = new InMemoryActionStore();
|
|
128
|
+
const reg = new ActionRegistry({ store, retentionMs: 60_000 });
|
|
129
|
+
reg.registerComponent("Confirm", Confirm);
|
|
130
|
+
const first = await reg.bindTree("Confirm", { action: "approve" }, "conv1", continuation);
|
|
131
|
+
const second = await reg.bindTree("Confirm", { action: "approve" }, "conv1", continuation);
|
|
132
|
+
const firstId = first[0].props.children[0].props.onClick.id;
|
|
133
|
+
const secondId = second[0].props.children[0].props.onClick.id;
|
|
134
|
+
expect(firstId).not.toBe(secondId);
|
|
135
|
+
expect(await store.get(firstId)).toMatchObject({
|
|
136
|
+
continuation: { ...continuation, actionId: firstId },
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
it("allows exactly one claimant across concurrent resume attempts", async () => {
|
|
140
|
+
const reg = new ActionRegistry({
|
|
141
|
+
store: new InMemoryActionStore(),
|
|
142
|
+
retentionMs: 60_000,
|
|
143
|
+
});
|
|
144
|
+
reg.registerComponent("Confirm", Confirm);
|
|
145
|
+
const tree = await reg.bindTree("Confirm", { action: "approve" }, "conv1", continuation);
|
|
146
|
+
const id = tree[0].props.children[0].props.onClick.id;
|
|
147
|
+
const outcomes = await Promise.allSettled([
|
|
148
|
+
reg.claimContinuation(id, continuation),
|
|
149
|
+
reg.claimContinuation(id, continuation),
|
|
150
|
+
]);
|
|
151
|
+
expect(outcomes.filter((outcome) => outcome.status === "fulfilled")).toHaveLength(1);
|
|
152
|
+
expect(outcomes.filter((outcome) => outcome.status === "rejected")).toHaveLength(1);
|
|
153
|
+
expect(outcomes.find((outcome) => outcome.status === "rejected").reason).toBeInstanceOf(ActionExpiredError);
|
|
154
|
+
});
|
|
155
|
+
it.each([
|
|
156
|
+
["Channel", { channelName: "wrong" }],
|
|
157
|
+
["conversation", { conversationKey: "wrong" }],
|
|
158
|
+
["Thread", { threadId: "wrong" }],
|
|
159
|
+
])("rejects a wrong %s binding without consuming the valid continuation", async (_label, tampered) => {
|
|
160
|
+
const reg = new ActionRegistry({
|
|
161
|
+
store: new InMemoryActionStore(),
|
|
162
|
+
retentionMs: 60_000,
|
|
163
|
+
});
|
|
164
|
+
reg.registerComponent("Confirm", Confirm);
|
|
165
|
+
const tree = await reg.bindTree("Confirm", { action: "approve" }, "conv1", continuation);
|
|
166
|
+
const id = tree[0].props.children[0].props.onClick.id;
|
|
167
|
+
await expect(reg.claimContinuation(id, { ...continuation, ...tampered })).rejects.toBeInstanceOf(ActionContinuationMismatchError);
|
|
168
|
+
await expect(reg.claimContinuation(id, continuation)).resolves.toMatchObject({
|
|
169
|
+
actionId: id,
|
|
170
|
+
runChainId: "run-chain-1",
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
it.each([
|
|
174
|
+
["run chain", { runChainId: "" }],
|
|
175
|
+
["action", { actionId: "ck:other" }],
|
|
176
|
+
[
|
|
177
|
+
"initiator actor",
|
|
178
|
+
{ initiator: { ...continuation.initiator, actor: {} } },
|
|
179
|
+
],
|
|
180
|
+
])("rejects a snapshot with a tampered %s", async (_label, tampered) => {
|
|
181
|
+
const id = `ck:${globalThis.crypto.randomUUID()}`;
|
|
182
|
+
const store = new InMemoryActionStore();
|
|
183
|
+
const reg = new ActionRegistry({ store, retentionMs: 60_000 });
|
|
184
|
+
await store.put(id, {
|
|
185
|
+
path: [0, "onClick"],
|
|
186
|
+
conversationKey: "conv1",
|
|
187
|
+
continuation: {
|
|
188
|
+
...continuation,
|
|
189
|
+
actionId: id,
|
|
190
|
+
...tampered,
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
await expect(reg.claimContinuation(id, continuation)).rejects.toBeInstanceOf(ActionContinuationMismatchError);
|
|
194
|
+
});
|
|
195
|
+
it("rejects a wrong random capability", async () => {
|
|
196
|
+
const reg = new ActionRegistry({
|
|
197
|
+
store: new InMemoryActionStore(),
|
|
198
|
+
retentionMs: 60_000,
|
|
199
|
+
});
|
|
200
|
+
await expect(reg.claimContinuation("ck:wrong-capability", continuation)).rejects.toBeInstanceOf(ActionExpiredError);
|
|
201
|
+
});
|
|
202
|
+
it("expires with the action retention window", async () => {
|
|
203
|
+
vi.useFakeTimers();
|
|
204
|
+
try {
|
|
205
|
+
const reg = new ActionRegistry({
|
|
206
|
+
store: new InMemoryActionStore(),
|
|
207
|
+
retentionMs: 100,
|
|
208
|
+
});
|
|
209
|
+
reg.registerComponent("Confirm", Confirm);
|
|
210
|
+
const tree = await reg.bindTree("Confirm", { action: "approve" }, "conv1", continuation);
|
|
211
|
+
const id = tree[0].props.children[0].props.onClick.id;
|
|
212
|
+
vi.advanceTimersByTime(101);
|
|
213
|
+
await expect(reg.claimContinuation(id, continuation)).rejects.toBeInstanceOf(ActionExpiredError);
|
|
214
|
+
}
|
|
215
|
+
finally {
|
|
216
|
+
vi.useRealTimers();
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
});
|
|
115
220
|
});
|
package/dist/action-store.d.ts
CHANGED
|
@@ -1,14 +1,36 @@
|
|
|
1
|
+
import type { ApplicationUser, ProviderActor } from "@copilotkit/channels-ui";
|
|
2
|
+
/** Trusted principal captured when one HITL run chain starts. */
|
|
3
|
+
export interface ActionContinuationInitiator {
|
|
4
|
+
user: ApplicationUser | null;
|
|
5
|
+
actor: ProviderActor;
|
|
6
|
+
}
|
|
7
|
+
/** Trusted facts bound to every one-use action rendered by a HITL run chain. */
|
|
8
|
+
export interface ActionContinuationContext {
|
|
9
|
+
channelName: string;
|
|
10
|
+
conversationKey: string;
|
|
11
|
+
threadId: string;
|
|
12
|
+
runChainId: string;
|
|
13
|
+
initiator: ActionContinuationInitiator;
|
|
14
|
+
}
|
|
15
|
+
/** Server-derived delivery facts used to validate a presented continuation. */
|
|
16
|
+
export type ActionContinuationBinding = Pick<ActionContinuationContext, "channelName" | "conversationKey" | "threadId">;
|
|
17
|
+
/** Persisted continuation binding, including its random action capability. */
|
|
18
|
+
export interface ActionContinuationSnapshot extends ActionContinuationContext {
|
|
19
|
+
actionId: string;
|
|
20
|
+
}
|
|
1
21
|
export interface ActionSnapshot {
|
|
2
22
|
component?: string;
|
|
3
23
|
props?: unknown;
|
|
4
24
|
path: (string | number)[];
|
|
5
25
|
boundArgs?: unknown;
|
|
6
26
|
conversationKey: string;
|
|
27
|
+
continuation?: ActionContinuationSnapshot;
|
|
7
28
|
}
|
|
8
29
|
/** @deprecated Configure `createChannel({ state })` instead. Action snapshots are stored via `StateStore.kv`. */
|
|
9
30
|
export interface ActionStore {
|
|
10
31
|
put(id: string, snap: ActionSnapshot, ttlMs?: number): Promise<void>;
|
|
11
32
|
get(id: string): Promise<ActionSnapshot | undefined>;
|
|
33
|
+
consume(id: string): Promise<ActionSnapshot | undefined>;
|
|
12
34
|
delete(id: string): Promise<void>;
|
|
13
35
|
}
|
|
14
36
|
/** @deprecated Configure `createChannel({ state })` instead. Action snapshots are stored via `StateStore.kv`. */
|
|
@@ -17,5 +39,6 @@ export declare class InMemoryActionStore implements ActionStore {
|
|
|
17
39
|
put(id: string, snap: ActionSnapshot, ttlMs?: number): Promise<void>;
|
|
18
40
|
get(id: string): Promise<ActionSnapshot | undefined>;
|
|
19
41
|
delete(id: string): Promise<void>;
|
|
42
|
+
consume(id: string): Promise<ActionSnapshot | undefined>;
|
|
20
43
|
}
|
|
21
44
|
//# sourceMappingURL=action-store.d.ts.map
|
|
@@ -1 +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;
|
|
1
|
+
{"version":3,"file":"action-store.d.ts","sourceRoot":"","sources":["../src/action-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAE9E,iEAAiE;AACjE,MAAM,WAAW,2BAA2B;IAC1C,IAAI,EAAE,eAAe,GAAG,IAAI,CAAC;IAC7B,KAAK,EAAE,aAAa,CAAC;CACtB;AAED,gFAAgF;AAChF,MAAM,WAAW,yBAAyB;IACxC,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,2BAA2B,CAAC;CACxC;AAED,+EAA+E;AAC/E,MAAM,MAAM,yBAAyB,GAAG,IAAI,CAC1C,yBAAyB,EACzB,aAAa,GAAG,iBAAiB,GAAG,UAAU,CAC/C,CAAC;AAEF,8EAA8E;AAC9E,MAAM,WAAW,0BAA2B,SAAQ,yBAAyB;IAC3E,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,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;IACxB,YAAY,CAAC,EAAE,0BAA0B,CAAC;CAC3C;AACD,iHAAiH;AACjH,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,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC,CAAC;IACzD,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACnC;AACD,iHAAiH;AACjH,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;IAGjC,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC;CAU/D"}
|
package/dist/action-store.js
CHANGED
|
@@ -20,4 +20,15 @@ export class InMemoryActionStore {
|
|
|
20
20
|
async delete(id) {
|
|
21
21
|
this.map.delete(id);
|
|
22
22
|
}
|
|
23
|
+
async consume(id) {
|
|
24
|
+
const entry = this.map.get(id);
|
|
25
|
+
if (!entry)
|
|
26
|
+
return undefined;
|
|
27
|
+
if (entry.expiresAt !== undefined && Date.now() > entry.expiresAt) {
|
|
28
|
+
this.map.delete(id);
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
this.map.delete(id);
|
|
32
|
+
return entry.snap;
|
|
33
|
+
}
|
|
23
34
|
}
|
|
@@ -334,7 +334,12 @@ test("terminal delivery tool failure stops the loop and closes renderer fanout",
|
|
|
334
334
|
tools: new Map([["echo", postFile]]),
|
|
335
335
|
toolDescriptors: [],
|
|
336
336
|
context: [],
|
|
337
|
-
makeToolCtx: () => ({
|
|
337
|
+
makeToolCtx: () => ({
|
|
338
|
+
thread: {},
|
|
339
|
+
user: null,
|
|
340
|
+
actor: { id: "actor", kind: "unknown" },
|
|
341
|
+
platform: "fake",
|
|
342
|
+
}),
|
|
338
343
|
subscriber: {
|
|
339
344
|
onEvent: ({ event }) => {
|
|
340
345
|
ingestedEvents.push(event);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"channel-identity.test.d.ts","sourceRoot":"","sources":["../src/channel-identity.test.ts"],"names":[],"mappings":""}
|