@assistant-ui/eve 0.0.12 → 0.0.14
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/convertEveMessages.d.ts +17 -3
- package/dist/convertEveMessages.d.ts.map +1 -1
- package/dist/convertEveMessages.js +11 -6
- package/dist/convertEveMessages.js.map +1 -1
- package/dist/eveExtras.d.ts +7 -0
- package/dist/eveExtras.d.ts.map +1 -0
- package/dist/eveExtras.js +7 -0
- package/dist/eveExtras.js.map +1 -0
- package/dist/hooks.d.ts +25 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +32 -0
- package/dist/hooks.js.map +1 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +2 -1
- package/dist/useEveAgentRuntime.d.ts.map +1 -1
- package/dist/useEveAgentRuntime.js +49 -20
- package/dist/useEveAgentRuntime.js.map +1 -1
- package/package.json +6 -5
- package/src/convertEveMessages.test.ts +299 -4
- package/src/convertEveMessages.ts +42 -6
- package/src/eveExtras.ts +10 -0
- package/src/hooks.test.tsx +84 -0
- package/src/hooks.ts +36 -0
- package/src/index.ts +3 -0
- package/src/useEveAgentRuntime.test.tsx +618 -51
- package/src/useEveAgentRuntime.ts +109 -32
|
@@ -8,6 +8,11 @@ import {
|
|
|
8
8
|
} from "./convertEveMessages";
|
|
9
9
|
import type { AppendMessage } from "@assistant-ui/core";
|
|
10
10
|
|
|
11
|
+
const eventMeta = (sequence: number) => ({
|
|
12
|
+
at: "2026-01-01T00:00:00.000Z",
|
|
13
|
+
id: `evt_${sequence}`,
|
|
14
|
+
});
|
|
15
|
+
|
|
11
16
|
describe("convertEveMessages", () => {
|
|
12
17
|
it("converts text and reasoning parts", () => {
|
|
13
18
|
const data = {
|
|
@@ -48,6 +53,75 @@ describe("convertEveMessages", () => {
|
|
|
48
53
|
});
|
|
49
54
|
});
|
|
50
55
|
|
|
56
|
+
it("omits the part status when the part state is still streaming", () => {
|
|
57
|
+
const data = {
|
|
58
|
+
messages: [
|
|
59
|
+
{
|
|
60
|
+
id: "a1",
|
|
61
|
+
role: "assistant",
|
|
62
|
+
metadata: { status: "streaming" },
|
|
63
|
+
parts: [
|
|
64
|
+
{ type: "reasoning", text: "Thinking", state: "streaming" },
|
|
65
|
+
{ type: "text", text: "Hi", state: "streaming" },
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
} satisfies EveMessageData;
|
|
70
|
+
|
|
71
|
+
const messages = convertEveMessages(data, { isRunning: true });
|
|
72
|
+
|
|
73
|
+
for (const part of messages[0]!.content) {
|
|
74
|
+
expect(part).not.toHaveProperty("status");
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("maps a done part state to a complete part status", () => {
|
|
79
|
+
const data = {
|
|
80
|
+
messages: [
|
|
81
|
+
{
|
|
82
|
+
id: "a1",
|
|
83
|
+
role: "assistant",
|
|
84
|
+
metadata: { status: "complete" },
|
|
85
|
+
parts: [
|
|
86
|
+
{ type: "reasoning", text: "Thinking", state: "done" },
|
|
87
|
+
{ type: "text", text: "Hi", state: "done" },
|
|
88
|
+
],
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
} satisfies EveMessageData;
|
|
92
|
+
|
|
93
|
+
const messages = convertEveMessages(data);
|
|
94
|
+
|
|
95
|
+
expect(messages[0]!.content).toEqual([
|
|
96
|
+
expect.objectContaining({
|
|
97
|
+
type: "reasoning",
|
|
98
|
+
status: { type: "complete" },
|
|
99
|
+
}),
|
|
100
|
+
expect.objectContaining({ type: "text", status: { type: "complete" } }),
|
|
101
|
+
]);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("omits the part status when the part state is absent", () => {
|
|
105
|
+
const data = {
|
|
106
|
+
messages: [
|
|
107
|
+
{
|
|
108
|
+
id: "a1",
|
|
109
|
+
role: "assistant",
|
|
110
|
+
parts: [
|
|
111
|
+
{ type: "reasoning", text: "Thinking" },
|
|
112
|
+
{ type: "text", text: "Hi" },
|
|
113
|
+
],
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
} satisfies EveMessageData;
|
|
117
|
+
|
|
118
|
+
const messages = convertEveMessages(data);
|
|
119
|
+
|
|
120
|
+
for (const part of messages[0]!.content) {
|
|
121
|
+
expect(part).not.toHaveProperty("status");
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
51
125
|
it("converts dynamic tool parts with approval options", () => {
|
|
52
126
|
const data = {
|
|
53
127
|
messages: [
|
|
@@ -68,11 +142,12 @@ describe("convertEveMessages", () => {
|
|
|
68
142
|
name: "send_email",
|
|
69
143
|
inputRequest: {
|
|
70
144
|
requestId: "req_1",
|
|
145
|
+
kind: "tool-approval",
|
|
71
146
|
prompt: "Send the email?",
|
|
72
147
|
display: "confirmation",
|
|
73
148
|
options: [
|
|
74
149
|
{ id: "approve", label: "Approve" },
|
|
75
|
-
{ id: "
|
|
150
|
+
{ id: "cancel", label: "Cancel", style: "danger" },
|
|
76
151
|
{ id: "escalate", label: "Escalate" },
|
|
77
152
|
],
|
|
78
153
|
},
|
|
@@ -98,7 +173,7 @@ describe("convertEveMessages", () => {
|
|
|
98
173
|
id: "req_1",
|
|
99
174
|
options: [
|
|
100
175
|
{ id: "approve", kind: "allow-once", label: "Approve" },
|
|
101
|
-
{ id: "
|
|
176
|
+
{ id: "cancel", kind: "reject-once", label: "Cancel" },
|
|
102
177
|
{ id: "escalate", kind: "_escalate", label: "Escalate" },
|
|
103
178
|
],
|
|
104
179
|
},
|
|
@@ -846,6 +921,7 @@ describe("convertEveMessages", () => {
|
|
|
846
921
|
const events: readonly EveAgentReducerEvent[] = [
|
|
847
922
|
{
|
|
848
923
|
type: "authorization.required",
|
|
924
|
+
meta: eventMeta(0),
|
|
849
925
|
data: {
|
|
850
926
|
turnId: "turn_1",
|
|
851
927
|
stepIndex: 0,
|
|
@@ -856,6 +932,7 @@ describe("convertEveMessages", () => {
|
|
|
856
932
|
},
|
|
857
933
|
{
|
|
858
934
|
type: "turn.cancelled",
|
|
935
|
+
meta: eventMeta(1),
|
|
859
936
|
data: { turnId: "turn_1", sequence: 1 },
|
|
860
937
|
},
|
|
861
938
|
];
|
|
@@ -1010,14 +1087,22 @@ describe("convertEveMessages", () => {
|
|
|
1010
1087
|
},
|
|
1011
1088
|
{
|
|
1012
1089
|
type: "turn.started",
|
|
1090
|
+
meta: eventMeta(0),
|
|
1013
1091
|
data: { turnId: "turn_1", sequence: 0 },
|
|
1014
1092
|
},
|
|
1015
1093
|
{
|
|
1016
1094
|
type: "step.started",
|
|
1017
|
-
|
|
1095
|
+
meta: eventMeta(1),
|
|
1096
|
+
data: {
|
|
1097
|
+
turnId: "turn_1",
|
|
1098
|
+
stepIndex: 0,
|
|
1099
|
+
sequence: 1,
|
|
1100
|
+
modelId: "test-model",
|
|
1101
|
+
},
|
|
1018
1102
|
},
|
|
1019
1103
|
{
|
|
1020
1104
|
type: "message.appended",
|
|
1105
|
+
meta: eventMeta(2),
|
|
1021
1106
|
data: {
|
|
1022
1107
|
turnId: "turn_1",
|
|
1023
1108
|
stepIndex: 0,
|
|
@@ -1033,6 +1118,7 @@ describe("convertEveMessages", () => {
|
|
|
1033
1118
|
...midStreamEvents,
|
|
1034
1119
|
{
|
|
1035
1120
|
type: "authorization.required",
|
|
1121
|
+
meta: eventMeta(3),
|
|
1036
1122
|
data: {
|
|
1037
1123
|
turnId: "turn_1",
|
|
1038
1124
|
stepIndex: 0,
|
|
@@ -1073,6 +1159,7 @@ describe("convertEveMessages", () => {
|
|
|
1073
1159
|
...midStreamEvents,
|
|
1074
1160
|
{
|
|
1075
1161
|
type: "authorization.required",
|
|
1162
|
+
meta: eventMeta(3),
|
|
1076
1163
|
data: {
|
|
1077
1164
|
turnId: "turn_1",
|
|
1078
1165
|
stepIndex: 0,
|
|
@@ -1083,6 +1170,7 @@ describe("convertEveMessages", () => {
|
|
|
1083
1170
|
},
|
|
1084
1171
|
{
|
|
1085
1172
|
type: "authorization.completed",
|
|
1173
|
+
meta: eventMeta(4),
|
|
1086
1174
|
data: {
|
|
1087
1175
|
turnId: "turn_1",
|
|
1088
1176
|
stepIndex: 0,
|
|
@@ -1112,6 +1200,7 @@ describe("convertEveMessages", () => {
|
|
|
1112
1200
|
...midStreamEvents,
|
|
1113
1201
|
{
|
|
1114
1202
|
type: "authorization.required",
|
|
1203
|
+
meta: eventMeta(3),
|
|
1115
1204
|
data: {
|
|
1116
1205
|
turnId: "turn_1",
|
|
1117
1206
|
stepIndex: 0,
|
|
@@ -1122,6 +1211,7 @@ describe("convertEveMessages", () => {
|
|
|
1122
1211
|
},
|
|
1123
1212
|
{
|
|
1124
1213
|
type: "authorization.required",
|
|
1214
|
+
meta: eventMeta(4),
|
|
1125
1215
|
data: {
|
|
1126
1216
|
turnId: "turn_1",
|
|
1127
1217
|
stepIndex: 0,
|
|
@@ -1132,6 +1222,7 @@ describe("convertEveMessages", () => {
|
|
|
1132
1222
|
},
|
|
1133
1223
|
{
|
|
1134
1224
|
type: "authorization.completed",
|
|
1225
|
+
meta: eventMeta(5),
|
|
1135
1226
|
data: {
|
|
1136
1227
|
turnId: "turn_1",
|
|
1137
1228
|
stepIndex: 0,
|
|
@@ -1147,6 +1238,193 @@ describe("convertEveMessages", () => {
|
|
|
1147
1238
|
).toEqual({ type: "requires-action", reason: "interrupt" });
|
|
1148
1239
|
});
|
|
1149
1240
|
|
|
1241
|
+
it("leaves a reasoning part unsettled when a tool call follows it", () => {
|
|
1242
|
+
const state = replay([
|
|
1243
|
+
...midStreamEvents.slice(0, 3),
|
|
1244
|
+
{
|
|
1245
|
+
type: "reasoning.appended",
|
|
1246
|
+
data: {
|
|
1247
|
+
turnId: "turn_1",
|
|
1248
|
+
stepIndex: 0,
|
|
1249
|
+
sequence: 2,
|
|
1250
|
+
reasoningDelta: "Think",
|
|
1251
|
+
reasoningSoFar: "Think",
|
|
1252
|
+
},
|
|
1253
|
+
},
|
|
1254
|
+
{
|
|
1255
|
+
type: "actions.requested",
|
|
1256
|
+
data: {
|
|
1257
|
+
turnId: "turn_1",
|
|
1258
|
+
stepIndex: 0,
|
|
1259
|
+
sequence: 3,
|
|
1260
|
+
actions: [
|
|
1261
|
+
{
|
|
1262
|
+
kind: "tool-call",
|
|
1263
|
+
callId: "call_1",
|
|
1264
|
+
toolName: "search",
|
|
1265
|
+
input: {},
|
|
1266
|
+
},
|
|
1267
|
+
],
|
|
1268
|
+
},
|
|
1269
|
+
},
|
|
1270
|
+
]);
|
|
1271
|
+
|
|
1272
|
+
const message = state.messages.find((m) => m.role === "assistant");
|
|
1273
|
+
expect(message?.parts).toEqual(
|
|
1274
|
+
expect.arrayContaining([
|
|
1275
|
+
expect.objectContaining({ type: "reasoning", state: "streaming" }),
|
|
1276
|
+
expect.objectContaining({ type: "dynamic-tool" }),
|
|
1277
|
+
]),
|
|
1278
|
+
);
|
|
1279
|
+
|
|
1280
|
+
const content = convertEveMessages(state, { isRunning: true }).at(
|
|
1281
|
+
-1,
|
|
1282
|
+
)?.content;
|
|
1283
|
+
expect(content).toEqual([
|
|
1284
|
+
expect.objectContaining({ type: "reasoning" }),
|
|
1285
|
+
expect.objectContaining({ type: "tool-call" }),
|
|
1286
|
+
]);
|
|
1287
|
+
expect(content?.[0]).not.toHaveProperty("status");
|
|
1288
|
+
});
|
|
1289
|
+
|
|
1290
|
+
it("settles leftover reasoning after the model stream completes a tool call", () => {
|
|
1291
|
+
const state = replay([
|
|
1292
|
+
...midStreamEvents.slice(0, 3),
|
|
1293
|
+
{
|
|
1294
|
+
type: "reasoning.appended",
|
|
1295
|
+
data: {
|
|
1296
|
+
turnId: "turn_1",
|
|
1297
|
+
stepIndex: 0,
|
|
1298
|
+
sequence: 2,
|
|
1299
|
+
reasoningDelta: "Think",
|
|
1300
|
+
reasoningSoFar: "Think",
|
|
1301
|
+
},
|
|
1302
|
+
},
|
|
1303
|
+
{
|
|
1304
|
+
type: "actions.requested",
|
|
1305
|
+
data: {
|
|
1306
|
+
turnId: "turn_1",
|
|
1307
|
+
stepIndex: 0,
|
|
1308
|
+
sequence: 3,
|
|
1309
|
+
actions: [
|
|
1310
|
+
{
|
|
1311
|
+
kind: "tool-call",
|
|
1312
|
+
callId: "call_1",
|
|
1313
|
+
toolName: "search",
|
|
1314
|
+
input: {},
|
|
1315
|
+
},
|
|
1316
|
+
],
|
|
1317
|
+
},
|
|
1318
|
+
},
|
|
1319
|
+
{
|
|
1320
|
+
type: "reasoning.completed",
|
|
1321
|
+
data: {
|
|
1322
|
+
turnId: "turn_1",
|
|
1323
|
+
stepIndex: 0,
|
|
1324
|
+
sequence: 4,
|
|
1325
|
+
reasoning: "Think",
|
|
1326
|
+
},
|
|
1327
|
+
},
|
|
1328
|
+
]);
|
|
1329
|
+
|
|
1330
|
+
const message = state.messages.find((m) => m.role === "assistant");
|
|
1331
|
+
expect(message?.parts).toEqual(
|
|
1332
|
+
expect.arrayContaining([
|
|
1333
|
+
expect.objectContaining({ type: "reasoning", state: "done" }),
|
|
1334
|
+
expect.objectContaining({ type: "dynamic-tool" }),
|
|
1335
|
+
]),
|
|
1336
|
+
);
|
|
1337
|
+
|
|
1338
|
+
const content = convertEveMessages(state, { isRunning: true }).at(
|
|
1339
|
+
-1,
|
|
1340
|
+
)?.content;
|
|
1341
|
+
expect(content).toEqual([
|
|
1342
|
+
expect.objectContaining({
|
|
1343
|
+
type: "reasoning",
|
|
1344
|
+
status: { type: "complete" },
|
|
1345
|
+
}),
|
|
1346
|
+
expect.objectContaining({ type: "tool-call" }),
|
|
1347
|
+
]);
|
|
1348
|
+
});
|
|
1349
|
+
|
|
1350
|
+
it("keeps a later step's live text part unsettled after an earlier step completed", () => {
|
|
1351
|
+
const state = replay([
|
|
1352
|
+
...midStreamEvents.slice(0, 3),
|
|
1353
|
+
{
|
|
1354
|
+
type: "message.appended",
|
|
1355
|
+
data: {
|
|
1356
|
+
turnId: "turn_1",
|
|
1357
|
+
stepIndex: 0,
|
|
1358
|
+
sequence: 2,
|
|
1359
|
+
messageDelta: "First",
|
|
1360
|
+
messageSoFar: "First",
|
|
1361
|
+
},
|
|
1362
|
+
},
|
|
1363
|
+
{
|
|
1364
|
+
type: "message.completed",
|
|
1365
|
+
data: {
|
|
1366
|
+
turnId: "turn_1",
|
|
1367
|
+
stepIndex: 0,
|
|
1368
|
+
sequence: 3,
|
|
1369
|
+
finishReason: "tool-calls",
|
|
1370
|
+
message: "First",
|
|
1371
|
+
},
|
|
1372
|
+
},
|
|
1373
|
+
{
|
|
1374
|
+
type: "step.completed",
|
|
1375
|
+
data: {
|
|
1376
|
+
turnId: "turn_1",
|
|
1377
|
+
stepIndex: 0,
|
|
1378
|
+
sequence: 4,
|
|
1379
|
+
finishReason: "tool-calls",
|
|
1380
|
+
},
|
|
1381
|
+
},
|
|
1382
|
+
{
|
|
1383
|
+
type: "step.started",
|
|
1384
|
+
data: { turnId: "turn_1", stepIndex: 1, sequence: 5 },
|
|
1385
|
+
},
|
|
1386
|
+
{
|
|
1387
|
+
type: "message.appended",
|
|
1388
|
+
data: {
|
|
1389
|
+
turnId: "turn_1",
|
|
1390
|
+
stepIndex: 1,
|
|
1391
|
+
sequence: 6,
|
|
1392
|
+
messageDelta: "Sec",
|
|
1393
|
+
messageSoFar: "Sec",
|
|
1394
|
+
},
|
|
1395
|
+
},
|
|
1396
|
+
]);
|
|
1397
|
+
|
|
1398
|
+
const assistant = state.messages.find((m) => m.role === "assistant");
|
|
1399
|
+
expect(assistant?.parts).toEqual([
|
|
1400
|
+
expect.objectContaining({ type: "step-start" }),
|
|
1401
|
+
expect.objectContaining({
|
|
1402
|
+
type: "text",
|
|
1403
|
+
text: "First",
|
|
1404
|
+
state: "done",
|
|
1405
|
+
}),
|
|
1406
|
+
expect.objectContaining({ type: "step-start" }),
|
|
1407
|
+
expect.objectContaining({
|
|
1408
|
+
type: "text",
|
|
1409
|
+
text: "Sec",
|
|
1410
|
+
state: "streaming",
|
|
1411
|
+
}),
|
|
1412
|
+
]);
|
|
1413
|
+
|
|
1414
|
+
const content = convertEveMessages(state, { isRunning: true }).at(
|
|
1415
|
+
-1,
|
|
1416
|
+
)?.content;
|
|
1417
|
+
expect(content).toEqual([
|
|
1418
|
+
expect.objectContaining({
|
|
1419
|
+
type: "text",
|
|
1420
|
+
text: "First",
|
|
1421
|
+
status: { type: "complete" },
|
|
1422
|
+
}),
|
|
1423
|
+
expect.objectContaining({ type: "text", text: "Sec" }),
|
|
1424
|
+
]);
|
|
1425
|
+
expect(content?.[1]).not.toHaveProperty("status");
|
|
1426
|
+
});
|
|
1427
|
+
|
|
1150
1428
|
it("a locally aborted turn keeps its streaming marker and converts to cancelled", () => {
|
|
1151
1429
|
const state = replay(midStreamEvents);
|
|
1152
1430
|
|
|
@@ -1165,6 +1443,7 @@ describe("convertEveMessages", () => {
|
|
|
1165
1443
|
...midStreamEvents,
|
|
1166
1444
|
{
|
|
1167
1445
|
type: "session.failed",
|
|
1446
|
+
meta: eventMeta(3),
|
|
1168
1447
|
data: { sessionId: "session_1", code: "internal", message: "boom" },
|
|
1169
1448
|
},
|
|
1170
1449
|
]);
|
|
@@ -1188,6 +1467,7 @@ describe("convertEveMessages", () => {
|
|
|
1188
1467
|
...midStreamEvents,
|
|
1189
1468
|
{
|
|
1190
1469
|
type: "turn.failed",
|
|
1470
|
+
meta: eventMeta(3),
|
|
1191
1471
|
data: {
|
|
1192
1472
|
turnId: "turn_1",
|
|
1193
1473
|
sequence: 3,
|
|
@@ -1209,6 +1489,7 @@ describe("convertEveMessages", () => {
|
|
|
1209
1489
|
...midStreamEvents,
|
|
1210
1490
|
{
|
|
1211
1491
|
type: "message.completed",
|
|
1492
|
+
meta: eventMeta(3),
|
|
1212
1493
|
data: {
|
|
1213
1494
|
turnId: "turn_1",
|
|
1214
1495
|
stepIndex: 0,
|
|
@@ -1219,18 +1500,32 @@ describe("convertEveMessages", () => {
|
|
|
1219
1500
|
},
|
|
1220
1501
|
{
|
|
1221
1502
|
type: "turn.completed",
|
|
1503
|
+
meta: eventMeta(4),
|
|
1222
1504
|
data: { turnId: "turn_1", sequence: 4 },
|
|
1223
1505
|
},
|
|
1224
1506
|
]);
|
|
1225
1507
|
|
|
1226
1508
|
const assistant = state.messages.find((m) => m.role === "assistant");
|
|
1227
1509
|
expect(assistant?.metadata?.status).toBe("complete");
|
|
1510
|
+
expect(assistant?.parts).toEqual(
|
|
1511
|
+
expect.arrayContaining([
|
|
1512
|
+
expect.objectContaining({ type: "text", state: "done" }),
|
|
1513
|
+
]),
|
|
1514
|
+
);
|
|
1228
1515
|
|
|
1229
1516
|
const converted = convertEveMessages(state, { isRunning: false });
|
|
1230
1517
|
expect(converted.at(-1)?.status).toEqual({
|
|
1231
1518
|
type: "complete",
|
|
1232
1519
|
reason: "stop",
|
|
1233
1520
|
});
|
|
1521
|
+
expect(converted.at(-1)?.content).toEqual(
|
|
1522
|
+
expect.arrayContaining([
|
|
1523
|
+
expect.objectContaining({
|
|
1524
|
+
type: "text",
|
|
1525
|
+
status: { type: "complete" },
|
|
1526
|
+
}),
|
|
1527
|
+
]),
|
|
1528
|
+
);
|
|
1234
1529
|
});
|
|
1235
1530
|
});
|
|
1236
1531
|
});
|
|
@@ -1428,7 +1723,7 @@ describe("toEveInputResponse", () => {
|
|
|
1428
1723
|
}),
|
|
1429
1724
|
).toEqual({
|
|
1430
1725
|
requestId: "req_1",
|
|
1431
|
-
optionId: "
|
|
1726
|
+
optionId: "cancel",
|
|
1432
1727
|
text: "Not yet",
|
|
1433
1728
|
});
|
|
1434
1729
|
});
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
type CompleteAttachment,
|
|
6
6
|
type DataMessagePart,
|
|
7
7
|
type FileMessagePart,
|
|
8
|
+
type MessagePartStreamStatus,
|
|
8
9
|
type MessageStatus,
|
|
9
10
|
type RespondToToolApprovalOptions,
|
|
10
11
|
type ThreadAssistantMessagePart,
|
|
@@ -24,7 +25,7 @@ import type {
|
|
|
24
25
|
EveMessageInputRequest,
|
|
25
26
|
EveMessagePart,
|
|
26
27
|
} from "eve/react";
|
|
27
|
-
import type { InputResponse
|
|
28
|
+
import type { InputResponse } from "eve/client";
|
|
28
29
|
|
|
29
30
|
const ASSISTANT_COMPLETE_STATUS = {
|
|
30
31
|
type: "complete",
|
|
@@ -148,7 +149,7 @@ const toolApprovalOptionsFromInputRequest = (
|
|
|
148
149
|
kind:
|
|
149
150
|
option.id === "approve"
|
|
150
151
|
? "allow-once"
|
|
151
|
-
: option.id === "
|
|
152
|
+
: option.id === "cancel"
|
|
152
153
|
? "reject-once"
|
|
153
154
|
: `_${option.id}`,
|
|
154
155
|
...(option.label && { label: option.label }),
|
|
@@ -287,13 +288,31 @@ const convertFilePart = (
|
|
|
287
288
|
};
|
|
288
289
|
};
|
|
289
290
|
|
|
291
|
+
// Eve's `state` is a run marker: the reducer settles a part to "done" only on
|
|
292
|
+
// `reasoning.completed`, `message.completed`, or `turn.cancelled`.
|
|
293
|
+
// `emitStreamContent` does not flush leftover reasoning on a tool-call, so a
|
|
294
|
+
// non-last reasoning part stays `streaming` until the model stream ends and the
|
|
295
|
+
// leftover flush runs. Mapping `streaming` to running would pin that block
|
|
296
|
+
// open. Only `done` is trustworthy; an unsettled part falls back to core's
|
|
297
|
+
// last-part rule.
|
|
298
|
+
const partStateToStatus = (
|
|
299
|
+
state: "done" | "streaming" | undefined,
|
|
300
|
+
): MessagePartStreamStatus | undefined =>
|
|
301
|
+
state === "done" ? { type: "complete" } : undefined;
|
|
302
|
+
|
|
290
303
|
const convertAssistantPart = (
|
|
291
304
|
part: EveMessagePart,
|
|
292
305
|
): ThreadAssistantMessagePart | null => {
|
|
293
306
|
switch (part.type) {
|
|
294
307
|
case "text":
|
|
295
|
-
case "reasoning":
|
|
296
|
-
|
|
308
|
+
case "reasoning": {
|
|
309
|
+
const status = partStateToStatus(part.state);
|
|
310
|
+
return {
|
|
311
|
+
type: part.type,
|
|
312
|
+
text: part.text,
|
|
313
|
+
...(status && { status }),
|
|
314
|
+
};
|
|
315
|
+
}
|
|
297
316
|
|
|
298
317
|
case "step-start":
|
|
299
318
|
return null;
|
|
@@ -418,13 +437,30 @@ export const convertEveMessages = (
|
|
|
418
437
|
convertEveMessage(message, index, messages, options),
|
|
419
438
|
);
|
|
420
439
|
|
|
440
|
+
/**
|
|
441
|
+
* Structural subset of the `string | UserContent` message content Eve's `send`
|
|
442
|
+
* API accepts. The helpers declare the shapes they produce and leave
|
|
443
|
+
* assignability to the send call site, checked against the installed version.
|
|
444
|
+
*/
|
|
445
|
+
export type EveMessageContent =
|
|
446
|
+
| string
|
|
447
|
+
| (
|
|
448
|
+
| { readonly type: "text"; readonly text: string }
|
|
449
|
+
| {
|
|
450
|
+
readonly type: "file";
|
|
451
|
+
readonly data: string;
|
|
452
|
+
readonly mediaType: string;
|
|
453
|
+
readonly filename?: string;
|
|
454
|
+
}
|
|
455
|
+
)[];
|
|
456
|
+
|
|
421
457
|
/**
|
|
422
458
|
* Converts an assistant-ui append message into the message payload accepted by
|
|
423
459
|
* Eve's `send` API.
|
|
424
460
|
*/
|
|
425
461
|
export const getEveMessageContent = (
|
|
426
462
|
message: AppendMessage,
|
|
427
|
-
):
|
|
463
|
+
): EveMessageContent => {
|
|
428
464
|
const content = [
|
|
429
465
|
...message.content,
|
|
430
466
|
...(message.attachments?.flatMap((attachment) => attachment.content) ?? []),
|
|
@@ -496,6 +532,6 @@ export const toEveInputResponse = (
|
|
|
496
532
|
response: RespondToToolApprovalOptions,
|
|
497
533
|
): InputResponse => ({
|
|
498
534
|
requestId: response.approvalId,
|
|
499
|
-
optionId: response.optionId ?? (response.approved ? "approve" : "
|
|
535
|
+
optionId: response.optionId ?? (response.approved ? "approve" : "cancel"),
|
|
500
536
|
...(response.reason && { text: response.reason }),
|
|
501
537
|
});
|
package/src/eveExtras.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createRuntimeExtras } from "@assistant-ui/core/react";
|
|
2
|
+
import type { EveMessageData, UseEveAgentHelpers } from "eve/react";
|
|
3
|
+
|
|
4
|
+
export type EveRuntimeExtras = Pick<
|
|
5
|
+
UseEveAgentHelpers<EveMessageData>,
|
|
6
|
+
"error" | "events" | "session" | "reset"
|
|
7
|
+
>;
|
|
8
|
+
|
|
9
|
+
export const eveExtras =
|
|
10
|
+
createRuntimeExtras<EveRuntimeExtras>("useEveAgentRuntime");
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { renderHook } from "@testing-library/react";
|
|
4
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
|
|
6
|
+
const { extrasRef } = vi.hoisted(() => ({
|
|
7
|
+
extrasRef: { current: undefined as unknown },
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
vi.mock("@assistant-ui/store", async (importOriginal) => ({
|
|
11
|
+
...(await importOriginal<typeof import("@assistant-ui/store")>()),
|
|
12
|
+
useAui: (() => ({
|
|
13
|
+
thread: { getState: () => ({ extras: extrasRef.current }) },
|
|
14
|
+
})) as unknown as typeof import("@assistant-ui/store").useAui,
|
|
15
|
+
useAuiState: ((selector: (s: unknown) => unknown) =>
|
|
16
|
+
selector({
|
|
17
|
+
thread: { extras: extrasRef.current },
|
|
18
|
+
})) as typeof import("@assistant-ui/store").useAuiState,
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
import { eveExtras, type EveRuntimeExtras } from "./eveExtras";
|
|
22
|
+
import { useEveError, useEveEvents, useEveReset, useEveSession } from "./hooks";
|
|
23
|
+
|
|
24
|
+
const provideExtras = (over: Partial<EveRuntimeExtras>) =>
|
|
25
|
+
eveExtras.provide({
|
|
26
|
+
error: undefined,
|
|
27
|
+
events: [],
|
|
28
|
+
session: {} as EveRuntimeExtras["session"],
|
|
29
|
+
reset: () => {},
|
|
30
|
+
...over,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
afterEach(() => {
|
|
34
|
+
extrasRef.current = undefined;
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe("Eve accessor hooks", () => {
|
|
38
|
+
it("reads the session error from the runtime extras", () => {
|
|
39
|
+
const error = new Error("boom");
|
|
40
|
+
extrasRef.current = provideExtras({ error });
|
|
41
|
+
|
|
42
|
+
expect(renderHook(() => useEveError()).result.current).toBe(error);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("reads the session cursor from the runtime extras", () => {
|
|
46
|
+
const session = {
|
|
47
|
+
sessionId: "s1",
|
|
48
|
+
} as unknown as EveRuntimeExtras["session"];
|
|
49
|
+
extrasRef.current = provideExtras({ session });
|
|
50
|
+
|
|
51
|
+
expect(renderHook(() => useEveSession()).result.current).toBe(session);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("reads the server event stream from the runtime extras", () => {
|
|
55
|
+
const events = [
|
|
56
|
+
{ type: "session.started" },
|
|
57
|
+
] as unknown as EveRuntimeExtras["events"];
|
|
58
|
+
extrasRef.current = provideExtras({ events });
|
|
59
|
+
|
|
60
|
+
expect(renderHook(() => useEveEvents()).result.current).toBe(events);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("reads reset from the runtime extras", () => {
|
|
64
|
+
const reset = vi.fn();
|
|
65
|
+
extrasRef.current = provideExtras({ reset });
|
|
66
|
+
|
|
67
|
+
renderHook(() => useEveReset()).result.current();
|
|
68
|
+
|
|
69
|
+
expect(reset).toHaveBeenCalledTimes(1);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("supplies fallbacks outside an Eve runtime, and defers the reset throw to invocation", () => {
|
|
73
|
+
expect(renderHook(() => useEveError()).result.current).toBeUndefined();
|
|
74
|
+
expect(renderHook(() => useEveSession()).result.current).toBeUndefined();
|
|
75
|
+
|
|
76
|
+
const events = renderHook(() => useEveEvents()).result.current;
|
|
77
|
+
expect(events).toEqual([]);
|
|
78
|
+
expect(renderHook(() => useEveEvents()).result.current).toBe(events);
|
|
79
|
+
expect(Object.isFrozen(events)).toBe(true);
|
|
80
|
+
|
|
81
|
+
const reset = renderHook(() => useEveReset()).result.current;
|
|
82
|
+
expect(() => reset()).toThrow("useEveAgentRuntime");
|
|
83
|
+
});
|
|
84
|
+
});
|
package/src/hooks.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useAui } from "@assistant-ui/store";
|
|
4
|
+
import { eveExtras } from "./eveExtras";
|
|
5
|
+
import type { EveRuntimeExtras } from "./eveExtras";
|
|
6
|
+
|
|
7
|
+
const EMPTY_EVENTS: EveRuntimeExtras["events"] = Object.freeze([]);
|
|
8
|
+
|
|
9
|
+
/** Read the last Eve session error from the runtime extras. */
|
|
10
|
+
export const useEveError = () => eveExtras.use((e) => e.error, undefined);
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Read the current Eve session cursor from the runtime extras. Persist it to
|
|
14
|
+
* resume the session later via `initialSession`. `undefined` when no session
|
|
15
|
+
* exists yet or outside an Eve runtime.
|
|
16
|
+
*/
|
|
17
|
+
export const useEveSession = (): EveRuntimeExtras["session"] | undefined =>
|
|
18
|
+
eveExtras.use((e) => e.session, undefined);
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Read the authoritative Eve server event stream from the runtime extras.
|
|
22
|
+
* Defaults to an empty array outside an Eve runtime.
|
|
23
|
+
*/
|
|
24
|
+
export const useEveEvents = (): EveRuntimeExtras["events"] =>
|
|
25
|
+
eveExtras.use((e) => e.events, EMPTY_EVENTS);
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Returns a function that resets the Eve session: aborts any in-flight turn,
|
|
29
|
+
* recreates the owned session, and clears events and projected data. Pending
|
|
30
|
+
* staged messages and tool execution state are discarded. Safe to render
|
|
31
|
+
* outside an Eve runtime; invoking the returned function there throws.
|
|
32
|
+
*/
|
|
33
|
+
export const useEveReset = () => {
|
|
34
|
+
const aui = useAui();
|
|
35
|
+
return () => eveExtras.get(aui).reset();
|
|
36
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -9,6 +9,9 @@ export {
|
|
|
9
9
|
export type {
|
|
10
10
|
ConvertEveMessagesOptions,
|
|
11
11
|
EveAuthorizationData,
|
|
12
|
+
EveMessageContent,
|
|
12
13
|
} from "./convertEveMessages";
|
|
13
14
|
export { useEveAgentRuntime } from "./useEveAgentRuntime";
|
|
14
15
|
export type { UseEveAgentRuntimeOptions } from "./useEveAgentRuntime";
|
|
16
|
+
export { useEveError, useEveEvents, useEveReset, useEveSession } from "./hooks";
|
|
17
|
+
export type { EveRuntimeExtras } from "./eveExtras";
|