@copilotkit/channels-core 0.4.1-canary.perfall1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +7 -8
  2. package/dist/canonical-run-loop.test.d.ts +2 -0
  3. package/dist/canonical-run-loop.test.d.ts.map +1 -0
  4. package/dist/canonical-run-loop.test.js +453 -0
  5. package/dist/codec.d.ts +2 -3
  6. package/dist/codec.d.ts.map +1 -1
  7. package/dist/create-channel.d.ts +110 -4
  8. package/dist/create-channel.d.ts.map +1 -1
  9. package/dist/create-channel.js +258 -92
  10. package/dist/create-channel.test.js +552 -29
  11. package/dist/delivery-error.d.ts +17 -0
  12. package/dist/delivery-error.d.ts.map +1 -0
  13. package/dist/delivery-error.js +22 -0
  14. package/dist/index.d.ts +6 -2
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +8 -0
  17. package/dist/managed-v1-await-choice-guard.test.d.ts +2 -0
  18. package/dist/managed-v1-await-choice-guard.test.d.ts.map +1 -0
  19. package/dist/managed-v1-await-choice-guard.test.js +52 -0
  20. package/dist/platform-adapter.d.ts +74 -11
  21. package/dist/platform-adapter.d.ts.map +1 -1
  22. package/dist/run-loop.d.ts +25 -6
  23. package/dist/run-loop.d.ts.map +1 -1
  24. package/dist/run-loop.js +289 -39
  25. package/dist/run-loop.test.js +37 -0
  26. package/dist/sanitize-agent-events.d.ts +24 -0
  27. package/dist/sanitize-agent-events.d.ts.map +1 -0
  28. package/dist/sanitize-agent-events.js +88 -0
  29. package/dist/sanitize-agent-events.test.d.ts +2 -0
  30. package/dist/sanitize-agent-events.test.d.ts.map +1 -0
  31. package/dist/sanitize-agent-events.test.js +194 -0
  32. package/dist/source-platform.test.d.ts +2 -0
  33. package/dist/source-platform.test.d.ts.map +1 -0
  34. package/dist/source-platform.test.js +149 -0
  35. package/dist/testing/fake-adapter.d.ts +8 -1
  36. package/dist/testing/fake-adapter.d.ts.map +1 -1
  37. package/dist/testing/fake-adapter.js +30 -1
  38. package/dist/testing/fake-agent.d.ts +5 -0
  39. package/dist/testing/fake-agent.d.ts.map +1 -1
  40. package/dist/testing/fake-agent.js +12 -0
  41. package/dist/thread-promise-contract.test.d.ts +2 -0
  42. package/dist/thread-promise-contract.test.d.ts.map +1 -0
  43. package/dist/thread-promise-contract.test.js +37 -0
  44. package/dist/thread.d.ts +5 -0
  45. package/dist/thread.d.ts.map +1 -1
  46. package/dist/thread.js +338 -243
  47. package/package.json +4 -4
package/README.md CHANGED
@@ -68,11 +68,8 @@ handler:
68
68
  ```ts
69
69
  import { createChannel } from "@copilotkit/channels-core";
70
70
  import { slack } from "@copilotkit/channels-slack";
71
- import {
72
- CopilotRuntime,
73
- CopilotKitIntelligence,
74
- createCopilotRuntimeHandler,
75
- } from "@copilotkit/runtime/v2";
71
+ import { CopilotRuntime, CopilotKitIntelligence } from "@copilotkit/runtime/v2";
72
+ import { createCopilotNodeListener } from "@copilotkit/runtime/v2/node";
76
73
 
77
74
  const channel = createChannel({
78
75
  name: "support-bot", // project-unique Intelligence Channel name
@@ -89,9 +86,11 @@ const runtime = new CopilotRuntime({
89
86
  channels: [channel],
90
87
  });
91
88
 
92
- const handler = createCopilotRuntimeHandler({ runtime });
93
- await handler.channels.ready(); // starts every declared channel
94
- // await handler.channels.stop(); // tears them down
89
+ // Creating the listener starts every declared channel's connection.
90
+ const listener = createCopilotNodeListener({ runtime });
91
+ // Optional: await that activation so a broken config fails startup loudly.
92
+ await listener.channels.ready();
93
+ // await listener.channels.stop(); // tears them down
95
94
  ```
96
95
 
97
96
  ## `Thread`
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=canonical-run-loop.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canonical-run-loop.test.d.ts","sourceRoot":"","sources":["../src/canonical-run-loop.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,453 @@
1
+ import { EventType } from "@ag-ui/client";
2
+ import { expect, test } from "vitest";
3
+ import { z } from "zod";
4
+ import { runAgentLoop } from "./run-loop.js";
5
+ import { ChannelDeliveryTerminatedError } from "./delivery-error.js";
6
+ import { FakeAgent } from "./testing/fake-agent.js";
7
+ const canonicalRun = {
8
+ threadId: "canonical-thread",
9
+ runId: "canonical-run",
10
+ };
11
+ function lifecycleBatch(runId, middle) {
12
+ return [
13
+ {
14
+ type: EventType.RUN_STARTED,
15
+ threadId: "inner-thread",
16
+ runId,
17
+ },
18
+ ...middle,
19
+ {
20
+ type: EventType.RUN_FINISHED,
21
+ threadId: "inner-thread",
22
+ runId,
23
+ },
24
+ ];
25
+ }
26
+ function textEvent(messageId, text) {
27
+ return {
28
+ type: EventType.TEXT_MESSAGE_CONTENT,
29
+ messageId,
30
+ delta: text,
31
+ };
32
+ }
33
+ async function emitBatch(subscriber, agent, runId, middle) {
34
+ const input = {
35
+ threadId: agent.threadId,
36
+ runId,
37
+ messages: agent.messages,
38
+ state: agent.state,
39
+ tools: [],
40
+ context: [],
41
+ forwardedProps: {},
42
+ };
43
+ const params = {
44
+ messages: agent.messages,
45
+ state: agent.state,
46
+ agent,
47
+ input,
48
+ };
49
+ for (const event of lifecycleBatch(runId, middle)) {
50
+ await subscriber.onEvent?.({ ...params, event });
51
+ switch (event.type) {
52
+ case EventType.RUN_STARTED:
53
+ await subscriber.onRunStartedEvent?.({ ...params, event });
54
+ break;
55
+ case EventType.RUN_FINISHED:
56
+ await subscriber.onRunFinishedEvent?.({
57
+ ...params,
58
+ event,
59
+ outcome: "success",
60
+ });
61
+ break;
62
+ case EventType.RUN_ERROR:
63
+ await subscriber.onRunErrorEvent?.({ ...params, event });
64
+ break;
65
+ case EventType.TEXT_MESSAGE_CONTENT:
66
+ await subscriber.onTextMessageContentEvent?.({
67
+ ...params,
68
+ event,
69
+ textMessageBuffer: event.delta,
70
+ });
71
+ break;
72
+ case EventType.TOOL_CALL_END:
73
+ await subscriber.onToolCallEndEvent?.({
74
+ ...params,
75
+ event,
76
+ toolCallName: "echo",
77
+ toolCallArgs: { value: "ok" },
78
+ });
79
+ break;
80
+ }
81
+ }
82
+ }
83
+ function setupRenderer(options = {}) {
84
+ const renderedEvents = [];
85
+ const renderedFinishMetadata = [];
86
+ const toolCalls = [];
87
+ const subscriber = {
88
+ onRunStartedEvent: ({ event }) => {
89
+ renderedEvents.push(event);
90
+ },
91
+ onRunFinishedEvent: ({ event }) => {
92
+ renderedEvents.push(event);
93
+ renderedFinishMetadata.push(event.metadata);
94
+ if (options.failOnFinish) {
95
+ throw new Error("renderer finalization failed");
96
+ }
97
+ },
98
+ onRunErrorEvent: ({ event }) => {
99
+ renderedEvents.push(event);
100
+ },
101
+ onTextMessageContentEvent: ({ event }) => {
102
+ renderedEvents.push(event);
103
+ if (options.failOnContent) {
104
+ throw new Error("renderer failed");
105
+ }
106
+ },
107
+ onToolCallEndEvent: ({ event, toolCallName, toolCallArgs }) => {
108
+ toolCalls.push({
109
+ toolCallId: event.toolCallId,
110
+ toolCallName,
111
+ toolCallArgs,
112
+ });
113
+ },
114
+ };
115
+ return {
116
+ renderer: {
117
+ subscriber,
118
+ markInterrupted: async () => { },
119
+ getCapturedToolCalls: () => toolCalls,
120
+ getPendingInterrupt: () => undefined,
121
+ clearPendingInterrupt: () => { },
122
+ },
123
+ renderedEvents,
124
+ renderedFinishMetadata,
125
+ };
126
+ }
127
+ test("managed runAgentLoop emits one canonical lifecycle and shares stamped events with ingestion and rendering", async () => {
128
+ let agent;
129
+ agent = new FakeAgent([
130
+ (subscriber) => emitBatch(subscriber, agent, "inner-run-1", [
131
+ textEvent("message-1", "first"),
132
+ {
133
+ type: EventType.TOOL_CALL_END,
134
+ toolCallId: "tool-call-1",
135
+ },
136
+ ]),
137
+ (subscriber) => emitBatch(subscriber, agent, "inner-run-2", [
138
+ textEvent("message-2", "second"),
139
+ ]),
140
+ ]);
141
+ const { renderer, renderedEvents } = setupRenderer();
142
+ const ingestedEvents = [];
143
+ let commits = 0;
144
+ const echo = {
145
+ name: "echo",
146
+ description: "Return the value.",
147
+ parameters: z.object({ value: z.string() }),
148
+ handler: ({ value }) => value,
149
+ };
150
+ const result = await runAgentLoop({
151
+ agent,
152
+ renderer,
153
+ tools: new Map([["echo", echo]]),
154
+ toolDescriptors: [],
155
+ context: [],
156
+ makeToolCtx: () => {
157
+ throw new Error("tool context is not used by this test");
158
+ },
159
+ subscriber: {
160
+ onEvent: ({ event }) => {
161
+ ingestedEvents.push(event);
162
+ },
163
+ },
164
+ canonicalRun: {
165
+ ...canonicalRun,
166
+ beforeToolCall: async () => {
167
+ commits += 1;
168
+ },
169
+ },
170
+ });
171
+ const lifecycleTypes = ingestedEvents
172
+ .filter(({ type }) => type === EventType.RUN_STARTED ||
173
+ type === EventType.RUN_FINISHED ||
174
+ type === EventType.RUN_ERROR)
175
+ .map(({ type }) => type);
176
+ const ingestedContent = ingestedEvents.filter(({ type }) => type === EventType.TEXT_MESSAGE_CONTENT);
177
+ const renderedContent = renderedEvents.filter(({ type }) => type === EventType.TEXT_MESSAGE_CONTENT);
178
+ expect(result).toEqual({ iterations: 2, interrupted: false });
179
+ expect(commits).toBe(1);
180
+ expect(lifecycleTypes).toEqual([
181
+ EventType.RUN_STARTED,
182
+ EventType.RUN_FINISHED,
183
+ ]);
184
+ expect(renderedEvents
185
+ .filter(({ type }) => type === EventType.RUN_STARTED ||
186
+ type === EventType.RUN_FINISHED ||
187
+ type === EventType.RUN_ERROR)
188
+ .map(({ type }) => type)).toEqual([EventType.RUN_STARTED, EventType.RUN_FINISHED]);
189
+ expect(ingestedContent).toHaveLength(2);
190
+ expect(renderedContent).toHaveLength(2);
191
+ expect(renderedContent[0]).toBe(ingestedContent[0]);
192
+ expect(renderedContent[1]).toBe(ingestedContent[1]);
193
+ expect(ingestedEvents).toEqual(expect.arrayContaining([
194
+ expect.objectContaining(canonicalRun),
195
+ expect.objectContaining({
196
+ ...canonicalRun,
197
+ type: EventType.TEXT_MESSAGE_CONTENT,
198
+ messageId: "message-1",
199
+ }),
200
+ expect.objectContaining({
201
+ ...canonicalRun,
202
+ type: EventType.TEXT_MESSAGE_CONTENT,
203
+ messageId: "message-2",
204
+ }),
205
+ ]));
206
+ });
207
+ test("managed renderer failure is deferred until canonical RUN_FINISHED reaches ingestion", async () => {
208
+ let agent;
209
+ agent = new FakeAgent([
210
+ (subscriber) => emitBatch(subscriber, agent, "inner-run", [
211
+ textEvent("message-1", "hello"),
212
+ ]),
213
+ ]);
214
+ const { renderer } = setupRenderer({ failOnContent: true });
215
+ const ingestedByTypedCallback = [];
216
+ const ingestedLifecycle = [];
217
+ const result = await runAgentLoop({
218
+ agent,
219
+ renderer,
220
+ tools: new Map(),
221
+ toolDescriptors: [],
222
+ context: [],
223
+ makeToolCtx: () => {
224
+ throw new Error("no tool calls are expected");
225
+ },
226
+ subscriber: {
227
+ onEvent: ({ event }) => {
228
+ if (event.type === EventType.RUN_STARTED ||
229
+ event.type === EventType.RUN_FINISHED ||
230
+ event.type === EventType.RUN_ERROR) {
231
+ ingestedLifecycle.push(event);
232
+ }
233
+ },
234
+ onTextMessageContentEvent: ({ event }) => {
235
+ ingestedByTypedCallback.push(event);
236
+ },
237
+ },
238
+ canonicalRun,
239
+ });
240
+ expect(result).toMatchObject({
241
+ iterations: 1,
242
+ interrupted: false,
243
+ deliveryError: { message: "renderer failed" },
244
+ });
245
+ expect(ingestedByTypedCallback).toEqual([
246
+ expect.objectContaining({
247
+ ...canonicalRun,
248
+ type: EventType.TEXT_MESSAGE_CONTENT,
249
+ messageId: "message-1",
250
+ }),
251
+ ]);
252
+ expect(ingestedLifecycle.map(({ type }) => type)).toEqual([
253
+ EventType.RUN_STARTED,
254
+ EventType.RUN_FINISHED,
255
+ ]);
256
+ });
257
+ test("managed renderer failure freezes later rendering while canonical ingestion finishes", async () => {
258
+ let agent;
259
+ agent = new FakeAgent([
260
+ (subscriber) => emitBatch(subscriber, agent, "inner-run", [
261
+ textEvent("message-1", "first"),
262
+ textEvent("message-2", "second"),
263
+ ]),
264
+ ]);
265
+ const { renderer, renderedEvents } = setupRenderer({
266
+ failOnContent: true,
267
+ });
268
+ const ingestedEvents = [];
269
+ const result = await runAgentLoop({
270
+ agent,
271
+ renderer,
272
+ tools: new Map(),
273
+ toolDescriptors: [],
274
+ context: [],
275
+ makeToolCtx: () => {
276
+ throw new Error("no tool calls are expected");
277
+ },
278
+ subscriber: {
279
+ onEvent: ({ event }) => {
280
+ ingestedEvents.push(event);
281
+ },
282
+ },
283
+ canonicalRun,
284
+ });
285
+ expect(result).toMatchObject({
286
+ deliveryError: { message: "renderer failed" },
287
+ });
288
+ expect(renderedEvents.map(({ type, ...event }) => ({
289
+ type,
290
+ messageId: "messageId" in event ? event.messageId : undefined,
291
+ }))).toEqual([
292
+ { type: EventType.RUN_STARTED, messageId: undefined },
293
+ { type: EventType.TEXT_MESSAGE_CONTENT, messageId: "message-1" },
294
+ ]);
295
+ expect(ingestedEvents
296
+ .filter(({ type }) => type === EventType.TEXT_MESSAGE_CONTENT ||
297
+ type === EventType.RUN_FINISHED)
298
+ .map(({ type, ...event }) => ({
299
+ type,
300
+ messageId: "messageId" in event ? event.messageId : undefined,
301
+ }))).toEqual([
302
+ { type: EventType.TEXT_MESSAGE_CONTENT, messageId: "message-1" },
303
+ { type: EventType.TEXT_MESSAGE_CONTENT, messageId: "message-2" },
304
+ { type: EventType.RUN_FINISHED, messageId: undefined },
305
+ ]);
306
+ });
307
+ test("terminal delivery tool failure stops the loop and closes renderer fanout", async () => {
308
+ const deliveryError = new ChannelDeliveryTerminatedError("provider delivery timed out");
309
+ let agent;
310
+ agent = new FakeAgent([
311
+ (subscriber) => emitBatch(subscriber, agent, "inner-run-1", [
312
+ {
313
+ type: EventType.TOOL_CALL_END,
314
+ toolCallId: "tool-call-1",
315
+ },
316
+ ]),
317
+ (subscriber) => emitBatch(subscriber, agent, "inner-run-2", [
318
+ textEvent("message-after-failure", "should not render"),
319
+ ]),
320
+ ]);
321
+ const { renderer, renderedEvents } = setupRenderer();
322
+ const ingestedEvents = [];
323
+ const postFile = {
324
+ name: "echo",
325
+ description: "Post a managed file.",
326
+ parameters: z.object({ value: z.string() }),
327
+ handler: () => {
328
+ throw deliveryError;
329
+ },
330
+ };
331
+ await expect(runAgentLoop({
332
+ agent,
333
+ renderer,
334
+ tools: new Map([["echo", postFile]]),
335
+ toolDescriptors: [],
336
+ context: [],
337
+ makeToolCtx: () => ({ thread: {}, platform: "fake" }),
338
+ subscriber: {
339
+ onEvent: ({ event }) => {
340
+ ingestedEvents.push(event);
341
+ },
342
+ },
343
+ canonicalRun,
344
+ })).rejects.toBe(deliveryError);
345
+ expect(agent.runAgentCalls).toBe(1);
346
+ expect(agent.messages.some(({ role }) => role === "tool")).toBe(false);
347
+ expect(renderedEvents
348
+ .filter(({ type }) => type === EventType.RUN_STARTED ||
349
+ type === EventType.RUN_FINISHED ||
350
+ type === EventType.RUN_ERROR ||
351
+ type === EventType.TEXT_MESSAGE_CONTENT)
352
+ .map(({ type }) => type)).toEqual([EventType.RUN_STARTED]);
353
+ expect(ingestedEvents
354
+ .filter(({ type }) => type === EventType.RUN_STARTED ||
355
+ type === EventType.RUN_FINISHED ||
356
+ type === EventType.RUN_ERROR)
357
+ .map(({ type }) => type)).toEqual([EventType.RUN_STARTED, EventType.RUN_ERROR]);
358
+ });
359
+ test("inner RUN_ERROR becomes one canonical outer RUN_ERROR", async () => {
360
+ let agent;
361
+ agent = new FakeAgent([
362
+ (subscriber) => emitBatch(subscriber, agent, "inner-run", [
363
+ {
364
+ type: EventType.RUN_ERROR,
365
+ message: "inner agent failed",
366
+ code: "INNER_FAILED",
367
+ },
368
+ ]),
369
+ ]);
370
+ const { renderer, renderedEvents } = setupRenderer();
371
+ const ingestedEvents = [];
372
+ await expect(runAgentLoop({
373
+ agent,
374
+ renderer,
375
+ tools: new Map(),
376
+ toolDescriptors: [],
377
+ context: [],
378
+ makeToolCtx: () => {
379
+ throw new Error("no tool calls are expected");
380
+ },
381
+ subscriber: {
382
+ onEvent: ({ event }) => {
383
+ ingestedEvents.push(event);
384
+ },
385
+ },
386
+ canonicalRun,
387
+ })).rejects.toMatchObject({
388
+ message: "inner agent failed",
389
+ code: "INNER_FAILED",
390
+ });
391
+ const expectedLifecycle = [
392
+ expect.objectContaining({
393
+ ...canonicalRun,
394
+ type: EventType.RUN_STARTED,
395
+ }),
396
+ expect.objectContaining({
397
+ ...canonicalRun,
398
+ type: EventType.RUN_ERROR,
399
+ message: "inner agent failed",
400
+ code: "INNER_FAILED",
401
+ }),
402
+ ];
403
+ expect(ingestedEvents.filter(({ type }) => type === EventType.RUN_STARTED ||
404
+ type === EventType.RUN_FINISHED ||
405
+ type === EventType.RUN_ERROR)).toEqual(expectedLifecycle);
406
+ expect(renderedEvents.filter(({ type }) => type === EventType.RUN_STARTED ||
407
+ type === EventType.RUN_FINISHED ||
408
+ type === EventType.RUN_ERROR)).toEqual(expectedLifecycle);
409
+ });
410
+ test("managed renderer finalization sees runner metadata without replacing canonical RUN_FINISHED", async () => {
411
+ let agent;
412
+ agent = new FakeAgent([
413
+ (subscriber) => emitBatch(subscriber, agent, "inner-run", []),
414
+ ]);
415
+ const { renderer, renderedFinishMetadata } = setupRenderer({
416
+ failOnFinish: true,
417
+ });
418
+ const ingestedEvents = [];
419
+ const runnerMetadata = {
420
+ cpki_event_id: "runner-event-finished",
421
+ cpki_event_seq: 1,
422
+ };
423
+ const result = await runAgentLoop({
424
+ agent,
425
+ renderer,
426
+ tools: new Map(),
427
+ toolDescriptors: [],
428
+ context: [],
429
+ makeToolCtx: () => {
430
+ throw new Error("no tool calls are expected");
431
+ },
432
+ subscriber: {
433
+ onEvent: ({ event }) => {
434
+ if (event.type === EventType.RUN_FINISHED) {
435
+ event.metadata = runnerMetadata;
436
+ }
437
+ ingestedEvents.push(event);
438
+ },
439
+ },
440
+ canonicalRun,
441
+ });
442
+ expect(result).toMatchObject({
443
+ iterations: 1,
444
+ interrupted: false,
445
+ deliveryError: { message: "renderer finalization failed" },
446
+ });
447
+ expect(ingestedEvents
448
+ .filter(({ type }) => type === EventType.RUN_STARTED ||
449
+ type === EventType.RUN_FINISHED ||
450
+ type === EventType.RUN_ERROR)
451
+ .map(({ type }) => type)).toEqual([EventType.RUN_STARTED, EventType.RUN_FINISHED]);
452
+ expect(renderedFinishMetadata).toEqual([runnerMetadata]);
453
+ });
package/dist/codec.d.ts CHANGED
@@ -4,13 +4,12 @@ import type { ChannelNode } from "@copilotkit/channels-ui";
4
4
  * the Intelligence side. It exists so platform *semantics* (how to render IR to
5
5
  * a native payload, and — later — how to normalize a native event to the
6
6
  * neutral ingress shape) live in ONE place, instead of being duplicated between
7
- * a credentialed local adapter (Bolt/discord.js) and the Connector Outbox /
8
- * webhook ingress.
7
+ * a credentialed local adapter and Gateway-owned provider delivery.
9
8
  *
10
9
  * Only the two creds/connection-bound concerns stay per-side: the transport
11
10
  * (who holds the platform connection) and the credentialed send. The codec
12
11
  * excludes both — `renderEgress` is pure (IR → native payload); the actual
13
- * send happens in the Connector Outbox with Intelligence-owned credentials.
12
+ * send happens in the Gateway with Intelligence-owned credentials.
14
13
  *
15
14
  * TODO(OSS-363): add `normalizeIngress(raw): NeutralEvent` once the pure Slack
16
15
  * ingress mapping (mention stripping, stable event-id derivation, real-user
@@ -1 +1 @@
1
- {"version":3,"file":"codec.d.ts","sourceRoot":"","sources":["../src/codec.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAE3D;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,yDAAyD;IACzD,YAAY,CAAC,EAAE,EAAE,WAAW,EAAE,GAAG,OAAO,CAAC;CAC1C"}
1
+ {"version":3,"file":"codec.d.ts","sourceRoot":"","sources":["../src/codec.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAE3D;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,yDAAyD;IACzD,YAAY,CAAC,EAAE,EAAE,WAAW,EAAE,GAAG,OAAO,CAAC;CAC1C"}
@@ -5,11 +5,54 @@ import type { ChannelTool, ContextEntry } from "./tools.js";
5
5
  import type { ChannelCommand, CommandContext } from "./commands.js";
6
6
  import { Thread } from "./thread.js";
7
7
  import type { AbstractAgent } from "@ag-ui/client";
8
- import type { InteractionContext, IncomingMessage, PlatformUser, EmojiValue, ComponentFn, MessageRef } from "@copilotkit/channels-ui";
8
+ import type { ChannelMessage, InteractionContext, IncomingMessage, PlatformUser, EmojiValue, ComponentFn, MessageRef } from "@copilotkit/channels-ui";
9
9
  import { Transcripts } from "./transcripts.js";
10
10
  import type { Identity, TranscriptsConfig } from "./transcripts.js";
11
11
  import type { StandardSchemaV1, InferSchemaOutput } from "./standard-schema.js";
12
12
  export type LockConflictDecision = "drop" | "force";
13
+ /**
14
+ * How overlapping turns on the same `conversationKey` are handled.
15
+ *
16
+ * - `"parallel"` (default) — concurrent turns run together (no exclusive turn lock).
17
+ * - `"serial"` — later turns wait for the in-flight turn on that conversation to finish.
18
+ * - `"drop"` — later turns are discarded while a turn is in flight.
19
+ */
20
+ export type ChannelConcurrency = "parallel" | "serial" | "drop";
21
+ /**
22
+ * Isolate an agent for one turn via `clone()`.
23
+ *
24
+ * Applied to every configured shape, so the object a turn runs on is never one
25
+ * the caller still holds a reference to:
26
+ * - `createChannel({ agent: shared })` — singleton config
27
+ * - `agent: (id) => new Agent()` — fresh factory, cloning an unused agent
28
+ * - `agent: (id) => shared` — factory returning the same object every call
29
+ *
30
+ * The last shape is the one that needs this, and it is easy to write by accident
31
+ * (it is also what a singleton becomes when someone refactors to get at the
32
+ * `threadId`). Sharing one `AbstractAgent` across turns is not safe, because
33
+ * turn concurrency defaults to `"parallel"` — see {@link ChannelConcurrency} —
34
+ * and only the managed adapter serializes same-thread deliveries. On a directly
35
+ * connected adapter two turns in one conversation can run at the same time, and
36
+ * on the same instance they corrupt each other: `messages` is a single array
37
+ * both runs append into, so each run's new-message diff picks up the other's,
38
+ * and `isRunning` / `activeRunDetach$` / `activeRunCompletionPromise` are
39
+ * single-slot fields the second run overwrites while the first is still
40
+ * streaming. Managed delivery serializes them instead, on object identity, which
41
+ * head-of-line blocks two *different* conversations that share one instance.
42
+ *
43
+ * Fails loud on all three ways cloning can fail to isolate: a missing `clone()`,
44
+ * a `clone()` that hands back the same object, and a `clone()` that silently
45
+ * drops subclass state (see {@link assertCloneKeptOwnFields}).
46
+ */
47
+ export declare function isolateAgentInstance(prototype: AbstractAgent, threadId: string): AbstractAgent;
48
+ /**
49
+ * Resolve effective turn concurrency from `store.concurrency` and legacy
50
+ * `store.onLockConflict`. Prefer `concurrency` when both are set.
51
+ */
52
+ export declare function resolveChannelConcurrency(cfg: {
53
+ concurrency?: ChannelConcurrency;
54
+ onLockConflict?: LockConflictDecision | ((conversationKey: string, message: IncomingMessage) => LockConflictDecision | Promise<LockConflictDecision>);
55
+ }): ChannelConcurrency | "legacy-callback";
13
56
  /**
14
57
  * The managed delivery provider a no-adapter Channel targets when it is
15
58
  * activated through CopilotKit Intelligence.
@@ -42,7 +85,7 @@ export type ManagedChannelProvider = "slack" | "teams";
42
85
  export type ChannelComponent = (props: never) => ReturnType<ComponentFn>;
43
86
  export type ChannelHandler<TState = unknown> = (ctx: {
44
87
  thread: StatefulThread<TState>;
45
- message: IncomingMessage;
88
+ message: ChannelMessage;
46
89
  }) => void | Promise<void>;
47
90
  /** Handler for a "conversation opened" lifecycle event (e.g. the Slack assistant pane). */
48
91
  export type ThreadStartHandler<TState = unknown> = (ctx: {
@@ -108,13 +151,51 @@ export interface StoreConfig<TStateSchema extends StandardSchemaV1 | undefined =
108
151
  identity?: Identity;
109
152
  /** Cross-platform transcript storage config. Paired with `identity`. */
110
153
  transcripts?: TranscriptsConfig;
111
- /** What to do when a turn arrives while a prior turn on the same conversationKey is processing. */
154
+ /**
155
+ * How overlapping turns on the same conversationKey are handled.
156
+ * Default: `"parallel"`. Prefer this over {@link onLockConflict}.
157
+ */
158
+ concurrency?: ChannelConcurrency;
159
+ /**
160
+ * What to do when a turn arrives while a prior turn on the same conversationKey is processing.
161
+ *
162
+ * @deprecated Prefer {@link concurrency}. When `concurrency` is unset:
163
+ * `"drop"` → `concurrency: "drop"`, `"force"` → `concurrency: "parallel"`.
164
+ * A callback keeps the legacy lock + drop/force path.
165
+ */
112
166
  onLockConflict?: LockConflictDecision | ((conversationKey: string, message: IncomingMessage) => LockConflictDecision | Promise<LockConflictDecision>);
113
- /** TTL (ms) for the per-conversation turn lock. Default 60_000. */
167
+ /** TTL (ms) for the per-conversation turn lock. Default 60_000. Used by `drop` / legacy paths. */
114
168
  lockTtl?: number;
115
169
  /** TTL (ms) for the inbound event dedup window. Default 300_000. */
116
170
  dedupTtl?: number;
117
171
  }
172
+ /**
173
+ * Tuning for how a long reply is spread across continuation messages.
174
+ *
175
+ * Providers cap how much text one message can hold; past that the reply is
176
+ * split, and past {@link ReplyContinuationOptions.maxMessages} it is truncated
177
+ * with a visible marker. Defaults are conservative and suit most bots — reach
178
+ * for these when a provider's real ceiling differs, when a product wants a
179
+ * different tolerance for how many messages one reply may occupy, or when the
180
+ * truncation notice needs to be in another language.
181
+ *
182
+ * Currently honoured by managed and direct Slack.
183
+ */
184
+ export interface ReplyContinuationOptions {
185
+ /**
186
+ * Soft cap on the UTF-8 bytes one message may hold before continuing into a
187
+ * new one. Bytes rather than characters because the provider ceiling may be
188
+ * counted either way, and bytes are the safe reading for non-Latin scripts.
189
+ */
190
+ readonly messageByteLimit?: number;
191
+ /**
192
+ * Ceiling on messages a single reply may occupy before it is truncated with a
193
+ * visible marker. Bounds a runaway reply.
194
+ */
195
+ readonly maxMessages?: number;
196
+ /** Notice appended when `maxMessages` is reached. Defaults to English copy. */
197
+ readonly truncationMarker?: string;
198
+ }
118
199
  export interface CreateChannelOptions<TStateSchema extends StandardSchemaV1 | undefined = undefined> {
119
200
  /**
120
201
  * Project-unique Intelligence Channel name. Required for Intelligence Channel
@@ -154,7 +235,27 @@ export interface CreateChannelOptions<TStateSchema extends StandardSchemaV1 | un
154
235
  * `slack({ showToolStatus: true })` instead.
155
236
  */
156
237
  showToolStatus?: boolean;
238
+ /**
239
+ * Tuning for splitting a long reply across continuation messages. See
240
+ * {@link ReplyContinuationOptions}. Applies to managed and direct Slack;
241
+ * configure direct Slack with `slack({ replyContinuation })` instead.
242
+ */
243
+ replyContinuation?: ReplyContinuationOptions;
157
244
  agent?: AbstractAgent | ((threadId: string) => AbstractAgent);
245
+ /**
246
+ * Tolerate the AG-UI event streams real agents emit. On by default.
247
+ *
248
+ * `@ag-ui/langgraph` emits a `TOOL_CALL_START` whose `parentMessageId` is
249
+ * `null` — notably the tool call that triggers an interrupt — which strict
250
+ * client-side validation rejects, aborting the whole run and breaking
251
+ * human-in-the-loop. Channels coerce that field on the wire so the run
252
+ * survives; see {@link sanitizeAgentEventStream} for exactly what is touched.
253
+ *
254
+ * Set `false` to stream events through unmodified and let a malformed event
255
+ * fail the run. Only meaningful for agents that stream over HTTP — nothing
256
+ * re-validates the events of an in-process agent.
257
+ */
258
+ sanitizeAgentEvents?: boolean;
158
259
  /** @deprecated Pass `store.adapter` instead. */
159
260
  actionStore?: ActionStore;
160
261
  tools?: ChannelTool[];
@@ -189,6 +290,11 @@ export interface Channel<TState = unknown> {
189
290
  * undefined. Ignored for direct-adapter Channels.
190
291
  */
191
292
  readonly showToolStatus?: boolean;
293
+ /**
294
+ * Continuation-message tuning from `createChannel({ replyContinuation })`.
295
+ * Undefined leaves the provider defaults in place.
296
+ */
297
+ readonly replyContinuation?: ReplyContinuationOptions;
192
298
  /** Declared slash-command names (normalized). Surfaced for Channel activation metadata. */
193
299
  readonly commandNames: string[];
194
300
  onMention(h: ChannelHandler<TState>): void;