@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.
@@ -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: "deny", label: "Deny", style: "danger" },
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: "deny", kind: "reject-once", label: "Deny" },
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
- data: { turnId: "turn_1", stepIndex: 0, sequence: 1 },
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: "deny",
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, SendTurnPayload } from "eve/client";
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 === "deny"
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
- return { type: part.type, text: part.text };
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
- ): NonNullable<SendTurnPayload["message"]> => {
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" : "deny"),
535
+ optionId: response.optionId ?? (response.approved ? "approve" : "cancel"),
500
536
  ...(response.reason && { text: response.reason }),
501
537
  });
@@ -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";