@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.
Files changed (44) hide show
  1. package/dist/attachments/utils.cjs +3 -0
  2. package/dist/attachments/utils.cjs.map +1 -1
  3. package/dist/attachments/utils.d.cts +2 -0
  4. package/dist/attachments/utils.d.cts.map +1 -1
  5. package/dist/attachments/utils.d.mts +2 -0
  6. package/dist/attachments/utils.d.mts.map +1 -1
  7. package/dist/attachments/utils.mjs +3 -0
  8. package/dist/attachments/utils.mjs.map +1 -1
  9. package/dist/finalize-events.cjs +69 -8
  10. package/dist/finalize-events.cjs.map +1 -1
  11. package/dist/finalize-events.d.cts +12 -2
  12. package/dist/finalize-events.d.cts.map +1 -1
  13. package/dist/finalize-events.d.mts +12 -2
  14. package/dist/finalize-events.d.mts.map +1 -1
  15. package/dist/finalize-events.mjs +69 -8
  16. package/dist/finalize-events.mjs.map +1 -1
  17. package/dist/index.cjs +2 -0
  18. package/dist/index.cjs.map +1 -1
  19. package/dist/index.d.cts +2 -1
  20. package/dist/index.d.cts.map +1 -1
  21. package/dist/index.d.mts +2 -1
  22. package/dist/index.d.mts.map +1 -1
  23. package/dist/index.mjs +2 -1
  24. package/dist/index.mjs.map +1 -1
  25. package/dist/index.umd.js +106 -9
  26. package/dist/index.umd.js.map +1 -1
  27. package/dist/intelligence-routing-fields.cjs +35 -0
  28. package/dist/intelligence-routing-fields.cjs.map +1 -0
  29. package/dist/intelligence-routing-fields.d.cts +18 -0
  30. package/dist/intelligence-routing-fields.d.cts.map +1 -0
  31. package/dist/intelligence-routing-fields.d.mts +18 -0
  32. package/dist/intelligence-routing-fields.d.mts.map +1 -0
  33. package/dist/intelligence-routing-fields.mjs +34 -0
  34. package/dist/intelligence-routing-fields.mjs.map +1 -0
  35. package/dist/package.cjs +1 -1
  36. package/dist/package.mjs +1 -1
  37. package/package.json +3 -2
  38. package/src/attachments/__tests__/utils.test.ts +16 -0
  39. package/src/attachments/utils.ts +5 -0
  40. package/src/finalize-events.test.ts +138 -0
  41. package/src/finalize-events.ts +103 -7
  42. package/src/index.ts +1 -0
  43. package/src/intelligence-routing-fields.test.ts +51 -0
  44. 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
@@ -1,6 +1,6 @@
1
1
 
2
2
  //#region package.json
3
- var version = "1.75.2";
3
+ var version = "1.76.0";
4
4
 
5
5
  //#endregion
6
6
  Object.defineProperty(exports, 'version', {
package/dist/package.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "1.75.2";
2
+ var version = "1.76.0";
3
3
 
4
4
  //#endregion
5
5
  export { version };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@copilotkit/shared",
3
- "version": "1.75.2",
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": "0.0.59",
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
+ });
@@ -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(
@@ -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 terminal
28
- * event. Closed lifecycles are forgotten at once and payloads are never kept,
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
- openMessageIds.clear();
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
- appended.push({ type: EventType.RUN_FINISHED } as BaseEvent);
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
- openMessageIds.clear();
142
- openToolCalls.clear();
238
+ clearOpen();
143
239
  return appended;
144
240
  };
145
241
 
package/src/index.ts CHANGED
@@ -40,6 +40,7 @@ export {
40
40
  type FinalizeRunOptions,
41
41
  type RunEventFinalizer,
42
42
  } from "./finalize-events";
43
+ export { stripIntelligenceRoutingFields } from "./intelligence-routing-fields";
43
44
 
44
45
  export {
45
46
  TranscriptionErrorCode,
@@ -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
+ }