@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.
- package/README.md +7 -8
- package/dist/canonical-run-loop.test.d.ts +2 -0
- package/dist/canonical-run-loop.test.d.ts.map +1 -0
- package/dist/canonical-run-loop.test.js +453 -0
- package/dist/codec.d.ts +2 -3
- package/dist/codec.d.ts.map +1 -1
- package/dist/create-channel.d.ts +110 -4
- package/dist/create-channel.d.ts.map +1 -1
- package/dist/create-channel.js +258 -92
- package/dist/create-channel.test.js +552 -29
- package/dist/delivery-error.d.ts +17 -0
- package/dist/delivery-error.d.ts.map +1 -0
- package/dist/delivery-error.js +22 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/managed-v1-await-choice-guard.test.d.ts +2 -0
- package/dist/managed-v1-await-choice-guard.test.d.ts.map +1 -0
- package/dist/managed-v1-await-choice-guard.test.js +52 -0
- package/dist/platform-adapter.d.ts +74 -11
- package/dist/platform-adapter.d.ts.map +1 -1
- package/dist/run-loop.d.ts +25 -6
- package/dist/run-loop.d.ts.map +1 -1
- package/dist/run-loop.js +289 -39
- package/dist/run-loop.test.js +37 -0
- package/dist/sanitize-agent-events.d.ts +24 -0
- package/dist/sanitize-agent-events.d.ts.map +1 -0
- package/dist/sanitize-agent-events.js +88 -0
- package/dist/sanitize-agent-events.test.d.ts +2 -0
- package/dist/sanitize-agent-events.test.d.ts.map +1 -0
- package/dist/sanitize-agent-events.test.js +194 -0
- package/dist/source-platform.test.d.ts +2 -0
- package/dist/source-platform.test.d.ts.map +1 -0
- package/dist/source-platform.test.js +149 -0
- package/dist/testing/fake-adapter.d.ts +8 -1
- package/dist/testing/fake-adapter.d.ts.map +1 -1
- package/dist/testing/fake-adapter.js +30 -1
- package/dist/testing/fake-agent.d.ts +5 -0
- package/dist/testing/fake-agent.d.ts.map +1 -1
- package/dist/testing/fake-agent.js +12 -0
- package/dist/thread-promise-contract.test.d.ts +2 -0
- package/dist/thread-promise-contract.test.d.ts.map +1 -0
- package/dist/thread-promise-contract.test.js +37 -0
- package/dist/thread.d.ts +5 -0
- package/dist/thread.d.ts.map +1 -1
- package/dist/thread.js +338 -243
- 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
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
// await
|
|
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 @@
|
|
|
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
|
|
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
|
|
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
|
package/dist/codec.d.ts.map
CHANGED
|
@@ -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
|
|
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"}
|
package/dist/create-channel.d.ts
CHANGED
|
@@ -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:
|
|
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
|
-
/**
|
|
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;
|