@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.
Files changed (82) hide show
  1. package/README.md +35 -14
  2. package/dist/action-registry.d.ts +15 -3
  3. package/dist/action-registry.d.ts.map +1 -1
  4. package/dist/action-registry.js +61 -9
  5. package/dist/action-registry.test.js +107 -2
  6. package/dist/action-store.d.ts +23 -0
  7. package/dist/action-store.d.ts.map +1 -1
  8. package/dist/action-store.js +11 -0
  9. package/dist/canonical-run-loop.test.js +6 -1
  10. package/dist/channel-identity.test.d.ts +2 -0
  11. package/dist/channel-identity.test.d.ts.map +1 -0
  12. package/dist/channel-identity.test.js +297 -0
  13. package/dist/channel-memory.test.d.ts +2 -0
  14. package/dist/channel-memory.test.d.ts.map +1 -0
  15. package/dist/channel-memory.test.js +199 -0
  16. package/dist/commands.d.ts +5 -3
  17. package/dist/commands.d.ts.map +1 -1
  18. package/dist/create-channel.d.ts +30 -56
  19. package/dist/create-channel.d.ts.map +1 -1
  20. package/dist/create-channel.foundations.test.js +30 -7
  21. package/dist/create-channel.js +209 -72
  22. package/dist/create-channel.test.js +293 -66
  23. package/dist/hitl-continuation.test.d.ts +2 -0
  24. package/dist/hitl-continuation.test.d.ts.map +1 -0
  25. package/dist/hitl-continuation.test.js +393 -0
  26. package/dist/identity.d.ts +51 -0
  27. package/dist/identity.d.ts.map +1 -0
  28. package/dist/identity.js +56 -0
  29. package/dist/identity.test.d.ts +2 -0
  30. package/dist/identity.test.d.ts.map +1 -0
  31. package/dist/identity.test.js +109 -0
  32. package/dist/index.d.ts +10 -5
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +5 -1
  35. package/dist/managed-v1-await-choice-guard.test.js +4 -1
  36. package/dist/memory.d.ts +23 -0
  37. package/dist/memory.d.ts.map +1 -0
  38. package/dist/memory.js +25 -0
  39. package/dist/modal-submit.test.js +21 -6
  40. package/dist/open-modal.test.js +16 -4
  41. package/dist/platform-adapter.d.ts +29 -6
  42. package/dist/platform-adapter.d.ts.map +1 -1
  43. package/dist/platform-adapter.test.js +2 -0
  44. package/dist/reactions.test.js +49 -11
  45. package/dist/run-loop.test.js +30 -5
  46. package/dist/sanitize-agent-events.test.js +2 -0
  47. package/dist/source-platform.test.js +24 -9
  48. package/dist/state/kv-action-store.d.ts.map +1 -1
  49. package/dist/state/kv-action-store.js +1 -0
  50. package/dist/state/kv-action-store.test.js +11 -0
  51. package/dist/state/memory-store.d.ts +1 -0
  52. package/dist/state/memory-store.d.ts.map +1 -1
  53. package/dist/state/memory-store.js +9 -0
  54. package/dist/state/state-store.d.ts +2 -0
  55. package/dist/state/state-store.d.ts.map +1 -1
  56. package/dist/telemetry/create-channel-telemetry.test.js +4 -2
  57. package/dist/telemetry/e2e-telemetry.test.js +1 -0
  58. package/dist/telemetry/install-id.test.js +5 -0
  59. package/dist/testing/fake-adapter.d.ts +26 -9
  60. package/dist/testing/fake-adapter.d.ts.map +1 -1
  61. package/dist/testing/fake-adapter.js +65 -21
  62. package/dist/testing/state-store-conformance.d.ts.map +1 -1
  63. package/dist/testing/state-store-conformance.js +16 -0
  64. package/dist/thread-capabilities.test.js +29 -9
  65. package/dist/thread-ephemeral.test.js +8 -5
  66. package/dist/thread-promise-contract.test.js +4 -0
  67. package/dist/thread-reactions.test.js +8 -2
  68. package/dist/thread.d.ts +50 -7
  69. package/dist/thread.d.ts.map +1 -1
  70. package/dist/thread.js +163 -17
  71. package/dist/thread.test.js +4 -0
  72. package/dist/tools.d.ts +5 -3
  73. package/dist/tools.d.ts.map +1 -1
  74. package/dist/tools.js +2 -1
  75. package/dist/transcripts.d.ts +6 -13
  76. package/dist/transcripts.d.ts.map +1 -1
  77. package/dist/transcripts.js +12 -12
  78. package/dist/transcripts.test.js +28 -29
  79. package/dist/welcome.test.d.ts +2 -0
  80. package/dist/welcome.test.d.ts.map +1 -0
  81. package/dist/welcome.test.js +53 -0
  82. package/package.json +5 -5
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=hitl-continuation.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hitl-continuation.test.d.ts","sourceRoot":"","sources":["../src/hitl-continuation.test.tsx"],"names":[],"mappings":""}
@@ -0,0 +1,393 @@
1
+ import { jsx as _jsx } from "@copilotkit/channels-core/jsx-runtime";
2
+ import { Button } from "@copilotkit/channels-ui";
3
+ import { afterEach, expect, test, vi } from "vitest";
4
+ import { createChannel } from "./create-channel.js";
5
+ import { MemoryStore } from "./state/memory-store.js";
6
+ import { FakeAdapter } from "./testing/fake-adapter.js";
7
+ import { FakeAgent } from "./testing/fake-agent.js";
8
+ const activeChannels = [];
9
+ afterEach(async () => {
10
+ await Promise.all(activeChannels.splice(0).map((channel) => channel.stop()));
11
+ });
12
+ function firstActionId(adapter) {
13
+ const button = adapter.posted.flat().find((node) => node.type === "button");
14
+ const action = button?.props.onClick;
15
+ if (typeof action?.id !== "string")
16
+ throw new Error("missing action id");
17
+ return action.id;
18
+ }
19
+ function actionIds(adapter) {
20
+ return adapter.posted
21
+ .flat()
22
+ .filter((node) => node.type === "button")
23
+ .map((node) => node.props.onClick?.id)
24
+ .filter((id) => typeof id === "string");
25
+ }
26
+ function interrupt(subscriber) {
27
+ subscriber.onCustomEvent?.({
28
+ event: { name: "approval", value: { requestId: "request-1" } },
29
+ });
30
+ subscriber.onRunFinishedEvent?.({ event: {} });
31
+ }
32
+ async function setup(subject, memory) {
33
+ const adapter = new FakeAdapter();
34
+ adapter.supportsIntelligenceMemory = true;
35
+ const lifecycleCalls = [];
36
+ adapter.runAgentLifecycle = vi.fn(async (args) => {
37
+ lifecycleCalls.push(args);
38
+ return args.execute(args.renderer.subscriber, undefined);
39
+ });
40
+ const state = new MemoryStore();
41
+ const agent = new FakeAgent([interrupt]);
42
+ function Approval() {
43
+ return (_jsx(Button, { value: { approved: true }, onClick: async ({ thread }) => {
44
+ await thread.resume({ approved: true }, { memory, subject });
45
+ }, children: "Approve" }));
46
+ }
47
+ const channel = createChannel({
48
+ name: "approvals",
49
+ identifyUser: ({ actor }) => actor.kind === "human"
50
+ ? { id: `app:${actor.id}`, name: actor.name ?? actor.id }
51
+ : null,
52
+ adapters: [adapter],
53
+ agent,
54
+ components: [Approval],
55
+ store: { adapter: state, actionRetentionMs: 60_000 },
56
+ });
57
+ channel.onMessage(async ({ thread }) => {
58
+ await thread.runAgent();
59
+ });
60
+ channel.onInterrupt("approval", async ({ thread }) => {
61
+ await thread.post(_jsx(Approval, {}));
62
+ });
63
+ await channel.ɵruntime.start();
64
+ activeChannels.push({ stop: () => channel.ɵruntime.stop() });
65
+ await adapter.getSink().onTurn({
66
+ conversationKey: "thread-1",
67
+ replyTarget: {},
68
+ userText: "start",
69
+ platform: "slack",
70
+ actor: { id: "alice", kind: "human", name: "Alice" },
71
+ });
72
+ return { adapter, lifecycleCalls, state, actionId: firstActionId(adapter) };
73
+ }
74
+ test("resume with initiator Memory keeps the original user when Bob clicks", async () => {
75
+ const { adapter, lifecycleCalls, actionId } = await setup("initiator", {
76
+ user: "read",
77
+ });
78
+ await adapter.getSink().onInteraction({
79
+ id: actionId,
80
+ conversationKey: "thread-1",
81
+ replyTarget: {},
82
+ eventId: "bob-click",
83
+ actor: { id: "bob", kind: "human", name: "Bob" },
84
+ });
85
+ expect(lifecycleCalls).toHaveLength(2);
86
+ expect(lifecycleCalls[1].memory).toEqual({
87
+ grant: { user: "read", project: "none" },
88
+ user: { id: "app:alice", name: "Alice" },
89
+ });
90
+ });
91
+ test("a managed continuation keeps its initiator across a Runtime restart", async () => {
92
+ const state = new MemoryStore();
93
+ const lifecycleCalls = [];
94
+ function Approval() {
95
+ return (_jsx(Button, { value: { approved: true }, onClick: async ({ thread }) => {
96
+ await thread.resume({ approved: true }, { memory: { user: "read" }, subject: "initiator" });
97
+ }, children: "Approve" }));
98
+ }
99
+ const makeRuntime = (agent) => {
100
+ const adapter = new FakeAdapter();
101
+ adapter.supportsIntelligenceMemory = true;
102
+ adapter.runAgentLifecycle = vi.fn(async (args) => {
103
+ lifecycleCalls.push(args);
104
+ return args.execute(args.renderer.subscriber, undefined);
105
+ });
106
+ const channel = createChannel({
107
+ name: "approvals",
108
+ identifyUser: ({ actor }) => actor.kind === "human"
109
+ ? { id: `app:${actor.id}`, name: actor.name ?? actor.id }
110
+ : null,
111
+ adapters: [adapter],
112
+ agent,
113
+ components: [Approval],
114
+ store: { adapter: state, actionRetentionMs: 60_000 },
115
+ });
116
+ channel.onMessage(async ({ thread }) => {
117
+ await thread.runAgent();
118
+ });
119
+ channel.onInterrupt("approval", async ({ thread }) => {
120
+ await thread.post(_jsx(Approval, {}));
121
+ });
122
+ return { adapter, channel };
123
+ };
124
+ const first = makeRuntime(new FakeAgent([interrupt]));
125
+ await first.channel.ɵruntime.start();
126
+ await first.adapter.getSink().onTurn({
127
+ conversationKey: "thread-1",
128
+ replyTarget: {},
129
+ userText: "start",
130
+ platform: "slack",
131
+ actor: { id: "alice", kind: "human", name: "Alice" },
132
+ });
133
+ const actionId = firstActionId(first.adapter);
134
+ await first.channel.ɵruntime.stop();
135
+ const restarted = makeRuntime(new FakeAgent());
136
+ await restarted.channel.ɵruntime.start();
137
+ activeChannels.push({ stop: () => restarted.channel.ɵruntime.stop() });
138
+ await restarted.adapter.getSink().onInteraction({
139
+ id: actionId,
140
+ conversationKey: "thread-1",
141
+ replyTarget: {},
142
+ eventId: "bob-click-after-restart",
143
+ actor: { id: "bob", kind: "human", name: "Bob" },
144
+ });
145
+ expect(lifecycleCalls).toHaveLength(2);
146
+ expect(lifecycleCalls[1].memory).toEqual({
147
+ grant: { user: "read", project: "none" },
148
+ user: { id: "app:alice", name: "Alice" },
149
+ });
150
+ expect(lifecycleCalls[1].isResume).toBe(true);
151
+ });
152
+ test("independent runs on one Thread snapshot new initiators", async () => {
153
+ const state = new MemoryStore();
154
+ const adapter = new FakeAdapter();
155
+ function Approval() {
156
+ return (_jsx(Button, { value: { approved: true }, onClick: () => undefined, children: "Approve" }));
157
+ }
158
+ const channel = createChannel({
159
+ name: "approvals",
160
+ identifyUser: ({ actor }) => actor.kind === "human"
161
+ ? { id: `app:${actor.id}`, name: actor.name ?? actor.id }
162
+ : null,
163
+ adapters: [adapter],
164
+ agent: new FakeAgent([interrupt]),
165
+ components: [Approval],
166
+ store: { adapter: state, actionRetentionMs: 60_000 },
167
+ });
168
+ channel.onMessage(async ({ thread }) => {
169
+ await thread.runAgent();
170
+ });
171
+ channel.onInterrupt("approval", async ({ thread }) => {
172
+ await thread.post(_jsx(Approval, {}));
173
+ });
174
+ await channel.ɵruntime.start();
175
+ activeChannels.push({ stop: () => channel.ɵruntime.stop() });
176
+ for (const actor of [
177
+ { id: "alice", kind: "human", name: "Alice" },
178
+ { id: "bob", kind: "human", name: "Bob" },
179
+ ]) {
180
+ await adapter.getSink().onTurn({
181
+ conversationKey: "thread-1",
182
+ replyTarget: {},
183
+ userText: "start",
184
+ platform: "slack",
185
+ actor,
186
+ });
187
+ }
188
+ const snapshots = await Promise.all(actionIds(adapter).map((id) => state.kv.get(`action:${id}`)));
189
+ expect(snapshots).toHaveLength(2);
190
+ expect(snapshots.map((snapshot) => snapshot?.continuation?.initiator.user?.id)).toEqual(["app:alice", "app:bob"]);
191
+ expect(new Set(snapshots.map((snapshot) => snapshot?.continuation?.runChainId))
192
+ .size).toBe(2);
193
+ });
194
+ test("concurrent independent runs cannot share a continuation chain", async () => {
195
+ const state = new MemoryStore();
196
+ const adapter = new FakeAdapter();
197
+ function Approval() {
198
+ return (_jsx(Button, { value: { approved: true }, onClick: () => undefined, children: "Approve" }));
199
+ }
200
+ const channel = createChannel({
201
+ name: "approvals",
202
+ identifyUser: "platform",
203
+ adapters: [adapter],
204
+ agent: () => new FakeAgent([interrupt]),
205
+ components: [Approval],
206
+ store: { adapter: state, actionRetentionMs: 60_000 },
207
+ });
208
+ let receivedThread;
209
+ channel.onMessage(({ thread }) => {
210
+ receivedThread = thread;
211
+ });
212
+ channel.onInterrupt("approval", async ({ thread }) => {
213
+ await thread.post(_jsx(Approval, {}));
214
+ });
215
+ await channel.ɵruntime.start();
216
+ activeChannels.push({ stop: () => channel.ɵruntime.stop() });
217
+ await adapter.getSink().onTurn({
218
+ conversationKey: "thread-concurrent",
219
+ replyTarget: {},
220
+ userText: "start",
221
+ platform: "slack",
222
+ actor: { id: "alice", kind: "human", name: "Alice" },
223
+ identityContext: {
224
+ tenant: { id: "T1" },
225
+ installation: { id: "I1" },
226
+ conversation: { id: "C1" },
227
+ trigger: "message",
228
+ event: { id: "E1" },
229
+ raw: {},
230
+ },
231
+ });
232
+ if (!receivedThread)
233
+ throw new Error("message handler did not receive a Thread");
234
+ await Promise.all([receivedThread.runAgent(), receivedThread.runAgent()]);
235
+ const snapshots = await Promise.all(actionIds(adapter).map((id) => state.kv.get(`action:${id}`)));
236
+ expect(snapshots).toHaveLength(2);
237
+ expect(new Set(snapshots.map((snapshot) => snapshot?.continuation?.runChainId))
238
+ .size).toBe(2);
239
+ });
240
+ test("resume with actor Memory selects the current interaction user", async () => {
241
+ const { adapter, lifecycleCalls, actionId } = await setup("actor", {
242
+ user: "read-write",
243
+ });
244
+ await adapter.getSink().onInteraction({
245
+ id: actionId,
246
+ conversationKey: "thread-1",
247
+ replyTarget: {},
248
+ eventId: "bob-click",
249
+ actor: { id: "bob", kind: "human", name: "Bob" },
250
+ });
251
+ expect(lifecycleCalls[1].memory?.user).toEqual({
252
+ id: "app:bob",
253
+ name: "Bob",
254
+ });
255
+ });
256
+ test("project-only resume needs no personal subject or fabricated user", async () => {
257
+ const { adapter, lifecycleCalls, actionId } = await setup(undefined, {
258
+ project: "read",
259
+ });
260
+ await adapter.getSink().onInteraction({
261
+ id: actionId,
262
+ conversationKey: "thread-1",
263
+ replyTarget: {},
264
+ eventId: "system-click",
265
+ actor: { id: "approval-bot", kind: "bot" },
266
+ });
267
+ expect(lifecycleCalls[1].memory).toEqual({
268
+ grant: { user: "none", project: "read" },
269
+ user: null,
270
+ });
271
+ });
272
+ test("personal resume requires an explicit trusted subject selector", async () => {
273
+ const { adapter, lifecycleCalls, actionId } = await setup(undefined, {
274
+ user: "read",
275
+ });
276
+ await expect(adapter.getSink().onInteraction({
277
+ id: actionId,
278
+ conversationKey: "thread-1",
279
+ replyTarget: {},
280
+ eventId: "bob-click",
281
+ actor: { id: "bob", kind: "human", name: "Bob" },
282
+ })).rejects.toMatchObject({ code: "channel_memory_subject_required" });
283
+ expect(lifecycleCalls).toHaveLength(1);
284
+ });
285
+ test("personal resume fails before execution when the selected actor has no application user", async () => {
286
+ const { adapter, lifecycleCalls, actionId } = await setup("actor", {
287
+ user: "read",
288
+ });
289
+ await expect(adapter.getSink().onInteraction({
290
+ id: actionId,
291
+ conversationKey: "thread-1",
292
+ replyTarget: {},
293
+ eventId: "system-click",
294
+ actor: { id: "approval-bot", kind: "bot" },
295
+ })).rejects.toMatchObject({ code: "channel_memory_user_required" });
296
+ expect(lifecycleCalls).toHaveLength(1);
297
+ });
298
+ test("a continuation starts only one resumed run", async () => {
299
+ const { adapter, lifecycleCalls, actionId } = await setup("actor", {
300
+ project: "read",
301
+ });
302
+ const click = (eventId) => adapter.getSink().onInteraction({
303
+ id: actionId,
304
+ conversationKey: "thread-1",
305
+ replyTarget: {},
306
+ eventId,
307
+ actor: { id: "bob", kind: "human", name: "Bob" },
308
+ });
309
+ await Promise.all([click("click-1"), click("click-2")]);
310
+ expect(lifecycleCalls).toHaveLength(2);
311
+ });
312
+ test("a wrong conversation cannot consume another conversation's continuation", async () => {
313
+ const { adapter, lifecycleCalls, actionId } = await setup("actor", {
314
+ project: "read",
315
+ });
316
+ await expect(adapter.getSink().onInteraction({
317
+ id: actionId,
318
+ conversationKey: "thread-wrong",
319
+ replyTarget: {},
320
+ eventId: "wrong-thread-click",
321
+ actor: { id: "bob", kind: "human", name: "Bob" },
322
+ })).rejects.toMatchObject({ code: "channel_continuation_mismatch" });
323
+ expect(lifecycleCalls).toHaveLength(1);
324
+ await adapter.getSink().onInteraction({
325
+ id: actionId,
326
+ conversationKey: "thread-1",
327
+ replyTarget: {},
328
+ eventId: "correct-thread-click",
329
+ actor: { id: "bob", kind: "human", name: "Bob" },
330
+ });
331
+ expect(lifecycleCalls).toHaveLength(2);
332
+ });
333
+ test("normal runs write no continuation state and one resume consumes once", async () => {
334
+ const state = new MemoryStore();
335
+ const set = vi.spyOn(state.kv, "set");
336
+ const consume = vi.spyOn(state.kv, "consume");
337
+ const adapter = new FakeAdapter();
338
+ adapter.supportsIntelligenceMemory = true;
339
+ function Approval() {
340
+ return (_jsx(Button, { value: { approved: true }, onClick: async ({ thread }) => {
341
+ await thread.resume({ approved: true }, { memory: { project: "read" } });
342
+ }, children: "Approve" }));
343
+ }
344
+ const finish = (subscriber) => {
345
+ subscriber.onRunFinishedEvent?.({ event: {} });
346
+ };
347
+ const agent = new FakeAgent([finish]);
348
+ const channel = createChannel({
349
+ name: "approvals",
350
+ identifyUser: ({ actor }) => actor.kind === "human"
351
+ ? { id: `app:${actor.id}`, name: actor.name ?? actor.id }
352
+ : null,
353
+ adapters: [adapter],
354
+ agent,
355
+ components: [Approval],
356
+ store: { adapter: state, actionRetentionMs: 60_000 },
357
+ });
358
+ channel.onMessage(async ({ thread, message }) => {
359
+ agent.setScript([message.text === "interrupt" ? interrupt : finish]);
360
+ await thread.runAgent();
361
+ });
362
+ channel.onInterrupt("approval", async ({ thread }) => {
363
+ await thread.post(_jsx(Approval, {}));
364
+ });
365
+ await channel.ɵruntime.start();
366
+ activeChannels.push({ stop: () => channel.ɵruntime.stop() });
367
+ const turn = (userText) => adapter.getSink().onTurn({
368
+ conversationKey: "thread-1",
369
+ replyTarget: {},
370
+ userText,
371
+ platform: "slack",
372
+ actor: { id: "alice", kind: "human", name: "Alice" },
373
+ });
374
+ await turn("normal");
375
+ expect(set).not.toHaveBeenCalled();
376
+ expect(consume).not.toHaveBeenCalled();
377
+ await turn("interrupt");
378
+ const actionId = firstActionId(adapter);
379
+ expect(set).toHaveBeenCalledTimes(1);
380
+ expect(set.mock.calls[0]?.[0]).toBe(`action:${actionId}`);
381
+ expect(consume).not.toHaveBeenCalled();
382
+ const click = (eventId) => adapter.getSink().onInteraction({
383
+ id: actionId,
384
+ conversationKey: "thread-1",
385
+ replyTarget: {},
386
+ eventId,
387
+ actor: { id: "bob", kind: "human", name: "Bob" },
388
+ });
389
+ await click("bob-click");
390
+ await click("bob-click-redelivery");
391
+ expect(consume).toHaveBeenCalledTimes(1);
392
+ expect(consume.mock.calls[0]?.[0]).toBe(`action:${actionId}`);
393
+ });
@@ -0,0 +1,51 @@
1
+ import type { ApplicationUser, ProviderActor } from "@copilotkit/channels-ui";
2
+ /** Provider tenant or workspace facts available at ingress. */
3
+ export interface ChannelTenant {
4
+ readonly id: string;
5
+ readonly name?: string;
6
+ }
7
+ /** Provider installation facts available at ingress. */
8
+ export interface ChannelInstallation {
9
+ readonly id: string;
10
+ }
11
+ /** Provider conversation facts available at ingress. */
12
+ export interface ChannelConversation {
13
+ readonly id: string;
14
+ readonly kind?: string;
15
+ }
16
+ /** Normalized provider event facts available at ingress. */
17
+ export interface ChannelEvent {
18
+ readonly id?: string;
19
+ readonly occurredAt?: string;
20
+ readonly [key: string]: unknown;
21
+ }
22
+ /** Adapter-provided facts used to select one canonical application user. */
23
+ export interface ChannelIdentityContext {
24
+ readonly provider: string;
25
+ readonly tenant: ChannelTenant;
26
+ readonly installation: ChannelInstallation;
27
+ readonly actor: ProviderActor;
28
+ readonly conversation: ChannelConversation;
29
+ readonly trigger: string;
30
+ readonly event: ChannelEvent;
31
+ readonly raw: unknown;
32
+ readonly lookupProfile?: () => Promise<ProviderActor | undefined>;
33
+ }
34
+ /** Explicit Channel identity strategy. */
35
+ export type ChannelIdentifyUser = "platform" | ((context: ChannelIdentityContext) => ApplicationUser | null | Promise<ApplicationUser | null>);
36
+ /** Identity facts supplied by an adapter alongside every ingress event. */
37
+ export type IngressIdentityContext = Omit<ChannelIdentityContext, "provider" | "actor">;
38
+ /** Stable public failure for malformed custom identity results. */
39
+ export declare class ChannelIdentityResultError extends Error {
40
+ readonly code = "channel_identity_invalid";
41
+ constructor();
42
+ }
43
+ /** Stable public failure for a configured identity callback exception. */
44
+ export declare class ChannelIdentityResolutionError extends Error {
45
+ readonly code = "channel_identity_failed";
46
+ readonly cause: unknown;
47
+ constructor(cause: unknown);
48
+ }
49
+ /** Resolve one immutable canonical application user for one ingress event. */
50
+ export declare function resolveChannelUser(identifyUser: ChannelIdentifyUser | undefined, context: ChannelIdentityContext): Promise<ApplicationUser | null>;
51
+ //# sourceMappingURL=identity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"identity.d.ts","sourceRoot":"","sources":["../src/identity.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAE9E,+DAA+D;AAC/D,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,wDAAwD;AACxD,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;CACrB;AAED,wDAAwD;AACxD,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,4DAA4D;AAC5D,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACjC;AAED,4EAA4E;AAC5E,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAC/B,QAAQ,CAAC,YAAY,EAAE,mBAAmB,CAAC;IAC3C,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC;IAC9B,QAAQ,CAAC,YAAY,EAAE,mBAAmB,CAAC;IAC3C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC;IAC7B,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC,CAAC;CACnE;AAED,0CAA0C;AAC1C,MAAM,MAAM,mBAAmB,GAC3B,UAAU,GACV,CAAC,CACC,OAAO,EAAE,sBAAsB,KAC5B,eAAe,GAAG,IAAI,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAAC,CAAC;AAEnE,2EAA2E;AAC3E,MAAM,MAAM,sBAAsB,GAAG,IAAI,CACvC,sBAAsB,EACtB,UAAU,GAAG,OAAO,CACrB,CAAC;AAEF,mEAAmE;AACnE,qBAAa,0BAA2B,SAAQ,KAAK;IACnD,QAAQ,CAAC,IAAI,8BAA8B;;CAQ5C;AAED,0EAA0E;AAC1E,qBAAa,8BAA+B,SAAQ,KAAK;IACvD,QAAQ,CAAC,IAAI,6BAA6B;IAC1C,SAAkB,KAAK,EAAE,OAAO,CAAC;gBAErB,KAAK,EAAE,OAAO;CAK3B;AAsBD,8EAA8E;AAC9E,wBAAsB,kBAAkB,CACtC,YAAY,EAAE,mBAAmB,GAAG,SAAS,EAC7C,OAAO,EAAE,sBAAsB,GAC9B,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAgBjC"}
@@ -0,0 +1,56 @@
1
+ /** Stable public failure for malformed custom identity results. */
2
+ export class ChannelIdentityResultError extends Error {
3
+ code = "channel_identity_invalid";
4
+ constructor() {
5
+ super("Channel identifyUser must return null or an application user with non-empty id and name");
6
+ this.name = "ChannelIdentityResultError";
7
+ }
8
+ }
9
+ /** Stable public failure for a configured identity callback exception. */
10
+ export class ChannelIdentityResolutionError extends Error {
11
+ code = "channel_identity_failed";
12
+ cause;
13
+ constructor(cause) {
14
+ super("Channel identifyUser failed");
15
+ this.name = "ChannelIdentityResolutionError";
16
+ this.cause = cause;
17
+ }
18
+ }
19
+ function hasText(value) {
20
+ return typeof value === "string" && value.trim().length > 0;
21
+ }
22
+ function validateApplicationUser(value) {
23
+ if (value === null)
24
+ return null;
25
+ if (typeof value !== "object" ||
26
+ value === undefined ||
27
+ !hasText(value.id) ||
28
+ !hasText(value.name)) {
29
+ throw new ChannelIdentityResultError();
30
+ }
31
+ return Object.freeze({
32
+ id: value.id,
33
+ name: value.name,
34
+ });
35
+ }
36
+ /** Resolve one immutable canonical application user for one ingress event. */
37
+ export async function resolveChannelUser(identifyUser, context) {
38
+ if (identifyUser === undefined)
39
+ return null;
40
+ if (identifyUser === "platform") {
41
+ if (context.actor.kind !== "human")
42
+ return null;
43
+ return Object.freeze({
44
+ id: [context.provider, context.tenant.id, context.actor.id].join(":"),
45
+ name: context.actor.name ?? context.actor.handle ?? context.actor.id,
46
+ });
47
+ }
48
+ let identified;
49
+ try {
50
+ identified = await identifyUser(context);
51
+ }
52
+ catch (cause) {
53
+ throw new ChannelIdentityResolutionError(cause);
54
+ }
55
+ return validateApplicationUser(identified);
56
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=identity.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"identity.test.d.ts","sourceRoot":"","sources":["../src/identity.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,109 @@
1
+ import { expect, test, vi } from "vitest";
2
+ import { ChannelIdentityResolutionError, ChannelIdentityResultError, resolveChannelUser, } from "./identity.js";
3
+ function context(kind = "human") {
4
+ return {
5
+ provider: "slack",
6
+ tenant: { id: "T1", name: "Acme" },
7
+ installation: { id: "I1" },
8
+ actor: {
9
+ id: "U1",
10
+ kind,
11
+ name: "Ada Lovelace",
12
+ handle: "ada",
13
+ },
14
+ conversation: { id: "C1", kind: "channel" },
15
+ trigger: "message",
16
+ event: { id: "E1" },
17
+ raw: { type: "app_mention" },
18
+ };
19
+ }
20
+ test("platform identity maps a human actor to a tenant-namespaced application user", async () => {
21
+ await expect(resolveChannelUser("platform", context())).resolves.toEqual({
22
+ id: "slack:T1:U1",
23
+ name: "Ada Lovelace",
24
+ });
25
+ });
26
+ test("platform identity keeps provider and tenant namespaces distinct", async () => {
27
+ const slack = context();
28
+ const otherTenant = { ...context(), tenant: { id: "T2" } };
29
+ const teams = { ...context(), provider: "teams" };
30
+ await expect(resolveChannelUser("platform", slack)).resolves.toMatchObject({
31
+ id: "slack:T1:U1",
32
+ });
33
+ await expect(resolveChannelUser("platform", otherTenant)).resolves.toMatchObject({ id: "slack:T2:U1" });
34
+ await expect(resolveChannelUser("platform", teams)).resolves.toMatchObject({
35
+ id: "teams:T1:U1",
36
+ });
37
+ });
38
+ test.each(["bot", "app", "system", "unknown"])("platform identity maps a %s actor to null", async (kind) => {
39
+ await expect(resolveChannelUser("platform", context(kind))).resolves.toBeNull();
40
+ });
41
+ test("platform identity falls back from name to handle and provider id", async () => {
42
+ const withHandle = {
43
+ ...context(),
44
+ actor: { id: "U1", kind: "human", handle: "ada" },
45
+ };
46
+ await expect(resolveChannelUser("platform", withHandle)).resolves.toMatchObject({
47
+ name: "ada",
48
+ });
49
+ const withId = {
50
+ ...context(),
51
+ actor: { id: "U1", kind: "human" },
52
+ };
53
+ await expect(resolveChannelUser("platform", withId)).resolves.toMatchObject({
54
+ name: "U1",
55
+ });
56
+ });
57
+ test("a custom identity callback may map a bot actor", async () => {
58
+ const botContext = context("bot");
59
+ await expect(resolveChannelUser(() => ({ id: "service-1", name: "Build bot" }), botContext)).resolves.toEqual({ id: "service-1", name: "Build bot" });
60
+ });
61
+ test.each([
62
+ undefined,
63
+ {},
64
+ { id: "", name: "Ada" },
65
+ { id: "person-1", name: "" },
66
+ ])("a malformed custom identity result fails with the stable public error", async (result) => {
67
+ await expect(resolveChannelUser(() => result, context())).rejects.toMatchObject({
68
+ code: "channel_identity_invalid",
69
+ name: new ChannelIdentityResultError().name,
70
+ });
71
+ });
72
+ test("a thrown custom identity error uses a stable public failure without platform fallback", async () => {
73
+ const failure = new Error("identity service unavailable");
74
+ const result = resolveChannelUser(() => {
75
+ throw failure;
76
+ }, context());
77
+ await expect(result).rejects.toMatchObject({
78
+ code: "channel_identity_failed",
79
+ cause: failure,
80
+ });
81
+ await expect(result).rejects.toBeInstanceOf(ChannelIdentityResolutionError);
82
+ });
83
+ test("profile lookup stays lazy unless the custom callback calls it", async () => {
84
+ const lookupProfile = vi.fn(async () => ({
85
+ id: "U1",
86
+ kind: "human",
87
+ email: "ada@example.com",
88
+ }));
89
+ const identityContext = { ...context(), lookupProfile };
90
+ await resolveChannelUser("platform", identityContext);
91
+ expect(lookupProfile).not.toHaveBeenCalled();
92
+ await resolveChannelUser(async (received) => {
93
+ const profile = await received.lookupProfile?.();
94
+ return { id: "person-1", name: profile?.email ?? "Ada" };
95
+ }, identityContext);
96
+ expect(lookupProfile).toHaveBeenCalledTimes(1);
97
+ });
98
+ test("a failing lazy profile lookup cannot break platform identity", async () => {
99
+ const lookupProfile = vi.fn(async () => {
100
+ throw new Error("provider profile unavailable");
101
+ });
102
+ const identityContext = {
103
+ ...context(),
104
+ actor: { id: "U1", kind: "human" },
105
+ lookupProfile,
106
+ };
107
+ await expect(resolveChannelUser("platform", identityContext)).resolves.toEqual({ id: "slack:T1:U1", name: "U1" });
108
+ expect(lookupProfile).not.toHaveBeenCalled();
109
+ });
package/dist/index.d.ts CHANGED
@@ -2,28 +2,33 @@ export { createChannel } from "./create-channel.js";
2
2
  export { isolateAgentInstance, resolveChannelConcurrency, } from "./create-channel.js";
3
3
  export { sanitizeAgentEventStream } from "./sanitize-agent-events.js";
4
4
  export { HttpAgent } from "@ag-ui/client";
5
- export type { Channel, CreateChannelOptions, ManagedChannelProvider, ReplyContinuationOptions, ChannelHandler, ThreadStartHandler, ReactionEvent, ReactionHandler, ModalSubmitEvent, ModalSubmitHandler, ModalCloseEvent, ModalCloseHandler, StoreConfig, LockConflictDecision, ChannelConcurrency, StatefulThread, ChannelComponent, } from "./create-channel.js";
5
+ export type { Channel, CreateChannelOptions, ReplyContinuationOptions, ChannelHandler, WelcomeHandler, ThreadStartHandler, ReactionEvent, ReactionHandler, ModalSubmitEvent, ModalSubmitHandler, ModalCloseEvent, ModalCloseHandler, StoreConfig, LockConflictDecision, ChannelConcurrency, StatefulThread, ChannelComponent, } from "./create-channel.js";
6
+ export { ChannelIdentityResolutionError, ChannelIdentityResultError, resolveChannelUser, } from "./identity.js";
7
+ export type { ChannelConversation, ChannelEvent, ChannelIdentifyUser, ChannelIdentityContext, ChannelInstallation, ChannelTenant, IngressIdentityContext, } from "./identity.js";
6
8
  export { Thread } from "./thread.js";
7
9
  export type { ThreadDeps } from "./thread.js";
8
- export type { PlatformAdapter, AdapterStartContext, RunRenderer, IngressSink, IncomingTurn, InteractionEvent, IncomingCommand, IncomingThreadStart, IncomingReaction, IncomingModalSubmit, IncomingModalClose, ModalSubmitResult, SurfaceCapabilities, ReplyTarget, ConversationStore, AgentSession, CapturedToolCall, CapturedInterrupt, ChannelAgentLifecycleArgs, ChannelAgentLoopResult, CanonicalRunIdentity, UserQuery, NativePayload, } from "./platform-adapter.js";
10
+ export type { PlatformAdapter, AdapterStartContext, RunRenderer, IngressSink, IncomingTurn, InteractionEvent, IncomingCommand, IncomingThreadStart, IncomingWelcome, IncomingReaction, IncomingModalSubmit, IncomingModalClose, ModalSubmitResult, SurfaceCapabilities, ReplyTarget, ConversationStore, AgentSession, CapturedToolCall, CapturedInterrupt, ChannelAgentLifecycleArgs, ChannelAgentLoopResult, CanonicalRunIdentity, UserQuery, NativePayload, } from "./platform-adapter.js";
9
11
  export { defineChannelCommand, normalizeCommandName, toCommandSpec, } from "./commands.js";
10
12
  export type { ChannelCommand, CommandContext, CommandSpec, } from "./commands.js";
11
13
  export { InMemoryActionStore } from "./action-store.js";
12
- export type { ActionStore, ActionSnapshot } from "./action-store.js";
13
- export { ActionRegistry, ActionExpiredError } from "./action-registry.js";
14
+ export type { ActionStore, ActionSnapshot, ActionContinuationContext, ActionContinuationBinding, ActionContinuationInitiator, ActionContinuationSnapshot, } from "./action-store.js";
15
+ export { ActionRegistry, ActionContinuationMismatchError, ActionExpiredError, } from "./action-registry.js";
14
16
  export type { StateStore } from "./state/state-store.js";
15
17
  export { MemoryStore } from "./state/memory-store.js";
16
18
  export { parseDuration } from "./state/duration.js";
17
19
  export { createStateBackedConversationStore } from "./state/state-conversation-store.js";
18
20
  export { Transcripts } from "./transcripts.js";
19
- export type { TranscriptEntry, Identity, TranscriptsConfig, } from "./transcripts.js";
21
+ export type { TranscriptEntry, TranscriptsConfig } from "./transcripts.js";
20
22
  export { toAgentToolDescriptors, parseToolArgs, stringifyHandlerResult, defineChannelTool, } from "./tools.js";
21
23
  export type { ChannelTool, ObjectSchema, ChannelToolContext, ContextEntry, AgentToolDescriptor, } from "./tools.js";
22
24
  export { mintId, stableStringify } from "./mint-id.js";
23
25
  export { runAgentLoop } from "./run-loop.js";
24
26
  export type { RunLoopArgs } from "./run-loop.js";
27
+ export { ChannelContinuationRequiredError, ChannelMemorySubjectRequiredError, ChannelMemoryUnavailableError, ChannelMemoryUserRequiredError, } from "./thread.js";
25
28
  export { ChannelDeliveryTerminatedError, isChannelDeliveryTerminatedError, } from "./delivery-error.js";
26
29
  export type { PlatformCodec } from "./codec.js";
30
+ export { ChannelMemoryGrantInvalidError, hasMemoryAccess, resolveMemoryGrant, } from "./memory.js";
31
+ export type { MemoryAccess, MemoryGrant, ResolvedChannelMemory, } from "./memory.js";
27
32
  export { FakeAdapter, makeFakeRunRenderer } from "./testing/fake-adapter.js";
28
33
  export { FakeAgent } from "./testing/fake-agent.js";
29
34
  export * from "@copilotkit/channels-ui";