@copilotkitnext/agent 1.51.4 → 1.51.5-next.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.
@@ -13,6 +13,9 @@ import {
13
13
  toolCallDelta,
14
14
  toolCall,
15
15
  toolResult,
16
+ reasoningStart,
17
+ reasoningDelta,
18
+ reasoningEnd,
16
19
  } from "./test-helpers";
17
20
 
18
21
  // Mock the ai module
@@ -818,4 +821,428 @@ describe("BasicAgent", () => {
818
821
  }
819
822
  });
820
823
  });
824
+
825
+ describe("Reasoning Event Emission", () => {
826
+ it("should emit full reasoning lifecycle events", async () => {
827
+ const agent = new BasicAgent({
828
+ model: "openai/gpt-4o",
829
+ });
830
+
831
+ vi.mocked(streamText).mockReturnValue(
832
+ mockStreamTextResponse([
833
+ reasoningStart(),
834
+ reasoningDelta("Let me think..."),
835
+ reasoningDelta(" about this."),
836
+ reasoningEnd(),
837
+ finish(),
838
+ ]) as any,
839
+ );
840
+
841
+ const input: RunAgentInput = {
842
+ threadId: "thread1",
843
+ runId: "run1",
844
+ messages: [],
845
+ tools: [],
846
+ context: [],
847
+ state: {},
848
+ };
849
+
850
+ const events = await collectEvents(agent["run"](input));
851
+
852
+ // Verify event order
853
+ const eventTypes = events.map((e: any) => e.type);
854
+ expect(eventTypes[0]).toBe(EventType.RUN_STARTED);
855
+
856
+ const reasoningStartIdx = eventTypes.indexOf(EventType.REASONING_START);
857
+ const reasoningMsgStartIdx = eventTypes.indexOf(
858
+ EventType.REASONING_MESSAGE_START,
859
+ );
860
+ const reasoningContentIndices = eventTypes.reduce(
861
+ (acc: number[], type: string, idx: number) =>
862
+ type === EventType.REASONING_MESSAGE_CONTENT ? [...acc, idx] : acc,
863
+ [],
864
+ );
865
+ const reasoningMsgEndIdx = eventTypes.indexOf(
866
+ EventType.REASONING_MESSAGE_END,
867
+ );
868
+ const reasoningEndIdx = eventTypes.indexOf(EventType.REASONING_END);
869
+
870
+ expect(reasoningStartIdx).toBeGreaterThan(0);
871
+ expect(reasoningMsgStartIdx).toBeGreaterThan(reasoningStartIdx);
872
+ expect(reasoningContentIndices).toHaveLength(2);
873
+ expect(reasoningContentIndices[0]).toBeGreaterThan(reasoningMsgStartIdx);
874
+ expect(reasoningMsgEndIdx).toBeGreaterThan(
875
+ reasoningContentIndices[reasoningContentIndices.length - 1],
876
+ );
877
+ expect(reasoningEndIdx).toBeGreaterThan(reasoningMsgEndIdx);
878
+
879
+ // Verify consistent messageId across all reasoning events
880
+ const reasoningEvents = events.filter((e: any) =>
881
+ [
882
+ EventType.REASONING_START,
883
+ EventType.REASONING_MESSAGE_START,
884
+ EventType.REASONING_MESSAGE_CONTENT,
885
+ EventType.REASONING_MESSAGE_END,
886
+ EventType.REASONING_END,
887
+ ].includes(e.type),
888
+ );
889
+ const messageIds = reasoningEvents.map((e: any) => e.messageId);
890
+ expect(new Set(messageIds).size).toBe(1);
891
+
892
+ // Verify REASONING_MESSAGE_START has role "reasoning"
893
+ const msgStartEvent = events.find(
894
+ (e: any) => e.type === EventType.REASONING_MESSAGE_START,
895
+ );
896
+ expect(msgStartEvent).toMatchObject({ role: "reasoning" });
897
+
898
+ // Verify content deltas
899
+ const contentEvents = events.filter(
900
+ (e: any) => e.type === EventType.REASONING_MESSAGE_CONTENT,
901
+ );
902
+ expect(contentEvents[0]).toMatchObject({ delta: "Let me think..." });
903
+ expect(contentEvents[1]).toMatchObject({ delta: " about this." });
904
+
905
+ // Verify last event is RUN_FINISHED
906
+ expect(eventTypes[eventTypes.length - 1]).toBe(EventType.RUN_FINISHED);
907
+ });
908
+
909
+ it("should emit reasoning events followed by text events", async () => {
910
+ const agent = new BasicAgent({
911
+ model: "openai/gpt-4o",
912
+ });
913
+
914
+ vi.mocked(streamText).mockReturnValue(
915
+ mockStreamTextResponse([
916
+ reasoningStart(),
917
+ reasoningDelta("thinking"),
918
+ reasoningEnd(),
919
+ textDelta("Hello"),
920
+ finish(),
921
+ ]) as any,
922
+ );
923
+
924
+ const input: RunAgentInput = {
925
+ threadId: "thread1",
926
+ runId: "run1",
927
+ messages: [],
928
+ tools: [],
929
+ context: [],
930
+ state: {},
931
+ };
932
+
933
+ const events = await collectEvents(agent["run"](input));
934
+ const eventTypes = events.map((e: any) => e.type);
935
+
936
+ // Reasoning events should come before text events
937
+ const reasoningEndIdx = eventTypes.indexOf(EventType.REASONING_END);
938
+ const textChunkIdx = eventTypes.indexOf(EventType.TEXT_MESSAGE_CHUNK);
939
+ expect(reasoningEndIdx).toBeLessThan(textChunkIdx);
940
+
941
+ // Reasoning messageId should differ from text messageId
942
+ const reasoningEvent = events.find(
943
+ (e: any) => e.type === EventType.REASONING_START,
944
+ );
945
+ const textEvent = events.find(
946
+ (e: any) => e.type === EventType.TEXT_MESSAGE_CHUNK,
947
+ );
948
+ expect(reasoningEvent.messageId).not.toBe(textEvent.messageId);
949
+ });
950
+
951
+ it("should use provider-supplied reasoning id", async () => {
952
+ const agent = new BasicAgent({
953
+ model: "openai/gpt-4o",
954
+ });
955
+
956
+ vi.mocked(streamText).mockReturnValue(
957
+ mockStreamTextResponse([
958
+ reasoningStart("reasoning-msg-123"),
959
+ reasoningDelta("content"),
960
+ reasoningEnd(),
961
+ finish(),
962
+ ]) as any,
963
+ );
964
+
965
+ const input: RunAgentInput = {
966
+ threadId: "thread1",
967
+ runId: "run1",
968
+ messages: [],
969
+ tools: [],
970
+ context: [],
971
+ state: {},
972
+ };
973
+
974
+ const events = await collectEvents(agent["run"](input));
975
+
976
+ const reasoningEvents = events.filter((e: any) =>
977
+ [
978
+ EventType.REASONING_START,
979
+ EventType.REASONING_MESSAGE_START,
980
+ EventType.REASONING_MESSAGE_CONTENT,
981
+ EventType.REASONING_MESSAGE_END,
982
+ EventType.REASONING_END,
983
+ ].includes(e.type),
984
+ );
985
+
986
+ for (const event of reasoningEvents) {
987
+ expect(event.messageId).toBe("reasoning-msg-123");
988
+ }
989
+ });
990
+
991
+ it("should generate unique reasoningMessageId when provider returns id '0'", async () => {
992
+ const agent = new BasicAgent({
993
+ model: "openai/gpt-4o",
994
+ });
995
+
996
+ vi.mocked(streamText).mockReturnValue(
997
+ mockStreamTextResponse([
998
+ reasoningStart("0"),
999
+ reasoningDelta("content"),
1000
+ reasoningEnd(),
1001
+ finish(),
1002
+ ]) as any,
1003
+ );
1004
+
1005
+ const input: RunAgentInput = {
1006
+ threadId: "thread1",
1007
+ runId: "run1",
1008
+ messages: [],
1009
+ tools: [],
1010
+ context: [],
1011
+ state: {},
1012
+ };
1013
+
1014
+ const events = await collectEvents(agent["run"](input));
1015
+
1016
+ const reasoningEvent = events.find(
1017
+ (e: any) => e.type === EventType.REASONING_START,
1018
+ );
1019
+ expect(reasoningEvent.messageId).not.toBe("0");
1020
+ expect(reasoningEvent.messageId).toMatch(
1021
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/,
1022
+ );
1023
+ });
1024
+
1025
+ it("should handle empty reasoning content", async () => {
1026
+ const agent = new BasicAgent({
1027
+ model: "openai/gpt-4o",
1028
+ });
1029
+
1030
+ vi.mocked(streamText).mockReturnValue(
1031
+ mockStreamTextResponse([
1032
+ reasoningStart(),
1033
+ reasoningDelta(""),
1034
+ reasoningEnd(),
1035
+ finish(),
1036
+ ]) as any,
1037
+ );
1038
+
1039
+ const input: RunAgentInput = {
1040
+ threadId: "thread1",
1041
+ runId: "run1",
1042
+ messages: [],
1043
+ tools: [],
1044
+ context: [],
1045
+ state: {},
1046
+ };
1047
+
1048
+ const events = await collectEvents(agent["run"](input));
1049
+
1050
+ const contentEvent = events.find(
1051
+ (e: any) => e.type === EventType.REASONING_MESSAGE_CONTENT,
1052
+ );
1053
+ expect(contentEvent).toMatchObject({ delta: "" });
1054
+
1055
+ // Full lifecycle should still complete
1056
+ const eventTypes = events.map((e: any) => e.type);
1057
+ expect(eventTypes).toContain(EventType.REASONING_START);
1058
+ expect(eventTypes).toContain(EventType.REASONING_MESSAGE_START);
1059
+ expect(eventTypes).toContain(EventType.REASONING_MESSAGE_END);
1060
+ expect(eventTypes).toContain(EventType.REASONING_END);
1061
+ expect(eventTypes).toContain(EventType.RUN_FINISHED);
1062
+ });
1063
+
1064
+ it("should handle reasoning-only stream (no text output)", async () => {
1065
+ const agent = new BasicAgent({
1066
+ model: "openai/gpt-4o",
1067
+ });
1068
+
1069
+ vi.mocked(streamText).mockReturnValue(
1070
+ mockStreamTextResponse([
1071
+ reasoningStart(),
1072
+ reasoningDelta("Deep thought"),
1073
+ reasoningEnd(),
1074
+ finish(),
1075
+ ]) as any,
1076
+ );
1077
+
1078
+ const input: RunAgentInput = {
1079
+ threadId: "thread1",
1080
+ runId: "run1",
1081
+ messages: [],
1082
+ tools: [],
1083
+ context: [],
1084
+ state: {},
1085
+ };
1086
+
1087
+ const events = await collectEvents(agent["run"](input));
1088
+
1089
+ // No TEXT_MESSAGE_CHUNK events
1090
+ const textEvents = events.filter(
1091
+ (e: any) => e.type === EventType.TEXT_MESSAGE_CHUNK,
1092
+ );
1093
+ expect(textEvents).toHaveLength(0);
1094
+
1095
+ // Reasoning events are present
1096
+ const reasoningContentEvents = events.filter(
1097
+ (e: any) => e.type === EventType.REASONING_MESSAGE_CONTENT,
1098
+ );
1099
+ expect(reasoningContentEvents).toHaveLength(1);
1100
+ expect(reasoningContentEvents[0]).toMatchObject({
1101
+ delta: "Deep thought",
1102
+ });
1103
+ });
1104
+
1105
+ it("should handle reasoning interleaved with tool calls", async () => {
1106
+ const agent = new BasicAgent({
1107
+ model: "openai/gpt-4o",
1108
+ });
1109
+
1110
+ vi.mocked(streamText).mockReturnValue(
1111
+ mockStreamTextResponse([
1112
+ reasoningStart(),
1113
+ reasoningDelta("I need to call a tool"),
1114
+ reasoningEnd(),
1115
+ toolCallStreamingStart("call1", "testTool"),
1116
+ toolCallDelta("call1", '{"arg":"val"}'),
1117
+ toolCall("call1", "testTool", { arg: "val" }),
1118
+ toolResult("call1", "testTool", { result: "success" }),
1119
+ finish(),
1120
+ ]) as any,
1121
+ );
1122
+
1123
+ const input: RunAgentInput = {
1124
+ threadId: "thread1",
1125
+ runId: "run1",
1126
+ messages: [],
1127
+ tools: [],
1128
+ context: [],
1129
+ state: {},
1130
+ };
1131
+
1132
+ const events = await collectEvents(agent["run"](input));
1133
+ const eventTypes = events.map((e: any) => e.type);
1134
+
1135
+ // Reasoning events precede tool call events
1136
+ const reasoningEndIdx = eventTypes.indexOf(EventType.REASONING_END);
1137
+ const toolCallStartIdx = eventTypes.indexOf(EventType.TOOL_CALL_START);
1138
+ expect(reasoningEndIdx).toBeLessThan(toolCallStartIdx);
1139
+
1140
+ // Both lifecycles complete
1141
+ expect(eventTypes).toContain(EventType.REASONING_START);
1142
+ expect(eventTypes).toContain(EventType.REASONING_END);
1143
+ expect(eventTypes).toContain(EventType.TOOL_CALL_START);
1144
+ expect(eventTypes).toContain(EventType.TOOL_CALL_END);
1145
+ });
1146
+ });
1147
+
1148
+ describe("Provider Options", () => {
1149
+ it("should pass providerOptions to streamText", async () => {
1150
+ const agent = new BasicAgent({
1151
+ model: "openai/gpt-4o",
1152
+ providerOptions: {
1153
+ openai: { reasoningEffort: "high", reasoningSummary: "detailed" },
1154
+ },
1155
+ });
1156
+
1157
+ vi.mocked(streamText).mockReturnValue(
1158
+ mockStreamTextResponse([finish()]) as any,
1159
+ );
1160
+
1161
+ const input: RunAgentInput = {
1162
+ threadId: "thread1",
1163
+ runId: "run1",
1164
+ messages: [],
1165
+ tools: [],
1166
+ context: [],
1167
+ state: {},
1168
+ };
1169
+
1170
+ await collectEvents(agent["run"](input));
1171
+
1172
+ const callArgs = vi.mocked(streamText).mock.calls[0][0];
1173
+ expect(callArgs.providerOptions).toEqual({
1174
+ openai: { reasoningEffort: "high", reasoningSummary: "detailed" },
1175
+ });
1176
+ });
1177
+
1178
+ it("should allow providerOptions override via forwardedProps when overridable", async () => {
1179
+ const agent = new BasicAgent({
1180
+ model: "openai/gpt-4o",
1181
+ providerOptions: {
1182
+ openai: { reasoningEffort: "low" },
1183
+ },
1184
+ overridableProperties: ["providerOptions"],
1185
+ });
1186
+
1187
+ vi.mocked(streamText).mockReturnValue(
1188
+ mockStreamTextResponse([finish()]) as any,
1189
+ );
1190
+
1191
+ const input: RunAgentInput = {
1192
+ threadId: "thread1",
1193
+ runId: "run1",
1194
+ messages: [],
1195
+ tools: [],
1196
+ context: [],
1197
+ state: {},
1198
+ forwardedProps: {
1199
+ providerOptions: {
1200
+ openai: { reasoningEffort: "high" },
1201
+ },
1202
+ },
1203
+ };
1204
+
1205
+ await collectEvents(agent["run"](input));
1206
+
1207
+ const callArgs = vi.mocked(streamText).mock.calls[0][0];
1208
+ expect(callArgs.providerOptions).toEqual({
1209
+ openai: { reasoningEffort: "high" },
1210
+ });
1211
+ });
1212
+
1213
+ it("should NOT allow providerOptions override when not in overridableProperties", async () => {
1214
+ const agent = new BasicAgent({
1215
+ model: "openai/gpt-4o",
1216
+ providerOptions: {
1217
+ openai: { reasoningEffort: "low" },
1218
+ },
1219
+ overridableProperties: [],
1220
+ });
1221
+
1222
+ vi.mocked(streamText).mockReturnValue(
1223
+ mockStreamTextResponse([finish()]) as any,
1224
+ );
1225
+
1226
+ const input: RunAgentInput = {
1227
+ threadId: "thread1",
1228
+ runId: "run1",
1229
+ messages: [],
1230
+ tools: [],
1231
+ context: [],
1232
+ state: {},
1233
+ forwardedProps: {
1234
+ providerOptions: {
1235
+ openai: { reasoningEffort: "high" },
1236
+ },
1237
+ },
1238
+ };
1239
+
1240
+ await collectEvents(agent["run"](input));
1241
+
1242
+ const callArgs = vi.mocked(streamText).mock.calls[0][0];
1243
+ expect(callArgs.providerOptions).toEqual({
1244
+ openai: { reasoningEffort: "low" },
1245
+ });
1246
+ });
1247
+ });
821
1248
  });
@@ -103,6 +103,38 @@ export function toolResult(
103
103
  };
104
104
  }
105
105
 
106
+ /**
107
+ * Helper to create a reasoning-start event
108
+ */
109
+ export function reasoningStart(id?: string): MockStreamEvent {
110
+ const event: MockStreamEvent = {
111
+ type: "reasoning-start",
112
+ };
113
+ if (id !== undefined) {
114
+ event.id = id;
115
+ }
116
+ return event;
117
+ }
118
+
119
+ /**
120
+ * Helper to create a reasoning-delta event
121
+ */
122
+ export function reasoningDelta(text: string): MockStreamEvent {
123
+ return {
124
+ type: "reasoning-delta",
125
+ text,
126
+ };
127
+ }
128
+
129
+ /**
130
+ * Helper to create a reasoning-end event
131
+ */
132
+ export function reasoningEnd(): MockStreamEvent {
133
+ return {
134
+ type: "reasoning-end",
135
+ };
136
+ }
137
+
106
138
  /**
107
139
  * Helper to create a finish event
108
140
  */
package/src/index.ts CHANGED
@@ -4,6 +4,11 @@ import {
4
4
  RunAgentInput,
5
5
  EventType,
6
6
  Message,
7
+ ReasoningEndEvent,
8
+ ReasoningMessageContentEvent,
9
+ ReasoningMessageEndEvent,
10
+ ReasoningMessageStartEvent,
11
+ ReasoningStartEvent,
7
12
  RunFinishedEvent,
8
13
  RunStartedEvent,
9
14
  TextMessageChunkEvent,
@@ -60,7 +65,8 @@ export type OverridableProperty =
60
65
  | "stopSequences"
61
66
  | "seed"
62
67
  | "maxRetries"
63
- | "prompt";
68
+ | "prompt"
69
+ | "providerOptions";
64
70
 
65
71
  /**
66
72
  * Supported model identifiers for BuiltInAgent
@@ -572,6 +578,11 @@ export interface BuiltInAgentConfiguration {
572
578
  * Default: false
573
579
  */
574
580
  forwardDeveloperMessages?: boolean;
581
+ /**
582
+ * Provider-specific options passed to the model (e.g., OpenAI reasoningEffort).
583
+ * Example: `{ openai: { reasoningEffort: "high" } }`
584
+ */
585
+ providerOptions?: Record<string, any>;
575
586
  }
576
587
 
577
588
  export class BuiltInAgent extends AbstractAgent {
@@ -683,6 +694,7 @@ export class BuiltInAgent extends AbstractAgent {
683
694
  frequencyPenalty: this.config.frequencyPenalty,
684
695
  stopSequences: this.config.stopSequences,
685
696
  seed: this.config.seed,
697
+ providerOptions: this.config.providerOptions,
686
698
  maxRetries: this.config.maxRetries,
687
699
  };
688
700
 
@@ -773,6 +785,20 @@ export class BuiltInAgent extends AbstractAgent {
773
785
  ) {
774
786
  streamTextParams.maxRetries = props.maxRetries;
775
787
  }
788
+ if (
789
+ props.providerOptions !== undefined &&
790
+ this.canOverride("providerOptions")
791
+ ) {
792
+ if (
793
+ typeof props.providerOptions === "object" &&
794
+ props.providerOptions !== null
795
+ ) {
796
+ streamTextParams.providerOptions = props.providerOptions as Record<
797
+ string,
798
+ any
799
+ >;
800
+ }
801
+ }
776
802
  }
777
803
 
778
804
  // Set up MCP clients if configured and process the stream
@@ -865,6 +891,7 @@ export class BuiltInAgent extends AbstractAgent {
865
891
  });
866
892
 
867
893
  let messageId = randomUUID();
894
+ let reasoningMessageId = randomUUID();
868
895
 
869
896
  const toolCallStates = new Map<
870
897
  string,
@@ -888,7 +915,7 @@ export class BuiltInAgent extends AbstractAgent {
888
915
  // Process fullStream events
889
916
  for await (const part of response.fullStream) {
890
917
  switch (part.type) {
891
- case "abort":
918
+ case "abort": {
892
919
  const abortEndEvent: RunFinishedEvent = {
893
920
  type: EventType.RUN_FINISHED,
894
921
  threadId: input.threadId,
@@ -900,7 +927,50 @@ export class BuiltInAgent extends AbstractAgent {
900
927
  // Complete the observable
901
928
  subscriber.complete();
902
929
  break;
903
-
930
+ }
931
+ case "reasoning-start": {
932
+ // New text message starting - use the SDK-provided id
933
+ // Use randomUUID() if part.id is falsy or "0" to prevent message merging issues
934
+ const providedId = "id" in part ? part.id : undefined;
935
+ if (providedId && providedId !== "0") {
936
+ reasoningMessageId = providedId as typeof reasoningMessageId;
937
+ }
938
+ const reasoningStartEvent: ReasoningStartEvent = {
939
+ type: EventType.REASONING_START,
940
+ messageId: reasoningMessageId,
941
+ };
942
+ subscriber.next(reasoningStartEvent);
943
+ const reasoningMessageStart: ReasoningMessageStartEvent = {
944
+ type: EventType.REASONING_MESSAGE_START,
945
+ messageId: reasoningMessageId,
946
+ role: "reasoning",
947
+ };
948
+ subscriber.next(reasoningMessageStart);
949
+ break;
950
+ }
951
+ case "reasoning-delta": {
952
+ const reasoningDeltaEvent: ReasoningMessageContentEvent = {
953
+ type: EventType.REASONING_MESSAGE_CONTENT,
954
+ messageId: reasoningMessageId,
955
+ delta:
956
+ ("text" in part ? part.text : (part as any).delta) ?? "",
957
+ };
958
+ subscriber.next(reasoningDeltaEvent);
959
+ break;
960
+ }
961
+ case "reasoning-end": {
962
+ const reasoningMessageEnd: ReasoningMessageEndEvent = {
963
+ type: EventType.REASONING_MESSAGE_END,
964
+ messageId: reasoningMessageId,
965
+ };
966
+ subscriber.next(reasoningMessageEnd);
967
+ const reasoningEndEvent: ReasoningEndEvent = {
968
+ type: EventType.REASONING_END,
969
+ messageId: reasoningMessageId,
970
+ };
971
+ subscriber.next(reasoningEndEvent);
972
+ break;
973
+ }
904
974
  case "tool-input-start": {
905
975
  const toolCallId = part.id;
906
976
  const state = ensureToolCallState(toolCallId);
@@ -1057,7 +1127,7 @@ export class BuiltInAgent extends AbstractAgent {
1057
1127
  break;
1058
1128
  }
1059
1129
 
1060
- case "finish":
1130
+ case "finish": {
1061
1131
  // Emit run finished event
1062
1132
  const finishedEvent: RunFinishedEvent = {
1063
1133
  type: EventType.RUN_FINISHED,
@@ -1070,6 +1140,7 @@ export class BuiltInAgent extends AbstractAgent {
1070
1140
  // Complete the observable
1071
1141
  subscriber.complete();
1072
1142
  break;
1143
+ }
1073
1144
 
1074
1145
  case "error": {
1075
1146
  if (abortController.signal.aborted) {