@assistant-ui/eve 0.0.16 → 0.0.18
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 +8 -9
- package/dist/convertEveMessages.d.ts.map +1 -1
- package/dist/deriveCreatedAt.d.ts +22 -0
- package/dist/deriveCreatedAt.d.ts.map +1 -0
- package/dist/deriveCreatedAt.js +79 -0
- package/dist/deriveCreatedAt.js.map +1 -0
- package/dist/eveExtras.d.ts +2 -3
- package/dist/eveExtras.d.ts.map +1 -1
- package/dist/hooks.d.ts +4 -5
- package/dist/hooks.d.ts.map +1 -1
- package/dist/useEveAgentRuntime.d.ts +2 -3
- package/dist/useEveAgentRuntime.d.ts.map +1 -1
- package/dist/useEveAgentRuntime.js +8 -1
- package/dist/useEveAgentRuntime.js.map +1 -1
- package/package.json +11 -10
- package/src/convertEveMessages.test.ts +0 -5
- package/src/deriveCreatedAt.test.ts +189 -0
- package/src/deriveCreatedAt.ts +96 -0
- package/src/useEveAgentRuntime.eveResume.test.tsx +80 -0
- package/src/useEveAgentRuntime.test.tsx +86 -0
- package/src/useEveAgentRuntime.ts +20 -1
|
@@ -2,7 +2,7 @@ import { AppendMessage, RespondToToolApprovalOptions, ThreadMessage } from "@ass
|
|
|
2
2
|
import { EveAuthorizationOutcome, EveAuthorizationPart, EveMessage, EveMessageData, EveMessageInputRequest } from "eve/react";
|
|
3
3
|
import { InputResponse } from "eve/client";
|
|
4
4
|
//#region src/convertEveMessages.d.ts
|
|
5
|
-
type ConvertEveMessagesOptions = {
|
|
5
|
+
export type ConvertEveMessagesOptions = {
|
|
6
6
|
/**
|
|
7
7
|
* Marks the last assistant message as running while Eve is submitting or
|
|
8
8
|
* streaming. When omitted, a message carrying Eve's `"streaming"` marker is
|
|
@@ -27,7 +27,7 @@ type ConvertEveMessagesOptions = {
|
|
|
27
27
|
* one part it renders, in Eve's own step order, so display never has to
|
|
28
28
|
* re-identify a challenge among several.
|
|
29
29
|
*/
|
|
30
|
-
type EveAuthorizationData = {
|
|
30
|
+
export type EveAuthorizationData = {
|
|
31
31
|
readonly state: EveAuthorizationPart["state"];
|
|
32
32
|
readonly name: string;
|
|
33
33
|
readonly displayName?: string;
|
|
@@ -42,17 +42,17 @@ type EveAuthorizationData = {
|
|
|
42
42
|
/**
|
|
43
43
|
* Converts a single Eve message into an assistant-ui thread message.
|
|
44
44
|
*/
|
|
45
|
-
declare const convertEveMessage: (message: EveMessage, index: number, messages: readonly EveMessage[], options?: ConvertEveMessagesOptions) => ThreadMessage;
|
|
45
|
+
export declare const convertEveMessage: (message: EveMessage, index: number, messages: readonly EveMessage[], options?: ConvertEveMessagesOptions) => ThreadMessage;
|
|
46
46
|
/**
|
|
47
47
|
* Converts the full Eve message data object into assistant-ui thread messages.
|
|
48
48
|
*/
|
|
49
|
-
declare const convertEveMessages: (data: EveMessageData, options?: ConvertEveMessagesOptions) => ThreadMessage[];
|
|
49
|
+
export declare const convertEveMessages: (data: EveMessageData, options?: ConvertEveMessagesOptions) => ThreadMessage[];
|
|
50
50
|
/**
|
|
51
51
|
* Structural subset of the `string | UserContent` message content Eve's `send`
|
|
52
52
|
* API accepts. The helpers declare the shapes they produce and leave
|
|
53
53
|
* assignability to the send call site, checked against the installed version.
|
|
54
54
|
*/
|
|
55
|
-
type EveMessageContent = string | ({
|
|
55
|
+
export type EveMessageContent = string | ({
|
|
56
56
|
readonly type: "text";
|
|
57
57
|
readonly text: string;
|
|
58
58
|
} | {
|
|
@@ -65,12 +65,12 @@ type EveMessageContent = string | ({
|
|
|
65
65
|
* Converts an assistant-ui append message into the message payload accepted by
|
|
66
66
|
* Eve's `send` API.
|
|
67
67
|
*/
|
|
68
|
-
declare const getEveMessageContent: (message: AppendMessage) => EveMessageContent;
|
|
68
|
+
export declare const getEveMessageContent: (message: AppendMessage) => EveMessageContent;
|
|
69
69
|
/**
|
|
70
70
|
* Finds the pending input request answered by the given approval id, so the
|
|
71
71
|
* response mapping can honor the request's display mode.
|
|
72
72
|
*/
|
|
73
|
-
declare const findEveInputRequest: (data: EveMessageData, approvalId: string) => EveMessageInputRequest | undefined;
|
|
73
|
+
export declare const findEveInputRequest: (data: EveMessageData, approvalId: string) => EveMessageInputRequest | undefined;
|
|
74
74
|
/**
|
|
75
75
|
* Converts an assistant-ui tool approval response into an Eve input response.
|
|
76
76
|
*
|
|
@@ -96,7 +96,6 @@ declare const findEveInputRequest: (data: EveMessageData, approvalId: string) =>
|
|
|
96
96
|
* a display mode is what this mapper stopped doing; an unknown request is not
|
|
97
97
|
* a guess about one.
|
|
98
98
|
*/
|
|
99
|
-
declare const toEveInputResponse: (response: RespondToToolApprovalOptions, inputRequest?: EveMessageInputRequest) => InputResponse;
|
|
99
|
+
export declare const toEveInputResponse: (response: RespondToToolApprovalOptions, inputRequest?: EveMessageInputRequest) => InputResponse;
|
|
100
100
|
//#endregion
|
|
101
|
-
export { ConvertEveMessagesOptions, EveAuthorizationData, EveMessageContent, convertEveMessage, convertEveMessages, findEveInputRequest, getEveMessageContent, toEveInputResponse };
|
|
102
101
|
//# sourceMappingURL=convertEveMessages.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"convertEveMessages.d.ts","names":[],"sources":["../src/convertEveMessages.ts"],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"convertEveMessages.d.ts","names":[],"sources":["../src/convertEveMessages.ts"],"mappings":";;;;YAqDY;;;;;;;WAOD;;;;WAIA;WACA,iBAAiB,SAAS,eAAe;;;;;;;;;;;;;YA0PxC;WACD,OAAO;WACP;WACA;WACA;WACA;WACA;WACA;WACA;WACA,UAAU;WACV;;;;;qBA6IE,oBACX,SAAS,YACT,eACA,mBAAmB,cACnB,UAAS,8BACR;;;;qBAuCU,qBACX,MAAM,gBACN,UAAS,8BACR;;;;;;YAUS;WAGO;WAAuB;;WAEvB;WACA;WACA;WACA;;;;;;qBAQN,uBACX,SAAS,kBACR;;;;;qBAuEU,sBACX,MAAM,gBACN,uBACC;;;;;;;;;;;;;;;;;;;;;;;;;;qBAoCU,qBACX,UAAU,8BACV,eAAe,2BACd"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { MessageStreamEvent } from "eve/client";
|
|
2
|
+
//#region src/deriveCreatedAt.d.ts
|
|
3
|
+
export type TurnTimestamps = {
|
|
4
|
+
readonly user?: Date;
|
|
5
|
+
readonly assistant?: Date;
|
|
6
|
+
};
|
|
7
|
+
export type TurnTimestampCache = {
|
|
8
|
+
lastEvents: readonly MessageStreamEvent[];
|
|
9
|
+
timestamps: ReadonlyMap<string, TurnTimestamps>;
|
|
10
|
+
};
|
|
11
|
+
export declare const createTurnTimestampCache: () => TurnTimestampCache;
|
|
12
|
+
/**
|
|
13
|
+
* Eve appends to its log (`[...events, event]`), so a snapshot that keeps the
|
|
14
|
+
* previous scan's last element by identity shares the whole prefix and the
|
|
15
|
+
* scan resumes there. Any other snapshot is re-derived into a fresh map: turn
|
|
16
|
+
* ids are per-session sequence numbers that recur after `reset()`, so entries
|
|
17
|
+
* must not outlive the log that produced them. Re-derivation is also what
|
|
18
|
+
* makes the render-phase cache writes safe when React discards a render.
|
|
19
|
+
*/
|
|
20
|
+
export declare const collectTurnTimestamps: (events: readonly MessageStreamEvent[], cache: TurnTimestampCache) => ReadonlyMap<string, TurnTimestamps>;
|
|
21
|
+
//#endregion
|
|
22
|
+
//# sourceMappingURL=deriveCreatedAt.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deriveCreatedAt.d.ts","names":[],"sources":["../src/deriveCreatedAt.ts"],"mappings":";;YAEY;WACD,OAAO;WACP,YAAY;;YAGX;EACV,qBAAqB;EACrB,YAAY,oBAAoB;;qBAiCrB,gCAA+B;;;;;;;;;qBAa/B,wBACX,iBAAiB,sBACjB,OAAO,uBACN,oBAAoB"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
//#region src/deriveCreatedAt.ts
|
|
2
|
+
/**
|
|
3
|
+
* The reducer cases eve projects into a message: `message.received` creates
|
|
4
|
+
* `${turnId}:user`, and every assistant entry reaches `updateAssistantMessage`,
|
|
5
|
+
* which creates `${turnId}:assistant` for a turn that has none. It enumerates
|
|
6
|
+
* that reducer across the `>=0.32.0` peer range rather than stating a rule
|
|
7
|
+
* about turn ids, because `turn.started` and the compaction events carry one
|
|
8
|
+
* before the model runs. An event eve adds to the creating set later is absent
|
|
9
|
+
* here and falls back to the wall clock instead of stamping a wrong time.
|
|
10
|
+
*/
|
|
11
|
+
const ROLE_BY_EVENT_TYPE = {
|
|
12
|
+
"message.received": "user",
|
|
13
|
+
"step.started": "assistant",
|
|
14
|
+
"reasoning.appended": "assistant",
|
|
15
|
+
"reasoning.completed": "assistant",
|
|
16
|
+
"action.input.appended": "assistant",
|
|
17
|
+
"actions.requested": "assistant",
|
|
18
|
+
"input.requested": "assistant",
|
|
19
|
+
"action.result": "assistant",
|
|
20
|
+
"action.partial": "assistant",
|
|
21
|
+
"authorization.required": "assistant",
|
|
22
|
+
"authorization.completed": "assistant",
|
|
23
|
+
"message.appended": "assistant",
|
|
24
|
+
"message.completed": "assistant",
|
|
25
|
+
"result.completed": "assistant",
|
|
26
|
+
"turn.completed": "assistant",
|
|
27
|
+
"turn.cancelled": "assistant"
|
|
28
|
+
};
|
|
29
|
+
const createTurnTimestampCache = () => ({
|
|
30
|
+
lastEvents: [],
|
|
31
|
+
timestamps: /* @__PURE__ */ new Map()
|
|
32
|
+
});
|
|
33
|
+
/**
|
|
34
|
+
* Eve appends to its log (`[...events, event]`), so a snapshot that keeps the
|
|
35
|
+
* previous scan's last element by identity shares the whole prefix and the
|
|
36
|
+
* scan resumes there. Any other snapshot is re-derived into a fresh map: turn
|
|
37
|
+
* ids are per-session sequence numbers that recur after `reset()`, so entries
|
|
38
|
+
* must not outlive the log that produced them. Re-derivation is also what
|
|
39
|
+
* makes the render-phase cache writes safe when React discards a render.
|
|
40
|
+
*/
|
|
41
|
+
const collectTurnTimestamps = (events, cache) => {
|
|
42
|
+
if (events === cache.lastEvents) return cache.timestamps;
|
|
43
|
+
const scanned = cache.lastEvents;
|
|
44
|
+
const resumesScan = scanned.length === 0 || events[scanned.length - 1] === scanned[scanned.length - 1];
|
|
45
|
+
let timestamps = cache.timestamps;
|
|
46
|
+
let draft;
|
|
47
|
+
if (!resumesScan) {
|
|
48
|
+
draft = /* @__PURE__ */ new Map();
|
|
49
|
+
timestamps = draft;
|
|
50
|
+
}
|
|
51
|
+
for (let i = resumesScan ? scanned.length : 0; i < events.length; i++) {
|
|
52
|
+
const event = events[i];
|
|
53
|
+
const role = ROLE_BY_EVENT_TYPE[event.type];
|
|
54
|
+
if (role === void 0) continue;
|
|
55
|
+
const data = event.data;
|
|
56
|
+
if (typeof data !== "object" || data === null) continue;
|
|
57
|
+
const turnId = data.turnId;
|
|
58
|
+
if (typeof turnId !== "string") continue;
|
|
59
|
+
const known = timestamps.get(turnId);
|
|
60
|
+
if (known?.[role] !== void 0) continue;
|
|
61
|
+
const at = event.meta?.at;
|
|
62
|
+
if (typeof at !== "string") continue;
|
|
63
|
+
const date = new Date(at);
|
|
64
|
+
if (Number.isNaN(date.getTime())) continue;
|
|
65
|
+
draft ??= new Map(timestamps);
|
|
66
|
+
draft.set(turnId, {
|
|
67
|
+
...known,
|
|
68
|
+
[role]: date
|
|
69
|
+
});
|
|
70
|
+
timestamps = draft;
|
|
71
|
+
}
|
|
72
|
+
cache.lastEvents = events;
|
|
73
|
+
cache.timestamps = timestamps;
|
|
74
|
+
return timestamps;
|
|
75
|
+
};
|
|
76
|
+
//#endregion
|
|
77
|
+
export { collectTurnTimestamps, createTurnTimestampCache };
|
|
78
|
+
|
|
79
|
+
//# sourceMappingURL=deriveCreatedAt.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deriveCreatedAt.js","names":[],"sources":["../src/deriveCreatedAt.ts"],"sourcesContent":["import type { MessageStreamEvent } from \"eve/client\";\n\nexport type TurnTimestamps = {\n readonly user?: Date;\n readonly assistant?: Date;\n};\n\nexport type TurnTimestampCache = {\n lastEvents: readonly MessageStreamEvent[];\n timestamps: ReadonlyMap<string, TurnTimestamps>;\n};\n\n/**\n * The reducer cases eve projects into a message: `message.received` creates\n * `${turnId}:user`, and every assistant entry reaches `updateAssistantMessage`,\n * which creates `${turnId}:assistant` for a turn that has none. It enumerates\n * that reducer across the `>=0.32.0` peer range rather than stating a rule\n * about turn ids, because `turn.started` and the compaction events carry one\n * before the model runs. An event eve adds to the creating set later is absent\n * here and falls back to the wall clock instead of stamping a wrong time.\n */\nconst ROLE_BY_EVENT_TYPE: Partial<\n Record<MessageStreamEvent[\"type\"], keyof TurnTimestamps>\n> = {\n \"message.received\": \"user\",\n \"step.started\": \"assistant\",\n \"reasoning.appended\": \"assistant\",\n \"reasoning.completed\": \"assistant\",\n \"action.input.appended\": \"assistant\",\n \"actions.requested\": \"assistant\",\n \"input.requested\": \"assistant\",\n \"action.result\": \"assistant\",\n \"action.partial\": \"assistant\",\n \"authorization.required\": \"assistant\",\n \"authorization.completed\": \"assistant\",\n \"message.appended\": \"assistant\",\n \"message.completed\": \"assistant\",\n \"result.completed\": \"assistant\",\n \"turn.completed\": \"assistant\",\n \"turn.cancelled\": \"assistant\",\n};\n\nexport const createTurnTimestampCache = (): TurnTimestampCache => ({\n lastEvents: [],\n timestamps: new Map(),\n});\n\n/**\n * Eve appends to its log (`[...events, event]`), so a snapshot that keeps the\n * previous scan's last element by identity shares the whole prefix and the\n * scan resumes there. Any other snapshot is re-derived into a fresh map: turn\n * ids are per-session sequence numbers that recur after `reset()`, so entries\n * must not outlive the log that produced them. Re-derivation is also what\n * makes the render-phase cache writes safe when React discards a render.\n */\nexport const collectTurnTimestamps = (\n events: readonly MessageStreamEvent[],\n cache: TurnTimestampCache,\n): ReadonlyMap<string, TurnTimestamps> => {\n if (events === cache.lastEvents) return cache.timestamps;\n\n const scanned = cache.lastEvents;\n const resumesScan =\n scanned.length === 0 ||\n events[scanned.length - 1] === scanned[scanned.length - 1];\n\n let timestamps = cache.timestamps;\n let draft: Map<string, TurnTimestamps> | undefined;\n if (!resumesScan) {\n draft = new Map();\n timestamps = draft;\n }\n for (let i = resumesScan ? scanned.length : 0; i < events.length; i++) {\n const event = events[i]!;\n const role = ROLE_BY_EVENT_TYPE[event.type];\n if (role === undefined) continue;\n const data: unknown = (event as { readonly data?: unknown }).data;\n if (typeof data !== \"object\" || data === null) continue;\n const turnId = (data as { readonly turnId?: unknown }).turnId;\n if (typeof turnId !== \"string\") continue;\n const known = timestamps.get(turnId);\n if (known?.[role] !== undefined) continue;\n const at: unknown = (event as { readonly meta?: { readonly at?: unknown } })\n .meta?.at;\n if (typeof at !== \"string\") continue;\n const date = new Date(at);\n if (Number.isNaN(date.getTime())) continue;\n draft ??= new Map(timestamps);\n draft.set(turnId, { ...known, [role]: date });\n timestamps = draft;\n }\n\n cache.lastEvents = events;\n cache.timestamps = timestamps;\n return timestamps;\n};\n"],"mappings":";;;;;;;;;;AAqBA,MAAM,qBAEF;CACF,oBAAoB;CACpB,gBAAgB;CAChB,sBAAsB;CACtB,uBAAuB;CACvB,yBAAyB;CACzB,qBAAqB;CACrB,mBAAmB;CACnB,iBAAiB;CACjB,kBAAkB;CAClB,0BAA0B;CAC1B,2BAA2B;CAC3B,oBAAoB;CACpB,qBAAqB;CACrB,oBAAoB;CACpB,kBAAkB;CAClB,kBAAkB;AACpB;AAEA,MAAa,kCAAsD;CACjE,YAAY,CAAC;CACb,4BAAY,IAAI,IAAI;AACtB;;;;;;;;;AAUA,MAAa,yBACX,QACA,UACwC;CACxC,IAAI,WAAW,MAAM,YAAY,OAAO,MAAM;CAE9C,MAAM,UAAU,MAAM;CACtB,MAAM,cACJ,QAAQ,WAAW,KACnB,OAAO,QAAQ,SAAS,OAAO,QAAQ,QAAQ,SAAS;CAE1D,IAAI,aAAa,MAAM;CACvB,IAAI;CACJ,IAAI,CAAC,aAAa;EAChB,wBAAQ,IAAI,IAAI;EAChB,aAAa;CACf;CACA,KAAK,IAAI,IAAI,cAAc,QAAQ,SAAS,GAAG,IAAI,OAAO,QAAQ,KAAK;EACrE,MAAM,QAAQ,OAAO;EACrB,MAAM,OAAO,mBAAmB,MAAM;EACtC,IAAI,SAAS,KAAA,GAAW;EACxB,MAAM,OAAiB,MAAsC;EAC7D,IAAI,OAAO,SAAS,YAAY,SAAS,MAAM;EAC/C,MAAM,SAAU,KAAuC;EACvD,IAAI,OAAO,WAAW,UAAU;EAChC,MAAM,QAAQ,WAAW,IAAI,MAAM;EACnC,IAAI,QAAQ,UAAU,KAAA,GAAW;EACjC,MAAM,KAAe,MAClB,MAAM;EACT,IAAI,OAAO,OAAO,UAAU;EAC5B,MAAM,OAAO,IAAI,KAAK,EAAE;EACxB,IAAI,OAAO,MAAM,KAAK,QAAQ,CAAC,GAAG;EAClC,UAAU,IAAI,IAAI,UAAU;EAC5B,MAAM,IAAI,QAAQ;GAAE,GAAG;IAAQ,OAAO;EAAK,CAAC;EAC5C,aAAa;CACf;CAEA,MAAM,aAAa;CACnB,MAAM,aAAa;CACnB,OAAO;AACT"}
|
package/dist/eveExtras.d.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { EveMessageData, UseEveAgentHelpers } from "eve/react";
|
|
2
2
|
//#region src/eveExtras.d.ts
|
|
3
|
-
type EveRuntimeExtras = Pick<UseEveAgentHelpers<EveMessageData>, "error" | "events" | "session" | "reset">;
|
|
4
|
-
declare const eveExtras: import("@assistant-ui/core/react").RuntimeExtras<EveRuntimeExtras>;
|
|
3
|
+
export type EveRuntimeExtras = Pick<UseEveAgentHelpers<EveMessageData>, "error" | "events" | "session" | "reset">;
|
|
4
|
+
export declare const eveExtras: import("@assistant-ui/core/react").RuntimeExtras<EveRuntimeExtras>;
|
|
5
5
|
//#endregion
|
|
6
|
-
export { EveRuntimeExtras, eveExtras };
|
|
7
6
|
//# sourceMappingURL=eveExtras.d.ts.map
|
package/dist/eveExtras.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"eveExtras.d.ts","names":[],"sources":["../src/eveExtras.ts"],"mappings":";;
|
|
1
|
+
{"version":3,"file":"eveExtras.d.ts","names":[],"sources":["../src/eveExtras.ts"],"mappings":";;YAGY,mBAAmB,KAC7B,mBAAmB;qBAIR,8CAAS,cAAA"}
|
package/dist/hooks.d.ts
CHANGED
|
@@ -1,25 +1,24 @@
|
|
|
1
1
|
import { EveRuntimeExtras } from "./eveExtras.js";
|
|
2
2
|
//#region src/hooks.d.ts
|
|
3
3
|
/** Read the last Eve session error from the runtime extras. */
|
|
4
|
-
declare const useEveError: () => Error | undefined;
|
|
4
|
+
export declare const useEveError: () => Error | undefined;
|
|
5
5
|
/**
|
|
6
6
|
* Read the current Eve session cursor from the runtime extras. Persist it to
|
|
7
7
|
* resume the session later via `initialSession`. `undefined` when no session
|
|
8
8
|
* exists yet or outside an Eve runtime.
|
|
9
9
|
*/
|
|
10
|
-
declare const useEveSession: () => EveRuntimeExtras["session"] | undefined;
|
|
10
|
+
export declare const useEveSession: () => EveRuntimeExtras["session"] | undefined;
|
|
11
11
|
/**
|
|
12
12
|
* Read the authoritative Eve server event stream from the runtime extras.
|
|
13
13
|
* Defaults to an empty array outside an Eve runtime.
|
|
14
14
|
*/
|
|
15
|
-
declare const useEveEvents: () => EveRuntimeExtras["events"];
|
|
15
|
+
export declare const useEveEvents: () => EveRuntimeExtras["events"];
|
|
16
16
|
/**
|
|
17
17
|
* Returns a function that resets the Eve session: aborts any in-flight turn,
|
|
18
18
|
* recreates the owned session, and clears events and projected data. Pending
|
|
19
19
|
* staged messages and tool execution state are discarded. Safe to render
|
|
20
20
|
* outside an Eve runtime; invoking the returned function there throws.
|
|
21
21
|
*/
|
|
22
|
-
declare const useEveReset: () => () => void;
|
|
22
|
+
export declare const useEveReset: () => () => void;
|
|
23
23
|
//#endregion
|
|
24
|
-
export { useEveError, useEveEvents, useEveReset, useEveSession };
|
|
25
24
|
//# sourceMappingURL=hooks.d.ts.map
|
package/dist/hooks.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hooks.d.ts","names":[],"sources":["../src/hooks.ts"],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"hooks.d.ts","names":[],"sources":["../src/hooks.ts"],"mappings":";;;qBASa,mBAAW;;;;;;qBAOX,qBAAoB;;;;;qBAOpB,oBAAmB;;;;;;;qBASnB"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { AttachmentAdapter, DictationAdapter, ExternalStoreSharedOptions, FeedbackAdapter, RealtimeVoiceAdapter, SpeechSynthesisAdapter } from "@assistant-ui/core";
|
|
2
2
|
import { EveMessageData, UseEveAgentOptions } from "eve/react";
|
|
3
3
|
//#region src/useEveAgentRuntime.d.ts
|
|
4
|
-
type UseEveAgentRuntimeOptions = Omit<UseEveAgentOptions<EveMessageData>, "reducer"> & ExternalStoreSharedOptions & {
|
|
4
|
+
export type UseEveAgentRuntimeOptions = Omit<UseEveAgentOptions<EveMessageData>, "reducer"> & ExternalStoreSharedOptions & {
|
|
5
5
|
readonly adapters?: {
|
|
6
6
|
readonly attachments?: AttachmentAdapter | undefined;
|
|
7
7
|
readonly speech?: SpeechSynthesisAdapter | undefined;
|
|
@@ -17,7 +17,6 @@ type UseEveAgentRuntimeOptions = Omit<UseEveAgentOptions<EveMessageData>, "reduc
|
|
|
17
17
|
* session, supports cancellation, and maps Eve input requests to assistant-ui
|
|
18
18
|
* tool approval UI.
|
|
19
19
|
*/
|
|
20
|
-
declare const useEveAgentRuntime: (options?: UseEveAgentRuntimeOptions) => import("@assistant-ui/core").AssistantRuntime;
|
|
20
|
+
export declare const useEveAgentRuntime: (options?: UseEveAgentRuntimeOptions) => import("@assistant-ui/core").AssistantRuntime;
|
|
21
21
|
//#endregion
|
|
22
|
-
export { UseEveAgentRuntimeOptions, useEveAgentRuntime };
|
|
23
22
|
//# sourceMappingURL=useEveAgentRuntime.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useEveAgentRuntime.d.ts","names":[],"sources":["../src/useEveAgentRuntime.ts"],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"useEveAgentRuntime.d.ts","names":[],"sources":["../src/useEveAgentRuntime.ts"],"mappings":";;;YA+HY,4BAA4B,KACtC,mBAAmB,8BAGnB;WACW;aAEM,cAAc;aACd,SAAS;aACT,YAAY;aACZ,QAAQ;aACR,WAAW;;;;;;;;;;qBAYjB,qBAAsB,UAAS,2DAA8B"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { convertEveMessages, findEveInputRequest, getEveMessageContent, toEveInputResponse } from "./convertEveMessages.js";
|
|
3
|
+
import { collectTurnTimestamps, createTurnTimestampCache } from "./deriveCreatedAt.js";
|
|
3
4
|
import { eveExtras } from "./eveExtras.js";
|
|
4
5
|
import { MessageNotSentError, fromThreadMessageLike, generateId, pickExternalStoreSharedOptions } from "@assistant-ui/core";
|
|
5
6
|
import { useExternalStoreRuntime, useRuntimeAdapters } from "@assistant-ui/core/react";
|
|
@@ -63,6 +64,8 @@ const useEveAgentRuntime = (options = {}) => {
|
|
|
63
64
|
const hasExecutingTools = Object.values(toolStatuses).some((status) => status?.type === "executing");
|
|
64
65
|
const providerIsRunning = agent.status === "submitted" || agent.status === "streaming";
|
|
65
66
|
const isRunning = providerIsRunning || hasExecutingTools;
|
|
67
|
+
const turnTimestampCacheRef = useRef(createTurnTimestampCache());
|
|
68
|
+
const turnTimestamps = useMemo(() => collectTurnTimestamps(agent.events, turnTimestampCacheRef.current), [agent.events]);
|
|
66
69
|
const convertedMessages = useMemo(() => {
|
|
67
70
|
const createdAtByMessageId = createdAtByMessageIdRef.current;
|
|
68
71
|
const messageIds = new Set(agent.data.messages.map((message) => message.id));
|
|
@@ -71,6 +74,9 @@ const useEveAgentRuntime = (options = {}) => {
|
|
|
71
74
|
isRunning,
|
|
72
75
|
error: agent.error,
|
|
73
76
|
getCreatedAt: (message) => {
|
|
77
|
+
const turnId = message.metadata?.turnId;
|
|
78
|
+
const durable = turnId === void 0 ? void 0 : turnTimestamps.get(turnId)?.[message.role];
|
|
79
|
+
if (durable !== void 0) return durable;
|
|
74
80
|
const existing = createdAtByMessageId.get(message.id);
|
|
75
81
|
if (existing) return existing;
|
|
76
82
|
const createdAt = /* @__PURE__ */ new Date();
|
|
@@ -81,7 +87,8 @@ const useEveAgentRuntime = (options = {}) => {
|
|
|
81
87
|
}, [
|
|
82
88
|
agent.data,
|
|
83
89
|
agent.error,
|
|
84
|
-
isRunning
|
|
90
|
+
isRunning,
|
|
91
|
+
turnTimestamps
|
|
85
92
|
]);
|
|
86
93
|
const messages = stagedMessages ?? convertedMessages;
|
|
87
94
|
const messagesRef = useRef(messages);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useEveAgentRuntime.js","names":[],"sources":["../src/useEveAgentRuntime.ts"],"sourcesContent":["\"use client\";\n\nimport {\n useCallback,\n useEffect,\n useInsertionEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport {\n fromThreadMessageLike,\n generateId,\n MessageNotSentError,\n pickExternalStoreSharedOptions,\n type AppendMessage,\n type AttachmentAdapter,\n type DictationAdapter,\n type ExternalStoreSharedOptions,\n type FeedbackAdapter,\n type RealtimeVoiceAdapter,\n type SpeechSynthesisAdapter,\n type ThreadMessage,\n type ToolExecutionStatus,\n} from \"@assistant-ui/core\";\nimport {\n useExternalStoreRuntime,\n useRuntimeAdapters,\n} from \"@assistant-ui/core/react\";\nimport {\n useEveAgent,\n type EveMessageData,\n type UseEveAgentOptions,\n type UseEveAgentStatus,\n} from \"eve/react\";\nimport {\n convertEveMessages,\n findEveInputRequest,\n getEveMessageContent,\n toEveInputResponse,\n} from \"./convertEveMessages\";\nimport { eveExtras } from \"./eveExtras\";\n\nconst USER_STAGED_STATUS = {\n type: \"complete\",\n reason: \"unknown\",\n} as const;\n\nconst sendCancelledError = new MessageNotSentError(\n \"eve send was dropped because the run was cancelled.\",\n);\n\nconst sendAbandonedError = new Error(\n \"eve send was dropped because the runtime unmounted.\",\n);\n\nconst isDroppedSend = (error: unknown) =>\n error === sendCancelledError || error === sendAbandonedError;\n\ntype EveLifecycleCallbackName =\n | \"onError\"\n | \"onEvent\"\n | \"onFinish\"\n | \"onSessionChange\";\n\nconst reportEveLifecycleCallbackError = (\n name: EveLifecycleCallbackName,\n error: unknown,\n) => {\n console.error(`[assistant-ui/eve] ${name} callback threw an error`, error);\n};\n\nconst invokeEveLifecycleCallback = <T>(\n name: EveLifecycleCallbackName,\n callback: ((value: T) => unknown) | undefined,\n value: T,\n) => {\n if (!callback) return;\n\n try {\n void Promise.resolve(callback(value)).catch((error) => {\n reportEveLifecycleCallbackError(name, error);\n });\n } catch (error) {\n reportEveLifecycleCallbackError(name, error);\n }\n};\n\nconst hasRunConfig = (\n runConfig: AppendMessage[\"runConfig\"],\n): runConfig is NonNullable<AppendMessage[\"runConfig\"]> =>\n runConfig?.custom !== undefined && Object.keys(runConfig.custom).length > 0;\n\ntype EveJsonValue =\n | string\n | number\n | boolean\n | null\n | readonly EveJsonValue[]\n | { readonly [key: string]: EveJsonValue };\n\n/**\n * Structural equivalent of eve's `JsonObject`. Eve demoted its public send\n * payload type once already (`SendTurnPayload`, public through 0.30, internal\n * from 0.31), so the helpers declare the shapes they forward instead of\n * re-exporting eve's option types, and assignability is checked at the send\n * call sites against the installed version.\n */\ntype EveClientContext = { readonly [key: string]: EveJsonValue };\n\n/**\n * Only the `custom` bag crosses the wire. Eve reads `clientContext` as its own\n * namespace and serializes it into a model-visible context message, so sending\n * the assistant-ui envelope would surface a literal `\"custom\"` key in the\n * prompt and to every eve-side handler.\n */\nconst toEveSendOptions = (\n runConfig: AppendMessage[\"runConfig\"],\n): { readonly clientContext: EveClientContext } | undefined =>\n hasRunConfig(runConfig)\n ? { clientContext: runConfig.custom as EveClientContext }\n : undefined;\n\nexport type UseEveAgentRuntimeOptions = Omit<\n UseEveAgentOptions<EveMessageData>,\n \"reducer\"\n> &\n ExternalStoreSharedOptions & {\n readonly adapters?:\n | {\n readonly attachments?: AttachmentAdapter | undefined;\n readonly speech?: SpeechSynthesisAdapter | undefined;\n readonly dictation?: DictationAdapter | undefined;\n readonly voice?: RealtimeVoiceAdapter | undefined;\n readonly feedback?: FeedbackAdapter | undefined;\n }\n | undefined;\n };\n\n/**\n * Connects Eve's `useEveAgent` hook to assistant-ui's runtime contract.\n *\n * The runtime renders Eve messages, forwards new user messages to the Eve\n * session, supports cancellation, and maps Eve input requests to assistant-ui\n * tool approval UI.\n */\nexport const useEveAgentRuntime = (options: UseEveAgentRuntimeOptions = {}) => {\n const {\n adapters,\n isDisabled: _isDisabled,\n isSendDisabled: _isSendDisabled,\n suggestions: _suggestions,\n unstable_capabilities: _unstable_capabilities,\n ...agentOptions\n } = options;\n true satisfies keyof typeof agentOptions &\n keyof ExternalStoreSharedOptions extends never\n ? true\n : never;\n\n const { onError, onEvent, onFinish, onSessionChange } = agentOptions;\n const lastFinishStatusRef = useRef<UseEveAgentStatus | null>(null);\n const agent = useEveAgent({\n ...agentOptions,\n ...(onError\n ? {\n onError: (error) =>\n invokeEveLifecycleCallback(\"onError\", onError, error),\n }\n : {}),\n ...(onEvent\n ? {\n onEvent: (event) =>\n invokeEveLifecycleCallback(\"onEvent\", onEvent, event),\n }\n : {}),\n onFinish: (snapshot) => {\n lastFinishStatusRef.current = snapshot.status;\n invokeEveLifecycleCallback(\"onFinish\", onFinish, snapshot);\n },\n ...(onSessionChange\n ? {\n onSessionChange: (session) =>\n invokeEveLifecycleCallback(\n \"onSessionChange\",\n onSessionChange,\n session,\n ),\n }\n : {}),\n });\n const runtimeAdapters = useRuntimeAdapters();\n const [toolStatuses, setToolStatuses] = useState<\n Record<string, ToolExecutionStatus>\n >({});\n const [stagedMessages, setStagedMessages] = useState<ThreadMessage[] | null>(\n null,\n );\n const createdAtByMessageIdRef = useRef(new Map<string, Date>());\n const stagedInputsRef = useRef(\n new Map<\n string,\n { message: AppendMessage; runConfig: AppendMessage[\"runConfig\"] }\n >(),\n );\n\n const hasExecutingTools = Object.values(toolStatuses).some(\n (status) => status?.type === \"executing\",\n );\n const providerIsRunning =\n agent.status === \"submitted\" || agent.status === \"streaming\";\n const isRunning = providerIsRunning || hasExecutingTools;\n\n const convertedMessages = useMemo(() => {\n const createdAtByMessageId = createdAtByMessageIdRef.current;\n const messageIds = new Set(\n agent.data.messages.map((message) => message.id),\n );\n for (const messageId of createdAtByMessageId.keys()) {\n if (!messageIds.has(messageId)) createdAtByMessageId.delete(messageId);\n }\n\n return convertEveMessages(agent.data, {\n isRunning,\n error: agent.error,\n getCreatedAt: (message) => {\n const existing = createdAtByMessageId.get(message.id);\n if (existing) return existing;\n\n const createdAt = new Date();\n createdAtByMessageId.set(message.id, createdAt);\n return createdAt;\n },\n });\n }, [agent.data, agent.error, isRunning]);\n\n const messages = stagedMessages ?? convertedMessages;\n const messagesRef = useRef(messages);\n const agentRef = useRef(agent);\n // Descendant layout effects can dispatch against these refs synchronously;\n // this matches useA2ARuntime's commit-scoped ref publication.\n useInsertionEffect(() => {\n messagesRef.current = messages;\n agentRef.current = agent;\n }, [agent, messages]);\n\n // Upstream `EveAgentStore` `send` and `respond` reject while a turn is in\n // flight and only resolve once the turn's stream parks, so a pending chain\n // link is exactly an active turn; chaining every dispatch serializes them\n // without watching status.\n const sendChainRef = useRef<Promise<void>>(Promise.resolve());\n const sendEpochRef = useRef(0);\n // A cancel drops the queued send but keeps its draft for a later promotion;\n // only a reset discards the draft with the session, so the two need separate\n // counters. A queued read has no draft and outlives a cancel, so it waits on\n // the reset counter instead.\n const resetEpochRef = useRef(0);\n const isMountedRef = useRef(true);\n const runtimeRef = useRef<ReturnType<typeof useExternalStoreRuntime> | null>(\n null,\n );\n\n const enqueueSend = (\n dispatch: () => Promise<void>,\n epochRef: { current: number } = sendEpochRef,\n ) => {\n const epoch = epochRef.current;\n const next = sendChainRef.current.then(() => {\n if (!isMountedRef.current) throw sendAbandonedError;\n if (epoch !== epochRef.current) throw sendCancelledError;\n return dispatch();\n });\n sendChainRef.current = next.catch(() => {});\n return next;\n };\n\n // The store outlives the component (useEveAgent holds it in a ref with no\n // cleanup), so queued sends must not fire server turns after unmount. The\n // flag separates that teardown from a user cancel, and is re-armed in setup\n // for a remounted tree.\n useEffect(() => {\n isMountedRef.current = true;\n return () => {\n isMountedRef.current = false;\n sendEpochRef.current += 1;\n };\n }, []);\n\n useEffect(() => {\n if (stagedInputsRef.current.size === 0) return;\n const baseIds = new Set(convertedMessages.map((message) => message.id));\n const remaining = messagesRef.current.filter(\n (message) =>\n stagedInputsRef.current.has(message.id) && !baseIds.has(message.id),\n );\n setStagedMessages(\n remaining.length > 0 ? [...convertedMessages, ...remaining] : null,\n );\n }, [convertedMessages]);\n\n const getStagedRun = (parentId: string | null) => {\n if (!parentId || !stagedInputsRef.current.has(parentId)) return null;\n const staged: {\n message: ThreadMessage;\n input: { message: AppendMessage; runConfig: AppendMessage[\"runConfig\"] };\n }[] = [];\n for (const message of messagesRef.current) {\n const input = stagedInputsRef.current.get(message.id);\n if (input) staged.push({ message, input });\n if (message.id === parentId) break;\n }\n return staged;\n };\n\n const stageUserMessage = (message: AppendMessage) => {\n const threadMessage = fromThreadMessageLike(\n message,\n generateId(),\n USER_STAGED_STATUS,\n );\n stagedInputsRef.current.set(threadMessage.id, {\n message,\n runConfig: message.runConfig,\n });\n const nextMessages = [...messagesRef.current, threadMessage];\n messagesRef.current = nextMessages;\n setStagedMessages(nextMessages);\n };\n\n const reset = useCallback(() => {\n runtimeRef.current?.thread.unstable_notifySessionReset();\n // Sends parked behind an active turn captured the pre-reset epoch, so the\n // epoch has to advance before the session is torn down or they dispatch\n // into the new one; `lastFinishStatusRef` is run-scoped for the same\n // reason.\n sendEpochRef.current += 1;\n resetEpochRef.current += 1;\n lastFinishStatusRef.current = null;\n setStagedMessages(null);\n stagedInputsRef.current.clear();\n setToolStatuses({});\n agent.reset();\n }, [agent]);\n\n const extras = useMemo(\n () =>\n eveExtras.provide({\n error: agent.error,\n events: agent.events,\n session: agent.session,\n reset,\n }),\n [agent.error, agent.events, agent.session, reset],\n );\n\n const runtime = useExternalStoreRuntime({\n ...pickExternalStoreSharedOptions(options),\n messages,\n isRunning: providerIsRunning,\n extras,\n unstable_enableToolInvocations: true,\n setToolStatuses,\n adapters: {\n attachments: adapters?.attachments ?? runtimeAdapters?.attachments,\n speech: adapters?.speech,\n dictation: adapters?.dictation,\n voice: adapters?.voice,\n feedback: adapters?.feedback,\n },\n onNew: async (message) => {\n if (!(message.startRun ?? message.role === \"user\")) {\n stageUserMessage(message);\n return;\n }\n try {\n await enqueueSend(() =>\n agent.send(\n getEveMessageContent(message),\n toEveSendOptions(message.runConfig),\n ),\n );\n } catch (error) {\n // A cancelled send never reached the session, so it rethrows for the\n // composer to take the draft back; an unmounted one has no composer\n // left to restore.\n if (error === sendAbandonedError) return;\n throw error;\n }\n },\n ...(stagedMessages\n ? {\n onReload: async (parentId: string | null, config) => {\n const stagedRun = getStagedRun(parentId);\n if (!stagedRun)\n throw new Error(\"Runtime does not support reloading messages.\");\n const epoch = sendEpochRef.current;\n const resetEpoch = resetEpochRef.current;\n for (const { message: stagedMessage, input } of stagedRun) {\n if (epoch !== sendEpochRef.current) return;\n const previousMessages = messagesRef.current;\n stagedInputsRef.current.delete(stagedMessage.id);\n const nextMessages = previousMessages.filter(\n (message) => message.id !== stagedMessage.id,\n );\n messagesRef.current = nextMessages;\n setStagedMessages(\n stagedInputsRef.current.size > 0 ? nextMessages : null,\n );\n // The reload config belongs to the message being reloaded; the\n // drafts promoted ahead of it keep the config they were staged\n // with, or reloading the tail would rewrite their context too.\n const runConfig =\n stagedMessage.id === parentId && hasRunConfig(config.runConfig)\n ? config.runConfig\n : input.runConfig;\n try {\n await enqueueSend(() =>\n agent.send(\n getEveMessageContent(input.message),\n toEveSendOptions(runConfig),\n ),\n );\n } catch (error) {\n // A reset discarded the draft along with the session, so\n // restoring it here would refloat a message the thread no\n // longer has. A cancel still restores.\n if (resetEpoch !== resetEpochRef.current) return;\n stagedInputsRef.current.set(stagedMessage.id, input);\n messagesRef.current = previousMessages;\n setStagedMessages(previousMessages);\n if (isDroppedSend(error)) return;\n throw error;\n }\n if (lastFinishStatusRef.current === \"error\") return;\n }\n },\n }\n : {}),\n onCancel: async () => {\n sendEpochRef.current += 1;\n // Eve 0.38 replaced the binding's local-abort `stop()` with the durable\n // `cancel()`, so the adapter detects which side of that break the host's\n // eve provides instead of pinning the peer range to one of them.\n const controls = agent as\n | { readonly cancel: () => Promise<unknown> }\n | { readonly stop: () => void };\n if (\"cancel\" in controls) {\n await controls.cancel();\n } else {\n controls.stop();\n }\n },\n // Hosts below eve 0.44.1 expose no `resume`; leaving the capability absent\n // keeps `threads.reloadMainThread()` on core's no-capability no-op.\n ...(\"resume\" in agent\n ? {\n onRefetchThread: () =>\n // `resume()` rejects during a turn, so the replay rides the send\n // chain like every other dispatch and runs once the turn parks; a\n // cancel of that turn does not drop it, or the caller would be told\n // the thread was refetched when it was not. Upstream shares one\n // replay across concurrent `resume()` calls, so a refetch\n // dispatched while `resume: true` is replaying on mount joins that\n // read instead of taking a fresh one. The session is read at\n // dispatch time because nothing durable exists before the first\n // send lands.\n enqueueSend(async () => {\n const live = agentRef.current;\n if (live.session === undefined) return;\n await live.resume();\n }, resetEpochRef).catch((error) => {\n if (isDroppedSend(error)) return;\n throw error;\n }),\n }\n : {}),\n onRespondToToolApproval: (response) => {\n // Eve resolves a request the moment any response for it arrives, and an\n // empty one is recorded as an answer with no content. Mapping before the\n // send is enqueued keeps an unmappable response unsent, so the request\n // stays answerable; the mapper's error reaches the caller as the seam's\n // rejection.\n const inputResponse = toEveInputResponse(\n response,\n findEveInputRequest(agent.data, response.approvalId),\n );\n return enqueueSend(() => agent.respond([inputResponse])).catch(\n (error) => {\n if (!isDroppedSend(error)) throw error;\n },\n );\n },\n });\n runtimeRef.current = runtime;\n\n return runtime;\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"useEveAgentRuntime.js","names":[],"sources":["../src/useEveAgentRuntime.ts"],"sourcesContent":["\"use client\";\n\nimport {\n useCallback,\n useEffect,\n useInsertionEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport {\n fromThreadMessageLike,\n generateId,\n MessageNotSentError,\n pickExternalStoreSharedOptions,\n type AppendMessage,\n type AttachmentAdapter,\n type DictationAdapter,\n type ExternalStoreSharedOptions,\n type FeedbackAdapter,\n type RealtimeVoiceAdapter,\n type SpeechSynthesisAdapter,\n type ThreadMessage,\n type ToolExecutionStatus,\n} from \"@assistant-ui/core\";\nimport {\n useExternalStoreRuntime,\n useRuntimeAdapters,\n} from \"@assistant-ui/core/react\";\nimport {\n useEveAgent,\n type EveMessageData,\n type UseEveAgentOptions,\n type UseEveAgentStatus,\n} from \"eve/react\";\nimport {\n convertEveMessages,\n findEveInputRequest,\n getEveMessageContent,\n toEveInputResponse,\n} from \"./convertEveMessages\";\nimport {\n collectTurnTimestamps,\n createTurnTimestampCache,\n} from \"./deriveCreatedAt\";\nimport { eveExtras } from \"./eveExtras\";\n\nconst USER_STAGED_STATUS = {\n type: \"complete\",\n reason: \"unknown\",\n} as const;\n\nconst sendCancelledError = new MessageNotSentError(\n \"eve send was dropped because the run was cancelled.\",\n);\n\nconst sendAbandonedError = new Error(\n \"eve send was dropped because the runtime unmounted.\",\n);\n\nconst isDroppedSend = (error: unknown) =>\n error === sendCancelledError || error === sendAbandonedError;\n\ntype EveLifecycleCallbackName =\n | \"onError\"\n | \"onEvent\"\n | \"onFinish\"\n | \"onSessionChange\";\n\nconst reportEveLifecycleCallbackError = (\n name: EveLifecycleCallbackName,\n error: unknown,\n) => {\n console.error(`[assistant-ui/eve] ${name} callback threw an error`, error);\n};\n\nconst invokeEveLifecycleCallback = <T>(\n name: EveLifecycleCallbackName,\n callback: ((value: T) => unknown) | undefined,\n value: T,\n) => {\n if (!callback) return;\n\n try {\n void Promise.resolve(callback(value)).catch((error) => {\n reportEveLifecycleCallbackError(name, error);\n });\n } catch (error) {\n reportEveLifecycleCallbackError(name, error);\n }\n};\n\nconst hasRunConfig = (\n runConfig: AppendMessage[\"runConfig\"],\n): runConfig is NonNullable<AppendMessage[\"runConfig\"]> =>\n runConfig?.custom !== undefined && Object.keys(runConfig.custom).length > 0;\n\ntype EveJsonValue =\n | string\n | number\n | boolean\n | null\n | readonly EveJsonValue[]\n | { readonly [key: string]: EveJsonValue };\n\n/**\n * Structural equivalent of eve's `JsonObject`. Eve demoted its public send\n * payload type once already (`SendTurnPayload`, public through 0.30, internal\n * from 0.31), so the helpers declare the shapes they forward instead of\n * re-exporting eve's option types, and assignability is checked at the send\n * call sites against the installed version.\n */\ntype EveClientContext = { readonly [key: string]: EveJsonValue };\n\n/**\n * Only the `custom` bag crosses the wire. Eve reads `clientContext` as its own\n * namespace and serializes it into a model-visible context message, so sending\n * the assistant-ui envelope would surface a literal `\"custom\"` key in the\n * prompt and to every eve-side handler.\n */\nconst toEveSendOptions = (\n runConfig: AppendMessage[\"runConfig\"],\n): { readonly clientContext: EveClientContext } | undefined =>\n hasRunConfig(runConfig)\n ? { clientContext: runConfig.custom as EveClientContext }\n : undefined;\n\nexport type UseEveAgentRuntimeOptions = Omit<\n UseEveAgentOptions<EveMessageData>,\n \"reducer\"\n> &\n ExternalStoreSharedOptions & {\n readonly adapters?:\n | {\n readonly attachments?: AttachmentAdapter | undefined;\n readonly speech?: SpeechSynthesisAdapter | undefined;\n readonly dictation?: DictationAdapter | undefined;\n readonly voice?: RealtimeVoiceAdapter | undefined;\n readonly feedback?: FeedbackAdapter | undefined;\n }\n | undefined;\n };\n\n/**\n * Connects Eve's `useEveAgent` hook to assistant-ui's runtime contract.\n *\n * The runtime renders Eve messages, forwards new user messages to the Eve\n * session, supports cancellation, and maps Eve input requests to assistant-ui\n * tool approval UI.\n */\nexport const useEveAgentRuntime = (options: UseEveAgentRuntimeOptions = {}) => {\n const {\n adapters,\n isDisabled: _isDisabled,\n isSendDisabled: _isSendDisabled,\n suggestions: _suggestions,\n unstable_capabilities: _unstable_capabilities,\n ...agentOptions\n } = options;\n true satisfies keyof typeof agentOptions &\n keyof ExternalStoreSharedOptions extends never\n ? true\n : never;\n\n const { onError, onEvent, onFinish, onSessionChange } = agentOptions;\n const lastFinishStatusRef = useRef<UseEveAgentStatus | null>(null);\n const agent = useEveAgent({\n ...agentOptions,\n ...(onError\n ? {\n onError: (error) =>\n invokeEveLifecycleCallback(\"onError\", onError, error),\n }\n : {}),\n ...(onEvent\n ? {\n onEvent: (event) =>\n invokeEveLifecycleCallback(\"onEvent\", onEvent, event),\n }\n : {}),\n onFinish: (snapshot) => {\n lastFinishStatusRef.current = snapshot.status;\n invokeEveLifecycleCallback(\"onFinish\", onFinish, snapshot);\n },\n ...(onSessionChange\n ? {\n onSessionChange: (session) =>\n invokeEveLifecycleCallback(\n \"onSessionChange\",\n onSessionChange,\n session,\n ),\n }\n : {}),\n });\n const runtimeAdapters = useRuntimeAdapters();\n const [toolStatuses, setToolStatuses] = useState<\n Record<string, ToolExecutionStatus>\n >({});\n const [stagedMessages, setStagedMessages] = useState<ThreadMessage[] | null>(\n null,\n );\n const createdAtByMessageIdRef = useRef(new Map<string, Date>());\n const stagedInputsRef = useRef(\n new Map<\n string,\n { message: AppendMessage; runConfig: AppendMessage[\"runConfig\"] }\n >(),\n );\n\n const hasExecutingTools = Object.values(toolStatuses).some(\n (status) => status?.type === \"executing\",\n );\n const providerIsRunning =\n agent.status === \"submitted\" || agent.status === \"streaming\";\n const isRunning = providerIsRunning || hasExecutingTools;\n\n // Kept apart from the message memo so events that teach no timestamp keep\n // the map identity and skip rebuilding every ThreadMessage.\n const turnTimestampCacheRef = useRef(createTurnTimestampCache());\n const turnTimestamps = useMemo(\n () => collectTurnTimestamps(agent.events, turnTimestampCacheRef.current),\n [agent.events],\n );\n\n const convertedMessages = useMemo(() => {\n const createdAtByMessageId = createdAtByMessageIdRef.current;\n const messageIds = new Set(\n agent.data.messages.map((message) => message.id),\n );\n for (const messageId of createdAtByMessageId.keys()) {\n if (!messageIds.has(messageId)) createdAtByMessageId.delete(messageId);\n }\n\n return convertEveMessages(agent.data, {\n isRunning,\n error: agent.error,\n getCreatedAt: (message) => {\n const turnId = message.metadata?.turnId;\n const durable =\n turnId === undefined\n ? undefined\n : turnTimestamps.get(turnId)?.[message.role];\n if (durable !== undefined) return durable;\n\n const existing = createdAtByMessageId.get(message.id);\n if (existing) return existing;\n\n const createdAt = new Date();\n createdAtByMessageId.set(message.id, createdAt);\n return createdAt;\n },\n });\n }, [agent.data, agent.error, isRunning, turnTimestamps]);\n\n const messages = stagedMessages ?? convertedMessages;\n const messagesRef = useRef(messages);\n const agentRef = useRef(agent);\n // Descendant layout effects can dispatch against these refs synchronously;\n // this matches useA2ARuntime's commit-scoped ref publication.\n useInsertionEffect(() => {\n messagesRef.current = messages;\n agentRef.current = agent;\n }, [agent, messages]);\n\n // Upstream `EveAgentStore` `send` and `respond` reject while a turn is in\n // flight and only resolve once the turn's stream parks, so a pending chain\n // link is exactly an active turn; chaining every dispatch serializes them\n // without watching status.\n const sendChainRef = useRef<Promise<void>>(Promise.resolve());\n const sendEpochRef = useRef(0);\n // A cancel drops the queued send but keeps its draft for a later promotion;\n // only a reset discards the draft with the session, so the two need separate\n // counters. A queued read has no draft and outlives a cancel, so it waits on\n // the reset counter instead.\n const resetEpochRef = useRef(0);\n const isMountedRef = useRef(true);\n const runtimeRef = useRef<ReturnType<typeof useExternalStoreRuntime> | null>(\n null,\n );\n\n const enqueueSend = (\n dispatch: () => Promise<void>,\n epochRef: { current: number } = sendEpochRef,\n ) => {\n const epoch = epochRef.current;\n const next = sendChainRef.current.then(() => {\n if (!isMountedRef.current) throw sendAbandonedError;\n if (epoch !== epochRef.current) throw sendCancelledError;\n return dispatch();\n });\n sendChainRef.current = next.catch(() => {});\n return next;\n };\n\n // The store outlives the component (useEveAgent holds it in a ref with no\n // cleanup), so queued sends must not fire server turns after unmount. The\n // flag separates that teardown from a user cancel, and is re-armed in setup\n // for a remounted tree.\n useEffect(() => {\n isMountedRef.current = true;\n return () => {\n isMountedRef.current = false;\n sendEpochRef.current += 1;\n };\n }, []);\n\n useEffect(() => {\n if (stagedInputsRef.current.size === 0) return;\n const baseIds = new Set(convertedMessages.map((message) => message.id));\n const remaining = messagesRef.current.filter(\n (message) =>\n stagedInputsRef.current.has(message.id) && !baseIds.has(message.id),\n );\n setStagedMessages(\n remaining.length > 0 ? [...convertedMessages, ...remaining] : null,\n );\n }, [convertedMessages]);\n\n const getStagedRun = (parentId: string | null) => {\n if (!parentId || !stagedInputsRef.current.has(parentId)) return null;\n const staged: {\n message: ThreadMessage;\n input: { message: AppendMessage; runConfig: AppendMessage[\"runConfig\"] };\n }[] = [];\n for (const message of messagesRef.current) {\n const input = stagedInputsRef.current.get(message.id);\n if (input) staged.push({ message, input });\n if (message.id === parentId) break;\n }\n return staged;\n };\n\n const stageUserMessage = (message: AppendMessage) => {\n const threadMessage = fromThreadMessageLike(\n message,\n generateId(),\n USER_STAGED_STATUS,\n );\n stagedInputsRef.current.set(threadMessage.id, {\n message,\n runConfig: message.runConfig,\n });\n const nextMessages = [...messagesRef.current, threadMessage];\n messagesRef.current = nextMessages;\n setStagedMessages(nextMessages);\n };\n\n const reset = useCallback(() => {\n runtimeRef.current?.thread.unstable_notifySessionReset();\n // Sends parked behind an active turn captured the pre-reset epoch, so the\n // epoch has to advance before the session is torn down or they dispatch\n // into the new one; `lastFinishStatusRef` is run-scoped for the same\n // reason.\n sendEpochRef.current += 1;\n resetEpochRef.current += 1;\n lastFinishStatusRef.current = null;\n setStagedMessages(null);\n stagedInputsRef.current.clear();\n setToolStatuses({});\n agent.reset();\n }, [agent]);\n\n const extras = useMemo(\n () =>\n eveExtras.provide({\n error: agent.error,\n events: agent.events,\n session: agent.session,\n reset,\n }),\n [agent.error, agent.events, agent.session, reset],\n );\n\n const runtime = useExternalStoreRuntime({\n ...pickExternalStoreSharedOptions(options),\n messages,\n isRunning: providerIsRunning,\n extras,\n unstable_enableToolInvocations: true,\n setToolStatuses,\n adapters: {\n attachments: adapters?.attachments ?? runtimeAdapters?.attachments,\n speech: adapters?.speech,\n dictation: adapters?.dictation,\n voice: adapters?.voice,\n feedback: adapters?.feedback,\n },\n onNew: async (message) => {\n if (!(message.startRun ?? message.role === \"user\")) {\n stageUserMessage(message);\n return;\n }\n try {\n await enqueueSend(() =>\n agent.send(\n getEveMessageContent(message),\n toEveSendOptions(message.runConfig),\n ),\n );\n } catch (error) {\n // A cancelled send never reached the session, so it rethrows for the\n // composer to take the draft back; an unmounted one has no composer\n // left to restore.\n if (error === sendAbandonedError) return;\n throw error;\n }\n },\n ...(stagedMessages\n ? {\n onReload: async (parentId: string | null, config) => {\n const stagedRun = getStagedRun(parentId);\n if (!stagedRun)\n throw new Error(\"Runtime does not support reloading messages.\");\n const epoch = sendEpochRef.current;\n const resetEpoch = resetEpochRef.current;\n for (const { message: stagedMessage, input } of stagedRun) {\n if (epoch !== sendEpochRef.current) return;\n const previousMessages = messagesRef.current;\n stagedInputsRef.current.delete(stagedMessage.id);\n const nextMessages = previousMessages.filter(\n (message) => message.id !== stagedMessage.id,\n );\n messagesRef.current = nextMessages;\n setStagedMessages(\n stagedInputsRef.current.size > 0 ? nextMessages : null,\n );\n // The reload config belongs to the message being reloaded; the\n // drafts promoted ahead of it keep the config they were staged\n // with, or reloading the tail would rewrite their context too.\n const runConfig =\n stagedMessage.id === parentId && hasRunConfig(config.runConfig)\n ? config.runConfig\n : input.runConfig;\n try {\n await enqueueSend(() =>\n agent.send(\n getEveMessageContent(input.message),\n toEveSendOptions(runConfig),\n ),\n );\n } catch (error) {\n // A reset discarded the draft along with the session, so\n // restoring it here would refloat a message the thread no\n // longer has. A cancel still restores.\n if (resetEpoch !== resetEpochRef.current) return;\n stagedInputsRef.current.set(stagedMessage.id, input);\n messagesRef.current = previousMessages;\n setStagedMessages(previousMessages);\n if (isDroppedSend(error)) return;\n throw error;\n }\n if (lastFinishStatusRef.current === \"error\") return;\n }\n },\n }\n : {}),\n onCancel: async () => {\n sendEpochRef.current += 1;\n // Eve 0.38 replaced the binding's local-abort `stop()` with the durable\n // `cancel()`, so the adapter detects which side of that break the host's\n // eve provides instead of pinning the peer range to one of them.\n const controls = agent as\n | { readonly cancel: () => Promise<unknown> }\n | { readonly stop: () => void };\n if (\"cancel\" in controls) {\n await controls.cancel();\n } else {\n controls.stop();\n }\n },\n // Hosts below eve 0.44.1 expose no `resume`; leaving the capability absent\n // keeps `threads.reloadMainThread()` on core's no-capability no-op.\n ...(\"resume\" in agent\n ? {\n onRefetchThread: () =>\n // `resume()` rejects during a turn, so the replay rides the send\n // chain like every other dispatch and runs once the turn parks; a\n // cancel of that turn does not drop it, or the caller would be told\n // the thread was refetched when it was not. Upstream shares one\n // replay across concurrent `resume()` calls, so a refetch\n // dispatched while `resume: true` is replaying on mount joins that\n // read instead of taking a fresh one. The session is read at\n // dispatch time because nothing durable exists before the first\n // send lands.\n enqueueSend(async () => {\n const live = agentRef.current;\n if (live.session === undefined) return;\n await live.resume();\n }, resetEpochRef).catch((error) => {\n if (isDroppedSend(error)) return;\n throw error;\n }),\n }\n : {}),\n onRespondToToolApproval: (response) => {\n // Eve resolves a request the moment any response for it arrives, and an\n // empty one is recorded as an answer with no content. Mapping before the\n // send is enqueued keeps an unmappable response unsent, so the request\n // stays answerable; the mapper's error reaches the caller as the seam's\n // rejection.\n const inputResponse = toEveInputResponse(\n response,\n findEveInputRequest(agent.data, response.approvalId),\n );\n return enqueueSend(() => agent.respond([inputResponse])).catch(\n (error) => {\n if (!isDroppedSend(error)) throw error;\n },\n );\n },\n });\n runtimeRef.current = runtime;\n\n return runtime;\n};\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@assistant-ui/eve",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.18",
|
|
4
4
|
"description": "Eve runtime adapter for assistant-ui",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"eve",
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
],
|
|
29
29
|
"sideEffects": false,
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@assistant-ui/core": "^0.3.
|
|
32
|
-
"@assistant-ui/store": "^0.3.
|
|
31
|
+
"@assistant-ui/core": "^0.3.20",
|
|
32
|
+
"@assistant-ui/store": "^0.3.14"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
35
|
"@types/react": "*",
|
|
@@ -42,15 +42,15 @@
|
|
|
42
42
|
}
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
|
-
"@assistant-ui/x-buildutils": "0.0.
|
|
46
|
-
"@testing-library/dom": "^10.4.
|
|
45
|
+
"@assistant-ui/x-buildutils": "0.0.28",
|
|
46
|
+
"@testing-library/dom": "^10.4.2",
|
|
47
47
|
"@testing-library/react": "^16.3.3",
|
|
48
|
-
"@types/react": "^19.
|
|
49
|
-
"eve": "^0.
|
|
48
|
+
"@types/react": "^19.3.0",
|
|
49
|
+
"eve": "^0.56.0",
|
|
50
50
|
"jsdom": "^30.0.1",
|
|
51
|
-
"react": "^19.
|
|
52
|
-
"react-dom": "^19.
|
|
53
|
-
"vitest": "^
|
|
51
|
+
"react": "^19.3.0",
|
|
52
|
+
"react-dom": "^19.3.0",
|
|
53
|
+
"vitest": "^5.0.1"
|
|
54
54
|
},
|
|
55
55
|
"publishConfig": {
|
|
56
56
|
"access": "public",
|
|
@@ -68,6 +68,7 @@
|
|
|
68
68
|
"scripts": {
|
|
69
69
|
"build": "aui-build",
|
|
70
70
|
"test": "vitest run",
|
|
71
|
+
"typecheck": "tsc --noEmit",
|
|
71
72
|
"test:watch": "vitest"
|
|
72
73
|
}
|
|
73
74
|
}
|
|
@@ -1326,7 +1326,6 @@ describe("convertEveMessages", () => {
|
|
|
1326
1326
|
stepIndex: 0,
|
|
1327
1327
|
sequence: 2,
|
|
1328
1328
|
messageDelta: "Let me th",
|
|
1329
|
-
messageSoFar: "Let me th",
|
|
1330
1329
|
},
|
|
1331
1330
|
},
|
|
1332
1331
|
];
|
|
@@ -1519,7 +1518,6 @@ describe("convertEveMessages", () => {
|
|
|
1519
1518
|
stepIndex: 0,
|
|
1520
1519
|
sequence: 2,
|
|
1521
1520
|
reasoningDelta: "Think",
|
|
1522
|
-
reasoningSoFar: "Think",
|
|
1523
1521
|
},
|
|
1524
1522
|
},
|
|
1525
1523
|
{
|
|
@@ -1570,7 +1568,6 @@ describe("convertEveMessages", () => {
|
|
|
1570
1568
|
stepIndex: 0,
|
|
1571
1569
|
sequence: 2,
|
|
1572
1570
|
reasoningDelta: "Think",
|
|
1573
|
-
reasoningSoFar: "Think",
|
|
1574
1571
|
},
|
|
1575
1572
|
},
|
|
1576
1573
|
{
|
|
@@ -1633,7 +1630,6 @@ describe("convertEveMessages", () => {
|
|
|
1633
1630
|
stepIndex: 0,
|
|
1634
1631
|
sequence: 2,
|
|
1635
1632
|
messageDelta: "First",
|
|
1636
|
-
messageSoFar: "First",
|
|
1637
1633
|
},
|
|
1638
1634
|
},
|
|
1639
1635
|
{
|
|
@@ -1675,7 +1671,6 @@ describe("convertEveMessages", () => {
|
|
|
1675
1671
|
stepIndex: 1,
|
|
1676
1672
|
sequence: 6,
|
|
1677
1673
|
messageDelta: "Sec",
|
|
1678
|
-
messageSoFar: "Sec",
|
|
1679
1674
|
},
|
|
1680
1675
|
},
|
|
1681
1676
|
]);
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { MessageStreamEvent } from "eve/client";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
collectTurnTimestamps,
|
|
6
|
+
createTurnTimestampCache,
|
|
7
|
+
} from "./deriveCreatedAt";
|
|
8
|
+
|
|
9
|
+
const TURN = "turn_1";
|
|
10
|
+
|
|
11
|
+
const turnStarted = (at: string, turnId = TURN) =>
|
|
12
|
+
({
|
|
13
|
+
type: "turn.started",
|
|
14
|
+
data: { sequence: 1, turnId },
|
|
15
|
+
meta: { at, id: `evt_${at}` },
|
|
16
|
+
}) as const satisfies MessageStreamEvent;
|
|
17
|
+
|
|
18
|
+
const messageReceived = (at: string, turnId = TURN) =>
|
|
19
|
+
({
|
|
20
|
+
type: "message.received",
|
|
21
|
+
data: { message: "hi", sequence: 2, turnId },
|
|
22
|
+
meta: { at, id: `evt_${at}` },
|
|
23
|
+
}) as const satisfies MessageStreamEvent;
|
|
24
|
+
|
|
25
|
+
const compactionRequested = (at: string, turnId = TURN) =>
|
|
26
|
+
({
|
|
27
|
+
type: "compaction.requested",
|
|
28
|
+
data: {
|
|
29
|
+
modelId: "m",
|
|
30
|
+
sequence: 3,
|
|
31
|
+
sessionId: "s",
|
|
32
|
+
turnId,
|
|
33
|
+
usageInputTokens: null,
|
|
34
|
+
},
|
|
35
|
+
meta: { at, id: `evt_${at}` },
|
|
36
|
+
}) as const satisfies MessageStreamEvent;
|
|
37
|
+
|
|
38
|
+
const messageAppended = (at: string, turnId = TURN) =>
|
|
39
|
+
({
|
|
40
|
+
type: "message.appended",
|
|
41
|
+
data: {
|
|
42
|
+
messageDelta: "he",
|
|
43
|
+
sequence: 4,
|
|
44
|
+
stepIndex: 0,
|
|
45
|
+
turnId,
|
|
46
|
+
},
|
|
47
|
+
meta: { at, id: `evt_${at}` },
|
|
48
|
+
}) as const satisfies MessageStreamEvent;
|
|
49
|
+
|
|
50
|
+
const stepStarted = (at: string, turnId = TURN) =>
|
|
51
|
+
({
|
|
52
|
+
type: "step.started",
|
|
53
|
+
data: { modelId: "m", sequence: 4, stepIndex: 0, turnId },
|
|
54
|
+
meta: { at, id: `evt_${at}` },
|
|
55
|
+
}) as const satisfies MessageStreamEvent;
|
|
56
|
+
|
|
57
|
+
describe("collectTurnTimestamps", () => {
|
|
58
|
+
it("stamps the user at message.received and the assistant at step.started, skipping the turn's other events", () => {
|
|
59
|
+
const timestamps = collectTurnTimestamps(
|
|
60
|
+
[
|
|
61
|
+
turnStarted("2026-01-02T10:00:00.000Z"),
|
|
62
|
+
messageReceived("2026-01-02T10:00:01.000Z"),
|
|
63
|
+
compactionRequested("2026-01-02T10:00:02.000Z"),
|
|
64
|
+
stepStarted("2026-01-02T10:02:00.000Z"),
|
|
65
|
+
stepStarted("2026-01-02T10:03:00.000Z"),
|
|
66
|
+
],
|
|
67
|
+
createTurnTimestampCache(),
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
expect(timestamps.get(TURN)?.user).toEqual(
|
|
71
|
+
new Date("2026-01-02T10:00:01.000Z"),
|
|
72
|
+
);
|
|
73
|
+
expect(timestamps.get(TURN)?.assistant).toEqual(
|
|
74
|
+
new Date("2026-01-02T10:02:00.000Z"),
|
|
75
|
+
);
|
|
76
|
+
expect(timestamps.get("turn_other")?.user).toBe(undefined);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("resumes the scan at appended events and keeps the map identity when they teach nothing", () => {
|
|
80
|
+
const cache = createTurnTimestampCache();
|
|
81
|
+
const events = [
|
|
82
|
+
turnStarted("2026-01-02T10:00:00.000Z"),
|
|
83
|
+
messageReceived("2026-01-02T10:00:01.000Z"),
|
|
84
|
+
];
|
|
85
|
+
const first = collectTurnTimestamps(events, cache);
|
|
86
|
+
expect(first.get(TURN)?.assistant).toBe(undefined);
|
|
87
|
+
|
|
88
|
+
const quiet = [
|
|
89
|
+
...events,
|
|
90
|
+
turnStarted("2026-01-02T10:00:02.000Z", "turn_2"),
|
|
91
|
+
];
|
|
92
|
+
expect(collectTurnTimestamps(quiet, cache)).toBe(first);
|
|
93
|
+
|
|
94
|
+
const timestamps = collectTurnTimestamps(
|
|
95
|
+
[...quiet, stepStarted("2026-01-02T10:02:00.000Z")],
|
|
96
|
+
cache,
|
|
97
|
+
);
|
|
98
|
+
expect(timestamps.get(TURN)?.user).toEqual(
|
|
99
|
+
new Date("2026-01-02T10:00:01.000Z"),
|
|
100
|
+
);
|
|
101
|
+
expect(timestamps.get(TURN)?.assistant).toEqual(
|
|
102
|
+
new Date("2026-01-02T10:02:00.000Z"),
|
|
103
|
+
);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("stamps an assistant created by a turn ending before its first step", () => {
|
|
107
|
+
const timestamps = collectTurnTimestamps(
|
|
108
|
+
[
|
|
109
|
+
messageReceived("2026-01-02T10:00:01.000Z"),
|
|
110
|
+
{
|
|
111
|
+
type: "turn.cancelled",
|
|
112
|
+
data: { sequence: 3, turnId: TURN },
|
|
113
|
+
meta: { at: "2026-01-02T10:00:05.000Z", id: "evt_cancel" },
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
createTurnTimestampCache(),
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
expect(timestamps.get(TURN)?.assistant).toEqual(
|
|
120
|
+
new Date("2026-01-02T10:00:05.000Z"),
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("stamps the assistant from the turn's first message-creating event when no step.started precedes it", () => {
|
|
125
|
+
const timestamps = collectTurnTimestamps(
|
|
126
|
+
[
|
|
127
|
+
turnStarted("2026-01-02T10:00:00.000Z"),
|
|
128
|
+
messageReceived("2026-01-02T10:00:01.000Z"),
|
|
129
|
+
compactionRequested("2026-01-02T10:00:02.000Z"),
|
|
130
|
+
messageAppended("2026-01-02T10:02:00.000Z"),
|
|
131
|
+
],
|
|
132
|
+
createTurnTimestampCache(),
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
expect(timestamps.get(TURN)?.assistant).toEqual(
|
|
136
|
+
new Date("2026-01-02T10:02:00.000Z"),
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("skips malformed events without throwing", () => {
|
|
141
|
+
const events = [
|
|
142
|
+
{ type: "message.received", data: null, meta: { at: "x", id: "e1" } },
|
|
143
|
+
{ type: "step.started", meta: { at: "2026-01-01T00:00:00Z", id: "e2" } },
|
|
144
|
+
{
|
|
145
|
+
type: "message.received",
|
|
146
|
+
data: { turnId: 42 },
|
|
147
|
+
meta: { at: "x", id: "e3" },
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
type: "message.received",
|
|
151
|
+
data: { message: "hi", sequence: 2, turnId: TURN },
|
|
152
|
+
meta: { at: "not a date", id: "e4" },
|
|
153
|
+
},
|
|
154
|
+
{ type: "step.started", data: { stepIndex: 0, turnId: TURN } },
|
|
155
|
+
stepStarted("2026-01-02T10:02:00.000Z"),
|
|
156
|
+
] as never as readonly MessageStreamEvent[];
|
|
157
|
+
|
|
158
|
+
const timestamps = collectTurnTimestamps(
|
|
159
|
+
events,
|
|
160
|
+
createTurnTimestampCache(),
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
expect(timestamps.get(TURN)?.user).toBe(undefined);
|
|
164
|
+
expect(timestamps.get(TURN)?.assistant).toEqual(
|
|
165
|
+
new Date("2026-01-02T10:02:00.000Z"),
|
|
166
|
+
);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("re-derives from scratch when the log is replaced, so a recurring turn id serves the new session's stamps", () => {
|
|
170
|
+
const cache = createTurnTimestampCache();
|
|
171
|
+
collectTurnTimestamps(
|
|
172
|
+
[
|
|
173
|
+
messageReceived("2026-01-02T10:00:01.000Z"),
|
|
174
|
+
stepStarted("2026-01-02T10:02:00.000Z"),
|
|
175
|
+
],
|
|
176
|
+
cache,
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
const timestamps = collectTurnTimestamps(
|
|
180
|
+
[messageReceived("2026-03-01T09:00:01.000Z")],
|
|
181
|
+
cache,
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
expect(timestamps.get(TURN)?.user).toEqual(
|
|
185
|
+
new Date("2026-03-01T09:00:01.000Z"),
|
|
186
|
+
);
|
|
187
|
+
expect(timestamps.get(TURN)?.assistant).toBe(undefined);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { MessageStreamEvent } from "eve/client";
|
|
2
|
+
|
|
3
|
+
export type TurnTimestamps = {
|
|
4
|
+
readonly user?: Date;
|
|
5
|
+
readonly assistant?: Date;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export type TurnTimestampCache = {
|
|
9
|
+
lastEvents: readonly MessageStreamEvent[];
|
|
10
|
+
timestamps: ReadonlyMap<string, TurnTimestamps>;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The reducer cases eve projects into a message: `message.received` creates
|
|
15
|
+
* `${turnId}:user`, and every assistant entry reaches `updateAssistantMessage`,
|
|
16
|
+
* which creates `${turnId}:assistant` for a turn that has none. It enumerates
|
|
17
|
+
* that reducer across the `>=0.32.0` peer range rather than stating a rule
|
|
18
|
+
* about turn ids, because `turn.started` and the compaction events carry one
|
|
19
|
+
* before the model runs. An event eve adds to the creating set later is absent
|
|
20
|
+
* here and falls back to the wall clock instead of stamping a wrong time.
|
|
21
|
+
*/
|
|
22
|
+
const ROLE_BY_EVENT_TYPE: Partial<
|
|
23
|
+
Record<MessageStreamEvent["type"], keyof TurnTimestamps>
|
|
24
|
+
> = {
|
|
25
|
+
"message.received": "user",
|
|
26
|
+
"step.started": "assistant",
|
|
27
|
+
"reasoning.appended": "assistant",
|
|
28
|
+
"reasoning.completed": "assistant",
|
|
29
|
+
"action.input.appended": "assistant",
|
|
30
|
+
"actions.requested": "assistant",
|
|
31
|
+
"input.requested": "assistant",
|
|
32
|
+
"action.result": "assistant",
|
|
33
|
+
"action.partial": "assistant",
|
|
34
|
+
"authorization.required": "assistant",
|
|
35
|
+
"authorization.completed": "assistant",
|
|
36
|
+
"message.appended": "assistant",
|
|
37
|
+
"message.completed": "assistant",
|
|
38
|
+
"result.completed": "assistant",
|
|
39
|
+
"turn.completed": "assistant",
|
|
40
|
+
"turn.cancelled": "assistant",
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const createTurnTimestampCache = (): TurnTimestampCache => ({
|
|
44
|
+
lastEvents: [],
|
|
45
|
+
timestamps: new Map(),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Eve appends to its log (`[...events, event]`), so a snapshot that keeps the
|
|
50
|
+
* previous scan's last element by identity shares the whole prefix and the
|
|
51
|
+
* scan resumes there. Any other snapshot is re-derived into a fresh map: turn
|
|
52
|
+
* ids are per-session sequence numbers that recur after `reset()`, so entries
|
|
53
|
+
* must not outlive the log that produced them. Re-derivation is also what
|
|
54
|
+
* makes the render-phase cache writes safe when React discards a render.
|
|
55
|
+
*/
|
|
56
|
+
export const collectTurnTimestamps = (
|
|
57
|
+
events: readonly MessageStreamEvent[],
|
|
58
|
+
cache: TurnTimestampCache,
|
|
59
|
+
): ReadonlyMap<string, TurnTimestamps> => {
|
|
60
|
+
if (events === cache.lastEvents) return cache.timestamps;
|
|
61
|
+
|
|
62
|
+
const scanned = cache.lastEvents;
|
|
63
|
+
const resumesScan =
|
|
64
|
+
scanned.length === 0 ||
|
|
65
|
+
events[scanned.length - 1] === scanned[scanned.length - 1];
|
|
66
|
+
|
|
67
|
+
let timestamps = cache.timestamps;
|
|
68
|
+
let draft: Map<string, TurnTimestamps> | undefined;
|
|
69
|
+
if (!resumesScan) {
|
|
70
|
+
draft = new Map();
|
|
71
|
+
timestamps = draft;
|
|
72
|
+
}
|
|
73
|
+
for (let i = resumesScan ? scanned.length : 0; i < events.length; i++) {
|
|
74
|
+
const event = events[i]!;
|
|
75
|
+
const role = ROLE_BY_EVENT_TYPE[event.type];
|
|
76
|
+
if (role === undefined) continue;
|
|
77
|
+
const data: unknown = (event as { readonly data?: unknown }).data;
|
|
78
|
+
if (typeof data !== "object" || data === null) continue;
|
|
79
|
+
const turnId = (data as { readonly turnId?: unknown }).turnId;
|
|
80
|
+
if (typeof turnId !== "string") continue;
|
|
81
|
+
const known = timestamps.get(turnId);
|
|
82
|
+
if (known?.[role] !== undefined) continue;
|
|
83
|
+
const at: unknown = (event as { readonly meta?: { readonly at?: unknown } })
|
|
84
|
+
.meta?.at;
|
|
85
|
+
if (typeof at !== "string") continue;
|
|
86
|
+
const date = new Date(at);
|
|
87
|
+
if (Number.isNaN(date.getTime())) continue;
|
|
88
|
+
draft ??= new Map(timestamps);
|
|
89
|
+
draft.set(turnId, { ...known, [role]: date });
|
|
90
|
+
timestamps = draft;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
cache.lastEvents = events;
|
|
94
|
+
cache.timestamps = timestamps;
|
|
95
|
+
return timestamps;
|
|
96
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
// Deliberately does not mock `eve/react`: the real `useEveAgent` resumes an
|
|
4
|
+
// offline session so the durable `meta.at` values travel through eve's own
|
|
5
|
+
// store and reducer before the adapter reads them. Supplying `session` keeps
|
|
6
|
+
// the store from constructing a network client.
|
|
7
|
+
|
|
8
|
+
import { renderHook, waitFor } from "@testing-library/react";
|
|
9
|
+
import { describe, expect, it } from "vitest";
|
|
10
|
+
import type { MessageStreamEvent } from "eve/client";
|
|
11
|
+
|
|
12
|
+
import { useEveAgentRuntime } from "./useEveAgentRuntime";
|
|
13
|
+
|
|
14
|
+
const TURN = "turn_resumed";
|
|
15
|
+
|
|
16
|
+
const USER_AT = "2026-01-02T10:00:01.000Z";
|
|
17
|
+
const ASSISTANT_AT = "2026-01-02T10:02:00.000Z";
|
|
18
|
+
|
|
19
|
+
const resumedEvents = [
|
|
20
|
+
{
|
|
21
|
+
type: "turn.started",
|
|
22
|
+
data: { sequence: 1, turnId: TURN },
|
|
23
|
+
meta: { at: "2026-01-02T10:00:00.000Z", id: "evt_001" },
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
type: "message.received",
|
|
27
|
+
data: { message: "hi", sequence: 2, turnId: TURN },
|
|
28
|
+
meta: { at: USER_AT, id: "evt_101" },
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
type: "step.started",
|
|
32
|
+
data: { modelId: "test-model", sequence: 3, stepIndex: 0, turnId: TURN },
|
|
33
|
+
meta: { at: ASSISTANT_AT, id: "evt_102" },
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
type: "message.completed",
|
|
37
|
+
data: {
|
|
38
|
+
finishReason: "stop",
|
|
39
|
+
message: "hello",
|
|
40
|
+
sequence: 5,
|
|
41
|
+
stepIndex: 0,
|
|
42
|
+
turnId: TURN,
|
|
43
|
+
},
|
|
44
|
+
meta: { at: "2026-01-02T10:02:06.000Z", id: "evt_003" },
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
type: "turn.completed",
|
|
48
|
+
data: { sequence: 6, turnId: TURN },
|
|
49
|
+
meta: { at: "2026-01-02T10:02:07.000Z", id: "evt_004" },
|
|
50
|
+
},
|
|
51
|
+
] as const satisfies readonly MessageStreamEvent[];
|
|
52
|
+
|
|
53
|
+
const offlineSession = {
|
|
54
|
+
state: { sessionId: "session_resumed", streamIndex: 0 },
|
|
55
|
+
cancel: async () => ({ status: "no_active_turn" }),
|
|
56
|
+
send: () => {
|
|
57
|
+
throw new Error("the resume test must not reach the network");
|
|
58
|
+
},
|
|
59
|
+
stream: async function* () {
|
|
60
|
+
yield* JSON.parse(
|
|
61
|
+
JSON.stringify(resumedEvents),
|
|
62
|
+
) as readonly MessageStreamEvent[];
|
|
63
|
+
},
|
|
64
|
+
} as never;
|
|
65
|
+
|
|
66
|
+
describe("useEveAgentRuntime against a resumed eve session", () => {
|
|
67
|
+
it("renders resumed history at its durable event times, not the current time", async () => {
|
|
68
|
+
const { result } = renderHook(() =>
|
|
69
|
+
useEveAgentRuntime({ resume: true, session: offlineSession }),
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
await waitFor(() =>
|
|
73
|
+
expect(result.current.thread.getState().messages).toHaveLength(2),
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const messages = result.current.thread.getState().messages;
|
|
77
|
+
expect(messages[0]?.createdAt).toEqual(new Date(USER_AT));
|
|
78
|
+
expect(messages[1]?.createdAt).toEqual(new Date(ASSISTANT_AT));
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -18,6 +18,7 @@ vi.mock("eve/react", async (importOriginal) => ({
|
|
|
18
18
|
useEveAgent: mockUseEveAgent,
|
|
19
19
|
}));
|
|
20
20
|
|
|
21
|
+
import type { MessageStreamEvent } from "eve/client";
|
|
21
22
|
import type { EveMessageData } from "eve/react";
|
|
22
23
|
import { useEveAgentRuntime } from "./useEveAgentRuntime";
|
|
23
24
|
import { eveExtras } from "./eveExtras";
|
|
@@ -2152,3 +2153,88 @@ describe("useEveAgentRuntime thread refetch", () => {
|
|
|
2152
2153
|
});
|
|
2153
2154
|
});
|
|
2154
2155
|
});
|
|
2156
|
+
|
|
2157
|
+
describe("useEveAgentRuntime createdAt derivation", () => {
|
|
2158
|
+
const TURN = "turn_ts";
|
|
2159
|
+
const USER_AT = "2026-01-02T10:00:01.000Z";
|
|
2160
|
+
const ASSISTANT_AT = "2026-01-02T10:02:00.000Z";
|
|
2161
|
+
|
|
2162
|
+
const turnEvents = [
|
|
2163
|
+
{
|
|
2164
|
+
type: "turn.started",
|
|
2165
|
+
data: { sequence: 1, turnId: TURN },
|
|
2166
|
+
meta: { at: "2026-01-02T10:00:00.000Z", id: "evt_1" },
|
|
2167
|
+
},
|
|
2168
|
+
{
|
|
2169
|
+
type: "message.received",
|
|
2170
|
+
data: { message: "hi", sequence: 2, turnId: TURN },
|
|
2171
|
+
meta: { at: USER_AT, id: "evt_2" },
|
|
2172
|
+
},
|
|
2173
|
+
{
|
|
2174
|
+
type: "step.started",
|
|
2175
|
+
data: { modelId: "m", sequence: 3, stepIndex: 0, turnId: TURN },
|
|
2176
|
+
meta: { at: ASSISTANT_AT, id: "evt_3" },
|
|
2177
|
+
},
|
|
2178
|
+
] satisfies readonly MessageStreamEvent[];
|
|
2179
|
+
|
|
2180
|
+
const turnData: EveMessageData = {
|
|
2181
|
+
messages: [
|
|
2182
|
+
{
|
|
2183
|
+
id: `${TURN}:user`,
|
|
2184
|
+
role: "user",
|
|
2185
|
+
metadata: { turnId: TURN },
|
|
2186
|
+
parts: [{ type: "text", text: "hi" }],
|
|
2187
|
+
},
|
|
2188
|
+
{
|
|
2189
|
+
id: `${TURN}:assistant`,
|
|
2190
|
+
role: "assistant",
|
|
2191
|
+
metadata: { turnId: TURN },
|
|
2192
|
+
parts: [{ type: "text", text: "hello" }],
|
|
2193
|
+
},
|
|
2194
|
+
],
|
|
2195
|
+
};
|
|
2196
|
+
|
|
2197
|
+
it("stamps a message with the wall clock when no event covers its turn, and keeps that stamp across renders", () => {
|
|
2198
|
+
const before = Date.now();
|
|
2199
|
+
const agent = createAgent({ data: turnData, events: [] });
|
|
2200
|
+
mockUseEveAgent.mockReturnValue(agent as never);
|
|
2201
|
+
|
|
2202
|
+
const { result, rerender } = renderHook(() => useEveAgentRuntime());
|
|
2203
|
+
|
|
2204
|
+
const stamped = result.current.thread.getState().messages[0]?.createdAt;
|
|
2205
|
+
expect(stamped?.getTime()).toBeGreaterThanOrEqual(before);
|
|
2206
|
+
|
|
2207
|
+
mockUseEveAgent.mockReturnValue({
|
|
2208
|
+
...agent,
|
|
2209
|
+
data: { messages: [...turnData.messages] },
|
|
2210
|
+
} as never);
|
|
2211
|
+
rerender();
|
|
2212
|
+
|
|
2213
|
+
expect(result.current.thread.getState().messages[0]?.createdAt).toBe(
|
|
2214
|
+
stamped,
|
|
2215
|
+
);
|
|
2216
|
+
});
|
|
2217
|
+
|
|
2218
|
+
it("keeps the message list identity when appended events stamp no message", () => {
|
|
2219
|
+
const agent = createAgent({ data: turnData, events: turnEvents });
|
|
2220
|
+
mockUseEveAgent.mockReturnValue(agent as never);
|
|
2221
|
+
|
|
2222
|
+
const { result, rerender } = renderHook(() => useEveAgentRuntime());
|
|
2223
|
+
const first = result.current.thread.getState().messages;
|
|
2224
|
+
|
|
2225
|
+
mockUseEveAgent.mockReturnValue({
|
|
2226
|
+
...agent,
|
|
2227
|
+
events: [
|
|
2228
|
+
...turnEvents,
|
|
2229
|
+
{
|
|
2230
|
+
type: "turn.started",
|
|
2231
|
+
data: { sequence: 4, turnId: "turn_next" },
|
|
2232
|
+
meta: { at: "2026-01-02T10:05:00.000Z", id: "evt_4" },
|
|
2233
|
+
},
|
|
2234
|
+
],
|
|
2235
|
+
} as never);
|
|
2236
|
+
rerender();
|
|
2237
|
+
|
|
2238
|
+
expect(result.current.thread.getState().messages).toBe(first);
|
|
2239
|
+
});
|
|
2240
|
+
});
|
|
@@ -39,6 +39,10 @@ import {
|
|
|
39
39
|
getEveMessageContent,
|
|
40
40
|
toEveInputResponse,
|
|
41
41
|
} from "./convertEveMessages";
|
|
42
|
+
import {
|
|
43
|
+
collectTurnTimestamps,
|
|
44
|
+
createTurnTimestampCache,
|
|
45
|
+
} from "./deriveCreatedAt";
|
|
42
46
|
import { eveExtras } from "./eveExtras";
|
|
43
47
|
|
|
44
48
|
const USER_STAGED_STATUS = {
|
|
@@ -211,6 +215,14 @@ export const useEveAgentRuntime = (options: UseEveAgentRuntimeOptions = {}) => {
|
|
|
211
215
|
agent.status === "submitted" || agent.status === "streaming";
|
|
212
216
|
const isRunning = providerIsRunning || hasExecutingTools;
|
|
213
217
|
|
|
218
|
+
// Kept apart from the message memo so events that teach no timestamp keep
|
|
219
|
+
// the map identity and skip rebuilding every ThreadMessage.
|
|
220
|
+
const turnTimestampCacheRef = useRef(createTurnTimestampCache());
|
|
221
|
+
const turnTimestamps = useMemo(
|
|
222
|
+
() => collectTurnTimestamps(agent.events, turnTimestampCacheRef.current),
|
|
223
|
+
[agent.events],
|
|
224
|
+
);
|
|
225
|
+
|
|
214
226
|
const convertedMessages = useMemo(() => {
|
|
215
227
|
const createdAtByMessageId = createdAtByMessageIdRef.current;
|
|
216
228
|
const messageIds = new Set(
|
|
@@ -224,6 +236,13 @@ export const useEveAgentRuntime = (options: UseEveAgentRuntimeOptions = {}) => {
|
|
|
224
236
|
isRunning,
|
|
225
237
|
error: agent.error,
|
|
226
238
|
getCreatedAt: (message) => {
|
|
239
|
+
const turnId = message.metadata?.turnId;
|
|
240
|
+
const durable =
|
|
241
|
+
turnId === undefined
|
|
242
|
+
? undefined
|
|
243
|
+
: turnTimestamps.get(turnId)?.[message.role];
|
|
244
|
+
if (durable !== undefined) return durable;
|
|
245
|
+
|
|
227
246
|
const existing = createdAtByMessageId.get(message.id);
|
|
228
247
|
if (existing) return existing;
|
|
229
248
|
|
|
@@ -232,7 +251,7 @@ export const useEveAgentRuntime = (options: UseEveAgentRuntimeOptions = {}) => {
|
|
|
232
251
|
return createdAt;
|
|
233
252
|
},
|
|
234
253
|
});
|
|
235
|
-
}, [agent.data, agent.error, isRunning]);
|
|
254
|
+
}, [agent.data, agent.error, isRunning, turnTimestamps]);
|
|
236
255
|
|
|
237
256
|
const messages = stagedMessages ?? convertedMessages;
|
|
238
257
|
const messagesRef = useRef(messages);
|