@copilotkit/shared 1.75.2 → 1.76.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/dist/attachments/utils.cjs +3 -0
- package/dist/attachments/utils.cjs.map +1 -1
- package/dist/attachments/utils.d.cts +2 -0
- package/dist/attachments/utils.d.cts.map +1 -1
- package/dist/attachments/utils.d.mts +2 -0
- package/dist/attachments/utils.d.mts.map +1 -1
- package/dist/attachments/utils.mjs +3 -0
- package/dist/attachments/utils.mjs.map +1 -1
- package/dist/finalize-events.cjs +69 -8
- package/dist/finalize-events.cjs.map +1 -1
- package/dist/finalize-events.d.cts +12 -2
- package/dist/finalize-events.d.cts.map +1 -1
- package/dist/finalize-events.d.mts +12 -2
- package/dist/finalize-events.d.mts.map +1 -1
- package/dist/finalize-events.mjs +69 -8
- package/dist/finalize-events.mjs.map +1 -1
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +2 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -1
- package/dist/index.mjs.map +1 -1
- package/dist/index.umd.js +106 -9
- package/dist/index.umd.js.map +1 -1
- package/dist/intelligence-routing-fields.cjs +35 -0
- package/dist/intelligence-routing-fields.cjs.map +1 -0
- package/dist/intelligence-routing-fields.d.cts +18 -0
- package/dist/intelligence-routing-fields.d.cts.map +1 -0
- package/dist/intelligence-routing-fields.d.mts +18 -0
- package/dist/intelligence-routing-fields.d.mts.map +1 -0
- package/dist/intelligence-routing-fields.mjs +34 -0
- package/dist/intelligence-routing-fields.mjs.map +1 -0
- package/dist/package.cjs +1 -1
- package/dist/package.mjs +1 -1
- package/package.json +3 -2
- package/src/attachments/__tests__/utils.test.ts +16 -0
- package/src/attachments/utils.ts +5 -0
- package/src/finalize-events.test.ts +138 -0
- package/src/finalize-events.ts +103 -7
- package/src/index.ts +1 -0
- package/src/intelligence-routing-fields.test.ts +51 -0
- package/src/intelligence-routing-fields.ts +40 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
|
|
2
|
+
//#region src/intelligence-routing-fields.ts
|
|
3
|
+
/**
|
|
4
|
+
* The only AG-UI 1.0 events whose schema declares `threadId` and `runId`.
|
|
5
|
+
* Plain literals, not `EventType`: a value import of `@ag-ui/client` here put
|
|
6
|
+
* the client and its zod validators into the initial chunk of every app that
|
|
7
|
+
* imports `@copilotkit/shared`.
|
|
8
|
+
*/
|
|
9
|
+
const RUN_IDENTITY_EVENTS = new Set(["RUN_STARTED", "RUN_FINISHED"]);
|
|
10
|
+
/**
|
|
11
|
+
* Removes the routing fields that the Intelligence runner stamps on every
|
|
12
|
+
* event it sends to the gateway (`thread_id`, `run_id`, and `threadId` /
|
|
13
|
+
* `runId` on events that do not declare them).
|
|
14
|
+
*
|
|
15
|
+
* The gateway relays these payloads unchanged. AG-UI 1.0 strips unknown
|
|
16
|
+
* fields before subscribers see them and warns once per field, per event,
|
|
17
|
+
* so a streamed answer would log hundreds of warnings. Call this where a
|
|
18
|
+
* gateway payload enters an AG-UI pipeline. The gateway still receives the
|
|
19
|
+
* fields, because the runner adds them after this point.
|
|
20
|
+
*/
|
|
21
|
+
function stripIntelligenceRoutingFields(event) {
|
|
22
|
+
const record = event;
|
|
23
|
+
const keepRunIdentity = RUN_IDENTITY_EVENTS.has(record.type);
|
|
24
|
+
if (!("thread_id" in record) && !("run_id" in record) && (keepRunIdentity || !("threadId" in record) && !("runId" in record))) return event;
|
|
25
|
+
const { thread_id: _t, run_id: _r, ...rest } = record;
|
|
26
|
+
if (!keepRunIdentity) {
|
|
27
|
+
delete rest.threadId;
|
|
28
|
+
delete rest.runId;
|
|
29
|
+
}
|
|
30
|
+
return rest;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
//#endregion
|
|
34
|
+
exports.stripIntelligenceRoutingFields = stripIntelligenceRoutingFields;
|
|
35
|
+
//# sourceMappingURL=intelligence-routing-fields.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"intelligence-routing-fields.cjs","names":[],"sources":["../src/intelligence-routing-fields.ts"],"sourcesContent":["import type { BaseEvent } from \"@ag-ui/client\";\n\n/**\n * The only AG-UI 1.0 events whose schema declares `threadId` and `runId`.\n * Plain literals, not `EventType`: a value import of `@ag-ui/client` here put\n * the client and its zod validators into the initial chunk of every app that\n * imports `@copilotkit/shared`.\n */\nconst RUN_IDENTITY_EVENTS = new Set<string>([\"RUN_STARTED\", \"RUN_FINISHED\"]);\n\n/**\n * Removes the routing fields that the Intelligence runner stamps on every\n * event it sends to the gateway (`thread_id`, `run_id`, and `threadId` /\n * `runId` on events that do not declare them).\n *\n * The gateway relays these payloads unchanged. AG-UI 1.0 strips unknown\n * fields before subscribers see them and warns once per field, per event,\n * so a streamed answer would log hundreds of warnings. Call this where a\n * gateway payload enters an AG-UI pipeline. The gateway still receives the\n * fields, because the runner adds them after this point.\n */\nexport function stripIntelligenceRoutingFields<T extends BaseEvent>(\n event: T,\n): T {\n const record = event as T & Record<string, unknown>;\n const keepRunIdentity = RUN_IDENTITY_EVENTS.has(record.type);\n if (\n !(\"thread_id\" in record) &&\n !(\"run_id\" in record) &&\n (keepRunIdentity || (!(\"threadId\" in record) && !(\"runId\" in record)))\n ) {\n return event;\n }\n const { thread_id: _t, run_id: _r, ...rest } = record;\n if (!keepRunIdentity) {\n delete rest.threadId;\n delete rest.runId;\n }\n return rest as unknown as T;\n}\n"],"mappings":";;;;;;;;AAQA,MAAM,sBAAsB,IAAI,IAAY,CAAC,eAAe,eAAe,CAAC;;;;;;;;;;;;AAa5E,SAAgB,+BACd,OACG;CACH,MAAM,SAAS;CACf,MAAM,kBAAkB,oBAAoB,IAAI,OAAO,KAAK;AAC5D,KACE,EAAE,eAAe,WACjB,EAAE,YAAY,YACb,mBAAoB,EAAE,cAAc,WAAW,EAAE,WAAW,SAE7D,QAAO;CAET,MAAM,EAAE,WAAW,IAAI,QAAQ,IAAI,GAAG,SAAS;AAC/C,KAAI,CAAC,iBAAiB;AACpB,SAAO,KAAK;AACZ,SAAO,KAAK;;AAEd,QAAO"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { BaseEvent } from "@ag-ui/client";
|
|
2
|
+
|
|
3
|
+
//#region src/intelligence-routing-fields.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Removes the routing fields that the Intelligence runner stamps on every
|
|
6
|
+
* event it sends to the gateway (`thread_id`, `run_id`, and `threadId` /
|
|
7
|
+
* `runId` on events that do not declare them).
|
|
8
|
+
*
|
|
9
|
+
* The gateway relays these payloads unchanged. AG-UI 1.0 strips unknown
|
|
10
|
+
* fields before subscribers see them and warns once per field, per event,
|
|
11
|
+
* so a streamed answer would log hundreds of warnings. Call this where a
|
|
12
|
+
* gateway payload enters an AG-UI pipeline. The gateway still receives the
|
|
13
|
+
* fields, because the runner adds them after this point.
|
|
14
|
+
*/
|
|
15
|
+
declare function stripIntelligenceRoutingFields<T extends BaseEvent>(event: T): T;
|
|
16
|
+
//#endregion
|
|
17
|
+
export { stripIntelligenceRoutingFields };
|
|
18
|
+
//# sourceMappingURL=intelligence-routing-fields.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"intelligence-routing-fields.d.cts","names":[],"sources":["../src/intelligence-routing-fields.ts"],"mappings":";;;;;AAqBA;;;;;;;;;iBAAgB,8BAAA,WAAyC,SAAA,CAAA,CACvD,KAAA,EAAO,CAAA,GACN,CAAA"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { BaseEvent } from "@ag-ui/client";
|
|
2
|
+
|
|
3
|
+
//#region src/intelligence-routing-fields.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Removes the routing fields that the Intelligence runner stamps on every
|
|
6
|
+
* event it sends to the gateway (`thread_id`, `run_id`, and `threadId` /
|
|
7
|
+
* `runId` on events that do not declare them).
|
|
8
|
+
*
|
|
9
|
+
* The gateway relays these payloads unchanged. AG-UI 1.0 strips unknown
|
|
10
|
+
* fields before subscribers see them and warns once per field, per event,
|
|
11
|
+
* so a streamed answer would log hundreds of warnings. Call this where a
|
|
12
|
+
* gateway payload enters an AG-UI pipeline. The gateway still receives the
|
|
13
|
+
* fields, because the runner adds them after this point.
|
|
14
|
+
*/
|
|
15
|
+
declare function stripIntelligenceRoutingFields<T extends BaseEvent>(event: T): T;
|
|
16
|
+
//#endregion
|
|
17
|
+
export { stripIntelligenceRoutingFields };
|
|
18
|
+
//# sourceMappingURL=intelligence-routing-fields.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"intelligence-routing-fields.d.mts","names":[],"sources":["../src/intelligence-routing-fields.ts"],"mappings":";;;;;AAqBA;;;;;;;;;iBAAgB,8BAAA,WAAyC,SAAA,CAAA,CACvD,KAAA,EAAO,CAAA,GACN,CAAA"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
//#region src/intelligence-routing-fields.ts
|
|
2
|
+
/**
|
|
3
|
+
* The only AG-UI 1.0 events whose schema declares `threadId` and `runId`.
|
|
4
|
+
* Plain literals, not `EventType`: a value import of `@ag-ui/client` here put
|
|
5
|
+
* the client and its zod validators into the initial chunk of every app that
|
|
6
|
+
* imports `@copilotkit/shared`.
|
|
7
|
+
*/
|
|
8
|
+
const RUN_IDENTITY_EVENTS = new Set(["RUN_STARTED", "RUN_FINISHED"]);
|
|
9
|
+
/**
|
|
10
|
+
* Removes the routing fields that the Intelligence runner stamps on every
|
|
11
|
+
* event it sends to the gateway (`thread_id`, `run_id`, and `threadId` /
|
|
12
|
+
* `runId` on events that do not declare them).
|
|
13
|
+
*
|
|
14
|
+
* The gateway relays these payloads unchanged. AG-UI 1.0 strips unknown
|
|
15
|
+
* fields before subscribers see them and warns once per field, per event,
|
|
16
|
+
* so a streamed answer would log hundreds of warnings. Call this where a
|
|
17
|
+
* gateway payload enters an AG-UI pipeline. The gateway still receives the
|
|
18
|
+
* fields, because the runner adds them after this point.
|
|
19
|
+
*/
|
|
20
|
+
function stripIntelligenceRoutingFields(event) {
|
|
21
|
+
const record = event;
|
|
22
|
+
const keepRunIdentity = RUN_IDENTITY_EVENTS.has(record.type);
|
|
23
|
+
if (!("thread_id" in record) && !("run_id" in record) && (keepRunIdentity || !("threadId" in record) && !("runId" in record))) return event;
|
|
24
|
+
const { thread_id: _t, run_id: _r, ...rest } = record;
|
|
25
|
+
if (!keepRunIdentity) {
|
|
26
|
+
delete rest.threadId;
|
|
27
|
+
delete rest.runId;
|
|
28
|
+
}
|
|
29
|
+
return rest;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
//#endregion
|
|
33
|
+
export { stripIntelligenceRoutingFields };
|
|
34
|
+
//# sourceMappingURL=intelligence-routing-fields.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"intelligence-routing-fields.mjs","names":[],"sources":["../src/intelligence-routing-fields.ts"],"sourcesContent":["import type { BaseEvent } from \"@ag-ui/client\";\n\n/**\n * The only AG-UI 1.0 events whose schema declares `threadId` and `runId`.\n * Plain literals, not `EventType`: a value import of `@ag-ui/client` here put\n * the client and its zod validators into the initial chunk of every app that\n * imports `@copilotkit/shared`.\n */\nconst RUN_IDENTITY_EVENTS = new Set<string>([\"RUN_STARTED\", \"RUN_FINISHED\"]);\n\n/**\n * Removes the routing fields that the Intelligence runner stamps on every\n * event it sends to the gateway (`thread_id`, `run_id`, and `threadId` /\n * `runId` on events that do not declare them).\n *\n * The gateway relays these payloads unchanged. AG-UI 1.0 strips unknown\n * fields before subscribers see them and warns once per field, per event,\n * so a streamed answer would log hundreds of warnings. Call this where a\n * gateway payload enters an AG-UI pipeline. The gateway still receives the\n * fields, because the runner adds them after this point.\n */\nexport function stripIntelligenceRoutingFields<T extends BaseEvent>(\n event: T,\n): T {\n const record = event as T & Record<string, unknown>;\n const keepRunIdentity = RUN_IDENTITY_EVENTS.has(record.type);\n if (\n !(\"thread_id\" in record) &&\n !(\"run_id\" in record) &&\n (keepRunIdentity || (!(\"threadId\" in record) && !(\"runId\" in record)))\n ) {\n return event;\n }\n const { thread_id: _t, run_id: _r, ...rest } = record;\n if (!keepRunIdentity) {\n delete rest.threadId;\n delete rest.runId;\n }\n return rest as unknown as T;\n}\n"],"mappings":";;;;;;;AAQA,MAAM,sBAAsB,IAAI,IAAY,CAAC,eAAe,eAAe,CAAC;;;;;;;;;;;;AAa5E,SAAgB,+BACd,OACG;CACH,MAAM,SAAS;CACf,MAAM,kBAAkB,oBAAoB,IAAI,OAAO,KAAK;AAC5D,KACE,EAAE,eAAe,WACjB,EAAE,YAAY,YACb,mBAAoB,EAAE,cAAc,WAAW,EAAE,WAAW,SAE7D,QAAO;CAET,MAAM,EAAE,WAAW,IAAI,QAAQ,IAAI,GAAG,SAAS;AAC/C,KAAI,CAAC,iBAAiB;AACpB,SAAO,KAAK;AACZ,SAAO,KAAK;;AAEd,QAAO"}
|
package/dist/package.cjs
CHANGED
package/dist/package.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@copilotkit/shared",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.76.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
59
|
"@ag-ui/a2ui-middleware": "0.0.10",
|
|
60
|
-
"@ag-ui/client": "
|
|
60
|
+
"@ag-ui/client": "1.0.1",
|
|
61
61
|
"@copilotkit/license-verifier": "~0.5.0",
|
|
62
62
|
"@segment/analytics-node": "^2.1.2",
|
|
63
63
|
"@standard-schema/spec": "^1.0.0",
|
|
@@ -71,6 +71,7 @@
|
|
|
71
71
|
"zod-to-json-schema": "^3.23.5"
|
|
72
72
|
},
|
|
73
73
|
"devDependencies": {
|
|
74
|
+
"@ag-ui/core": "1.0.1",
|
|
74
75
|
"@types/clarinet": "^0.12.3",
|
|
75
76
|
"@types/uuid": "^10.0.0",
|
|
76
77
|
"@valibot/to-json-schema": "^1.5.0",
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
formatFileSize,
|
|
4
4
|
exceedsMaxSize,
|
|
5
5
|
matchesAcceptFilter,
|
|
6
|
+
getSourceUrl,
|
|
6
7
|
} from "../utils";
|
|
7
8
|
|
|
8
9
|
// ---------------------------------------------------------------------------
|
|
@@ -196,3 +197,18 @@ describe("matchesAcceptFilter", () => {
|
|
|
196
197
|
expect(matchesAcceptFilter(mockFileWithType(""), "image/*")).toBe(false);
|
|
197
198
|
});
|
|
198
199
|
});
|
|
200
|
+
|
|
201
|
+
describe("getSourceUrl", () => {
|
|
202
|
+
it("returns a URL source as is and builds a data URL for inline data", () => {
|
|
203
|
+
expect(getSourceUrl({ type: "url", value: "https://x.test/a.png" })).toBe(
|
|
204
|
+
"https://x.test/a.png",
|
|
205
|
+
);
|
|
206
|
+
expect(
|
|
207
|
+
getSourceUrl({ type: "data", value: "aGk=", mimeType: "image/png" }),
|
|
208
|
+
).toBe("data:image/png;base64,aGk=");
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("returns no address for a provider file handle", () => {
|
|
212
|
+
expect(getSourceUrl({ type: "file", value: "file-abc123" })).toBe("");
|
|
213
|
+
});
|
|
214
|
+
});
|
package/src/attachments/utils.ts
CHANGED
|
@@ -142,11 +142,16 @@ export function matchesAcceptFilter(file: File, accept: string): boolean {
|
|
|
142
142
|
/**
|
|
143
143
|
* Convert an InputContentSource to a usable URL string.
|
|
144
144
|
* For data sources, returns a base64 data URL; for URL sources, returns the URL directly.
|
|
145
|
+
* A provider file handle (AG-UI 1.0 `file` source) has no address a browser
|
|
146
|
+
* can load, so it returns an empty string and the caller shows no preview.
|
|
145
147
|
*/
|
|
146
148
|
export function getSourceUrl(source: InputContentSource): string {
|
|
147
149
|
if (source.type === "url") {
|
|
148
150
|
return source.value;
|
|
149
151
|
}
|
|
152
|
+
if (source.type === "file") {
|
|
153
|
+
return "";
|
|
154
|
+
}
|
|
150
155
|
return `data:${source.mimeType};base64,${source.value}`;
|
|
151
156
|
}
|
|
152
157
|
|
|
@@ -57,6 +57,49 @@ describe("createRunEventFinalizer", () => {
|
|
|
57
57
|
},
|
|
58
58
|
);
|
|
59
59
|
|
|
60
|
+
it("closes open reasoning, message before span, because AG-UI 1.0 requires it", () => {
|
|
61
|
+
const finalizer = createRunEventFinalizer();
|
|
62
|
+
for (const value of [
|
|
63
|
+
event({ type: EventType.REASONING_START, messageId: "span-1" }),
|
|
64
|
+
event({ type: EventType.REASONING_MESSAGE_START, messageId: "r-1" }),
|
|
65
|
+
event({ type: EventType.REASONING_START, messageId: "span-2" }),
|
|
66
|
+
event({ type: EventType.REASONING_END, messageId: "span-2" }),
|
|
67
|
+
]) {
|
|
68
|
+
finalizer.observe(value);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
expect(finalizer.finalize({ stopRequested: true }).slice(0, 2)).toEqual([
|
|
72
|
+
{ type: EventType.REASONING_MESSAGE_END, messageId: "r-1" },
|
|
73
|
+
{ type: EventType.REASONING_END, messageId: "span-1" },
|
|
74
|
+
]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("stamps the run identity on a stop and marks it cancelled for a 1.0 client", () => {
|
|
78
|
+
const started = event({
|
|
79
|
+
type: EventType.RUN_STARTED,
|
|
80
|
+
threadId: "thread-1",
|
|
81
|
+
runId: "run-1",
|
|
82
|
+
});
|
|
83
|
+
const stopFor = (protocolVersion?: string) => {
|
|
84
|
+
const finalizer = createRunEventFinalizer();
|
|
85
|
+
finalizer.observe(started);
|
|
86
|
+
return finalizer.finalize({ stopRequested: true, protocolVersion });
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
expect(stopFor("1.0")).toEqual([
|
|
90
|
+
{
|
|
91
|
+
type: EventType.RUN_FINISHED,
|
|
92
|
+
threadId: "thread-1",
|
|
93
|
+
runId: "run-1",
|
|
94
|
+
outcome: { type: "cancelled" },
|
|
95
|
+
},
|
|
96
|
+
]);
|
|
97
|
+
// A client without a declared version predates 1.0 and cannot parse it.
|
|
98
|
+
expect(stopFor(undefined)).toEqual([
|
|
99
|
+
{ type: EventType.RUN_FINISHED, threadId: "thread-1", runId: "run-1" },
|
|
100
|
+
]);
|
|
101
|
+
});
|
|
102
|
+
|
|
60
103
|
it("forgets a lifecycle as soon as it closes", () => {
|
|
61
104
|
const finalizer = createRunEventFinalizer();
|
|
62
105
|
for (let index = 0; index < 1000; index += 1) {
|
|
@@ -102,6 +145,101 @@ describe("createRunEventFinalizer", () => {
|
|
|
102
145
|
]);
|
|
103
146
|
});
|
|
104
147
|
|
|
148
|
+
it("closes open subagents on a stop, children first, after their messages and before the terminal", () => {
|
|
149
|
+
const finalizer = createRunEventFinalizer();
|
|
150
|
+
for (const value of [
|
|
151
|
+
event({
|
|
152
|
+
type: EventType.SUBAGENT_STARTED,
|
|
153
|
+
subagentRunId: "parent",
|
|
154
|
+
name: "researcher",
|
|
155
|
+
}),
|
|
156
|
+
event({
|
|
157
|
+
type: EventType.SUBAGENT_STARTED,
|
|
158
|
+
subagentRunId: "child",
|
|
159
|
+
name: "searcher",
|
|
160
|
+
parentSubagentRunId: "parent",
|
|
161
|
+
}),
|
|
162
|
+
event({
|
|
163
|
+
type: EventType.TEXT_MESSAGE_START,
|
|
164
|
+
messageId: "message-1",
|
|
165
|
+
subagentRunId: "child",
|
|
166
|
+
}),
|
|
167
|
+
]) {
|
|
168
|
+
finalizer.observe(value);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
expect(
|
|
172
|
+
finalizer.finalize({
|
|
173
|
+
stopRequested: true,
|
|
174
|
+
interruptionMessage: "Stopped from the toolbar",
|
|
175
|
+
}),
|
|
176
|
+
).toEqual([
|
|
177
|
+
{ type: EventType.TEXT_MESSAGE_END, messageId: "message-1" },
|
|
178
|
+
{
|
|
179
|
+
type: EventType.SUBAGENT_ERROR,
|
|
180
|
+
subagentRunId: "child",
|
|
181
|
+
message: "Stopped from the toolbar",
|
|
182
|
+
code: "CANCELLED",
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
type: EventType.SUBAGENT_ERROR,
|
|
186
|
+
subagentRunId: "parent",
|
|
187
|
+
message: "Stopped from the toolbar",
|
|
188
|
+
code: "CANCELLED",
|
|
189
|
+
},
|
|
190
|
+
{ type: EventType.RUN_FINISHED },
|
|
191
|
+
]);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it.each([EventType.SUBAGENT_FINISHED, EventType.SUBAGENT_ERROR])(
|
|
195
|
+
"does not close a subagent again after %s",
|
|
196
|
+
(closer) => {
|
|
197
|
+
const finalizer = createRunEventFinalizer();
|
|
198
|
+
finalizer.observe(
|
|
199
|
+
event({
|
|
200
|
+
type: EventType.SUBAGENT_STARTED,
|
|
201
|
+
subagentRunId: "done",
|
|
202
|
+
name: "writer",
|
|
203
|
+
}),
|
|
204
|
+
);
|
|
205
|
+
finalizer.observe(
|
|
206
|
+
event({ type: closer, subagentRunId: "done", message: "boom" }),
|
|
207
|
+
);
|
|
208
|
+
finalizer.observe(
|
|
209
|
+
event({
|
|
210
|
+
type: EventType.SUBAGENT_STARTED,
|
|
211
|
+
subagentRunId: "open",
|
|
212
|
+
name: "critic",
|
|
213
|
+
}),
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
expect(finalizer.finalize({ stopRequested: true })).toEqual([
|
|
217
|
+
{
|
|
218
|
+
type: EventType.SUBAGENT_ERROR,
|
|
219
|
+
subagentRunId: "open",
|
|
220
|
+
message: "Run stopped by user",
|
|
221
|
+
code: "CANCELLED",
|
|
222
|
+
},
|
|
223
|
+
{ type: EventType.RUN_FINISHED },
|
|
224
|
+
]);
|
|
225
|
+
},
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
it("leaves open subagents unclosed when the stream ends abruptly, because RUN_ERROR abandons them", () => {
|
|
229
|
+
const finalizer = createRunEventFinalizer();
|
|
230
|
+
finalizer.observe(
|
|
231
|
+
event({
|
|
232
|
+
type: EventType.SUBAGENT_STARTED,
|
|
233
|
+
subagentRunId: "open",
|
|
234
|
+
name: "critic",
|
|
235
|
+
}),
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
expect(finalizer.finalize().map(({ type }) => type)).toEqual([
|
|
239
|
+
EventType.RUN_ERROR,
|
|
240
|
+
]);
|
|
241
|
+
});
|
|
242
|
+
|
|
105
243
|
it("ignores start events without an id so finalize never emits an empty id", () => {
|
|
106
244
|
const finalizer = createRunEventFinalizer();
|
|
107
245
|
finalizer.observe(
|
package/src/finalize-events.ts
CHANGED
|
@@ -4,6 +4,13 @@ import { EventType } from "@ag-ui/client";
|
|
|
4
4
|
export interface FinalizeRunOptions {
|
|
5
5
|
stopRequested?: boolean;
|
|
6
6
|
interruptionMessage?: string;
|
|
7
|
+
/**
|
|
8
|
+
* The `protocolVersion` the client declared on its RunAgentInput. When it is
|
|
9
|
+
* set, a stopped run finishes with the AG-UI 1.0 `cancelled` outcome. A
|
|
10
|
+
* client that declares no version predates 1.0 and cannot parse that
|
|
11
|
+
* outcome, so it gets the plain RUN_FINISHED it always got.
|
|
12
|
+
*/
|
|
13
|
+
protocolVersion?: string;
|
|
7
14
|
}
|
|
8
15
|
|
|
9
16
|
export interface RunEventFinalizer {
|
|
@@ -24,19 +31,64 @@ interface OpenToolCall {
|
|
|
24
31
|
/**
|
|
25
32
|
* Incremental finalizer for a streamed AG-UI run. Feed every event to
|
|
26
33
|
* `observe`; when the stream ends without a terminal event, `finalize` returns
|
|
27
|
-
* the closers for text messages and tool calls still open plus a
|
|
28
|
-
* event.
|
|
34
|
+
* the closers for reasoning, text messages and tool calls still open plus a
|
|
35
|
+
* terminal event. AG-UI 1.0 fails a run that ends with reasoning still open,
|
|
36
|
+
* so reasoning is closed too. It also fails a RUN_FINISHED while a subagent
|
|
37
|
+
* is still open, so a stop closes open subagents as well; a RUN_ERROR
|
|
38
|
+
* abandons them, which the protocol allows. Closed lifecycles are forgotten at once and payloads are never kept,
|
|
29
39
|
* so a caller does not have to retain the event array for this purpose.
|
|
30
40
|
*/
|
|
31
41
|
export function createRunEventFinalizer(): RunEventFinalizer {
|
|
32
42
|
const openMessageIds = new Set<string>();
|
|
43
|
+
const openReasoningMessageIds = new Set<string>();
|
|
44
|
+
const openReasoningSpanIds = new Set<string>();
|
|
33
45
|
const openToolCalls = new Map<string, OpenToolCall>();
|
|
46
|
+
// Insertion order is start order, so closing in reverse closes children first.
|
|
47
|
+
const openSubagentRunIds = new Set<string>();
|
|
48
|
+
let runIdentity: { threadId?: string; runId?: string } = {};
|
|
34
49
|
let terminalEventObserved = false;
|
|
35
50
|
|
|
51
|
+
const clearOpen = () => {
|
|
52
|
+
openMessageIds.clear();
|
|
53
|
+
openReasoningMessageIds.clear();
|
|
54
|
+
openReasoningSpanIds.clear();
|
|
55
|
+
openToolCalls.clear();
|
|
56
|
+
openSubagentRunIds.clear();
|
|
57
|
+
};
|
|
58
|
+
|
|
36
59
|
const observe = (event: BaseEvent) => {
|
|
37
60
|
if (terminalEventObserved) return;
|
|
38
61
|
|
|
39
62
|
switch (event.type) {
|
|
63
|
+
case EventType.RUN_STARTED: {
|
|
64
|
+
const { threadId, runId } = event as {
|
|
65
|
+
threadId?: string;
|
|
66
|
+
runId?: string;
|
|
67
|
+
};
|
|
68
|
+
runIdentity = { threadId, runId };
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
case EventType.REASONING_START:
|
|
72
|
+
case EventType.REASONING_END:
|
|
73
|
+
case EventType.REASONING_MESSAGE_START:
|
|
74
|
+
case EventType.REASONING_MESSAGE_END: {
|
|
75
|
+
const messageId = (event as { messageId?: string }).messageId;
|
|
76
|
+
if (typeof messageId !== "string") break;
|
|
77
|
+
const open =
|
|
78
|
+
event.type === EventType.REASONING_START ||
|
|
79
|
+
event.type === EventType.REASONING_END
|
|
80
|
+
? openReasoningSpanIds
|
|
81
|
+
: openReasoningMessageIds;
|
|
82
|
+
if (
|
|
83
|
+
event.type === EventType.REASONING_START ||
|
|
84
|
+
event.type === EventType.REASONING_MESSAGE_START
|
|
85
|
+
) {
|
|
86
|
+
open.add(messageId);
|
|
87
|
+
} else {
|
|
88
|
+
open.delete(messageId);
|
|
89
|
+
}
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
40
92
|
case EventType.TEXT_MESSAGE_START: {
|
|
41
93
|
const messageId = (event as { messageId?: string }).messageId;
|
|
42
94
|
if (messageId) openMessageIds.add(messageId);
|
|
@@ -66,11 +118,23 @@ export function createRunEventFinalizer(): RunEventFinalizer {
|
|
|
66
118
|
if (info.hasEnd && info.hasResult) openToolCalls.delete(toolCallId);
|
|
67
119
|
break;
|
|
68
120
|
}
|
|
121
|
+
case EventType.SUBAGENT_STARTED:
|
|
122
|
+
case EventType.SUBAGENT_FINISHED:
|
|
123
|
+
case EventType.SUBAGENT_ERROR: {
|
|
124
|
+
const subagentRunId = (event as { subagentRunId?: string })
|
|
125
|
+
.subagentRunId;
|
|
126
|
+
if (!subagentRunId) break;
|
|
127
|
+
if (event.type === EventType.SUBAGENT_STARTED) {
|
|
128
|
+
openSubagentRunIds.add(subagentRunId);
|
|
129
|
+
} else {
|
|
130
|
+
openSubagentRunIds.delete(subagentRunId);
|
|
131
|
+
}
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
69
134
|
case EventType.RUN_FINISHED:
|
|
70
135
|
case EventType.RUN_ERROR:
|
|
71
136
|
terminalEventObserved = true;
|
|
72
|
-
|
|
73
|
-
openToolCalls.clear();
|
|
137
|
+
clearOpen();
|
|
74
138
|
break;
|
|
75
139
|
default:
|
|
76
140
|
break;
|
|
@@ -88,6 +152,17 @@ export function createRunEventFinalizer(): RunEventFinalizer {
|
|
|
88
152
|
: defaultAbruptEndMessage;
|
|
89
153
|
const appended: BaseEvent[] = [];
|
|
90
154
|
|
|
155
|
+
// A reasoning message closes before the span that holds it.
|
|
156
|
+
for (const messageId of openReasoningMessageIds) {
|
|
157
|
+
appended.push({
|
|
158
|
+
type: EventType.REASONING_MESSAGE_END,
|
|
159
|
+
messageId,
|
|
160
|
+
} as BaseEvent);
|
|
161
|
+
}
|
|
162
|
+
for (const messageId of openReasoningSpanIds) {
|
|
163
|
+
appended.push({ type: EventType.REASONING_END, messageId } as BaseEvent);
|
|
164
|
+
}
|
|
165
|
+
|
|
91
166
|
for (const messageId of openMessageIds) {
|
|
92
167
|
appended.push({
|
|
93
168
|
type: EventType.TEXT_MESSAGE_END,
|
|
@@ -127,7 +202,29 @@ export function createRunEventFinalizer(): RunEventFinalizer {
|
|
|
127
202
|
}
|
|
128
203
|
|
|
129
204
|
if (stopRequested) {
|
|
130
|
-
|
|
205
|
+
// Newest first, so a nested subagent closes before its parent. An index
|
|
206
|
+
// loop, because `toReversed` is ES2023 and this package targets older.
|
|
207
|
+
const openIds = [...openSubagentRunIds];
|
|
208
|
+
for (let index = openIds.length - 1; index >= 0; index -= 1) {
|
|
209
|
+
appended.push({
|
|
210
|
+
type: EventType.SUBAGENT_ERROR,
|
|
211
|
+
subagentRunId: openIds[index],
|
|
212
|
+
message: resolvedStopMessage,
|
|
213
|
+
code: "CANCELLED",
|
|
214
|
+
} as BaseEvent);
|
|
215
|
+
}
|
|
216
|
+
appended.push({
|
|
217
|
+
type: EventType.RUN_FINISHED,
|
|
218
|
+
...(runIdentity.threadId !== undefined
|
|
219
|
+
? { threadId: runIdentity.threadId }
|
|
220
|
+
: {}),
|
|
221
|
+
...(runIdentity.runId !== undefined
|
|
222
|
+
? { runId: runIdentity.runId }
|
|
223
|
+
: {}),
|
|
224
|
+
...(options.protocolVersion !== undefined
|
|
225
|
+
? { outcome: { type: "cancelled" } }
|
|
226
|
+
: {}),
|
|
227
|
+
} as BaseEvent);
|
|
131
228
|
} else {
|
|
132
229
|
const errorEvent: RunErrorEvent = {
|
|
133
230
|
type: EventType.RUN_ERROR,
|
|
@@ -138,8 +235,7 @@ export function createRunEventFinalizer(): RunEventFinalizer {
|
|
|
138
235
|
}
|
|
139
236
|
|
|
140
237
|
terminalEventObserved = true;
|
|
141
|
-
|
|
142
|
-
openToolCalls.clear();
|
|
238
|
+
clearOpen();
|
|
143
239
|
return appended;
|
|
144
240
|
};
|
|
145
241
|
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { BaseEvent } from "@ag-ui/client";
|
|
2
|
+
import { EventType } from "@ag-ui/client";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import { stripIntelligenceRoutingFields } from "./intelligence-routing-fields";
|
|
5
|
+
|
|
6
|
+
const stamped = {
|
|
7
|
+
threadId: "t1",
|
|
8
|
+
runId: "r1",
|
|
9
|
+
thread_id: "t1",
|
|
10
|
+
run_id: "r1",
|
|
11
|
+
metadata: { cpki_event_id: "e1" },
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
describe("stripIntelligenceRoutingFields", () => {
|
|
15
|
+
it("removes all routing fields from an event that does not declare them", () => {
|
|
16
|
+
const event = {
|
|
17
|
+
type: EventType.TEXT_MESSAGE_CONTENT,
|
|
18
|
+
messageId: "m1",
|
|
19
|
+
delta: "hi",
|
|
20
|
+
...stamped,
|
|
21
|
+
} as BaseEvent;
|
|
22
|
+
|
|
23
|
+
expect(stripIntelligenceRoutingFields(event)).toEqual({
|
|
24
|
+
type: EventType.TEXT_MESSAGE_CONTENT,
|
|
25
|
+
messageId: "m1",
|
|
26
|
+
delta: "hi",
|
|
27
|
+
metadata: { cpki_event_id: "e1" },
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("keeps threadId and runId on the run events that declare them", () => {
|
|
32
|
+
const event = { type: EventType.RUN_FINISHED, ...stamped } as BaseEvent;
|
|
33
|
+
|
|
34
|
+
expect(stripIntelligenceRoutingFields(event)).toEqual({
|
|
35
|
+
type: EventType.RUN_FINISHED,
|
|
36
|
+
threadId: "t1",
|
|
37
|
+
runId: "r1",
|
|
38
|
+
metadata: { cpki_event_id: "e1" },
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("returns an event without routing fields unchanged", () => {
|
|
43
|
+
const event = {
|
|
44
|
+
type: EventType.RUN_STARTED,
|
|
45
|
+
threadId: "t1",
|
|
46
|
+
runId: "r1",
|
|
47
|
+
} as BaseEvent;
|
|
48
|
+
|
|
49
|
+
expect(stripIntelligenceRoutingFields(event)).toBe(event);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { BaseEvent } from "@ag-ui/client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The only AG-UI 1.0 events whose schema declares `threadId` and `runId`.
|
|
5
|
+
* Plain literals, not `EventType`: a value import of `@ag-ui/client` here put
|
|
6
|
+
* the client and its zod validators into the initial chunk of every app that
|
|
7
|
+
* imports `@copilotkit/shared`.
|
|
8
|
+
*/
|
|
9
|
+
const RUN_IDENTITY_EVENTS = new Set<string>(["RUN_STARTED", "RUN_FINISHED"]);
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Removes the routing fields that the Intelligence runner stamps on every
|
|
13
|
+
* event it sends to the gateway (`thread_id`, `run_id`, and `threadId` /
|
|
14
|
+
* `runId` on events that do not declare them).
|
|
15
|
+
*
|
|
16
|
+
* The gateway relays these payloads unchanged. AG-UI 1.0 strips unknown
|
|
17
|
+
* fields before subscribers see them and warns once per field, per event,
|
|
18
|
+
* so a streamed answer would log hundreds of warnings. Call this where a
|
|
19
|
+
* gateway payload enters an AG-UI pipeline. The gateway still receives the
|
|
20
|
+
* fields, because the runner adds them after this point.
|
|
21
|
+
*/
|
|
22
|
+
export function stripIntelligenceRoutingFields<T extends BaseEvent>(
|
|
23
|
+
event: T,
|
|
24
|
+
): T {
|
|
25
|
+
const record = event as T & Record<string, unknown>;
|
|
26
|
+
const keepRunIdentity = RUN_IDENTITY_EVENTS.has(record.type);
|
|
27
|
+
if (
|
|
28
|
+
!("thread_id" in record) &&
|
|
29
|
+
!("run_id" in record) &&
|
|
30
|
+
(keepRunIdentity || (!("threadId" in record) && !("runId" in record)))
|
|
31
|
+
) {
|
|
32
|
+
return event;
|
|
33
|
+
}
|
|
34
|
+
const { thread_id: _t, run_id: _r, ...rest } = record;
|
|
35
|
+
if (!keepRunIdentity) {
|
|
36
|
+
delete rest.threadId;
|
|
37
|
+
delete rest.runId;
|
|
38
|
+
}
|
|
39
|
+
return rest as unknown as T;
|
|
40
|
+
}
|