@assistant-ui/eve 0.0.24 → 0.0.26
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 +40 -51
- package/dist/convertEveMessages.d.ts.map +1 -1
- package/dist/convertEveMessages.js +146 -117
- package/dist/convertEveMessages.js.map +1 -1
- package/dist/deriveCreatedAt.d.ts +9 -9
- package/dist/deriveCreatedAt.d.ts.map +1 -1
- package/dist/deriveCreatedAt.js +25 -34
- package/dist/deriveCreatedAt.js.map +1 -1
- package/dist/eveCloudSessions.js +1 -2
- package/dist/eveExtras.d.ts +4 -2
- package/dist/eveExtras.d.ts.map +1 -1
- package/dist/eveExtras.js +1 -2
- package/dist/eveExtras.js.map +1 -1
- package/dist/hooks.d.ts +4 -3
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +5 -5
- package/dist/hooks.js.map +1 -1
- package/dist/sdkIdentity.js +2 -3
- package/dist/useEveAgentRuntime.d.ts +3 -5
- package/dist/useEveAgentRuntime.d.ts.map +1 -1
- package/dist/useEveAgentRuntime.js +34 -23
- package/dist/useEveAgentRuntime.js.map +1 -1
- package/package.json +10 -10
- package/src/convertEveMessages.test.ts +925 -729
- package/src/convertEveMessages.ts +282 -227
- package/src/deriveCreatedAt.test.ts +90 -88
- package/src/deriveCreatedAt.ts +32 -36
- package/src/eveExtras.ts +4 -4
- package/src/hooks.ts +4 -3
- package/src/testUtils.ts +189 -8
- package/src/useEveAgentRuntime.cloud.test.tsx +105 -136
- package/src/useEveAgentRuntime.eveResume.test.tsx +22 -104
- package/src/useEveAgentRuntime.fast-refresh.test.tsx +195 -37
- package/src/useEveAgentRuntime.interruptedTurn.test.tsx +86 -40
- package/src/useEveAgentRuntime.refetch.test.tsx +19 -58
- package/src/useEveAgentRuntime.test.tsx +372 -291
- package/src/useEveAgentRuntime.ts +100 -113
|
@@ -25,16 +25,18 @@ import {
|
|
|
25
25
|
toMediaWireUrl,
|
|
26
26
|
} from "@assistant-ui/core/internal";
|
|
27
27
|
import type {
|
|
28
|
-
EveAuthorizationOutcome,
|
|
29
|
-
EveAuthorizationPart,
|
|
30
|
-
EveDynamicToolPart,
|
|
31
28
|
EveMessage,
|
|
32
|
-
EveMessageData,
|
|
33
|
-
EveMessageInputRequest,
|
|
34
29
|
EveMessagePart,
|
|
30
|
+
EveToolPart,
|
|
31
|
+
Interaction,
|
|
35
32
|
} from "eve/react";
|
|
36
|
-
import type {
|
|
37
|
-
import
|
|
33
|
+
import type { ConversationState, SessionStreamEvent } from "eve/client";
|
|
34
|
+
import {
|
|
35
|
+
APPROVE_OPTION_ID,
|
|
36
|
+
DENY_OPTION_ID,
|
|
37
|
+
type Answer,
|
|
38
|
+
type InteractionOutcome,
|
|
39
|
+
} from "eve/events";
|
|
38
40
|
|
|
39
41
|
const ASSISTANT_COMPLETE_STATUS = {
|
|
40
42
|
type: "complete",
|
|
@@ -57,8 +59,8 @@ const USER_FALLBACK_STATUS = {
|
|
|
57
59
|
|
|
58
60
|
export type ConvertEveMessagesOptions = {
|
|
59
61
|
/**
|
|
60
|
-
* Marks the last assistant message as running while Eve
|
|
61
|
-
*
|
|
62
|
+
* Marks the last assistant message as running while Eve has a turn in
|
|
63
|
+
* flight. When omitted, a message carrying Eve's `"streaming"` marker is
|
|
62
64
|
* treated as running; pass `false` to settle interrupted messages to a
|
|
63
65
|
* terminal status.
|
|
64
66
|
*/
|
|
@@ -69,11 +71,11 @@ export type ConvertEveMessagesOptions = {
|
|
|
69
71
|
readonly error?: unknown;
|
|
70
72
|
readonly getCreatedAt?: ((message: EveMessage) => Date) | undefined;
|
|
71
73
|
/**
|
|
72
|
-
* The session's stream events. A
|
|
73
|
-
*
|
|
74
|
-
*
|
|
74
|
+
* The session's stream events. A failed or cancelled turn marks its
|
|
75
|
+
* assistant message as incomplete, with a failure's code and message as its
|
|
76
|
+
* error.
|
|
75
77
|
*/
|
|
76
|
-
readonly events?: readonly
|
|
78
|
+
readonly events?: readonly SessionStreamEvent[] | undefined;
|
|
77
79
|
};
|
|
78
80
|
|
|
79
81
|
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
@@ -109,25 +111,33 @@ const toMessageStatus = (
|
|
|
109
111
|
if (interrupted) return interrupted;
|
|
110
112
|
|
|
111
113
|
const isLast = index === messages.length - 1;
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
114
|
+
const interactions = message.parts.flatMap((part) =>
|
|
115
|
+
part.type === "interaction"
|
|
116
|
+
? [part]
|
|
117
|
+
: part.type === "tool" && part.interaction
|
|
118
|
+
? [part.interaction]
|
|
119
|
+
: [],
|
|
120
|
+
);
|
|
121
|
+
const hasPendingApproval = interactions.some(
|
|
122
|
+
(interaction) =>
|
|
123
|
+
interaction.status === "open" && interaction.request.kind !== "sign-in",
|
|
115
124
|
);
|
|
116
125
|
// Scoped to the stale-marker case so a terminalized or errored turn keeps its
|
|
117
126
|
// own status; the hold only claims the fall-through that reads as cancelled.
|
|
118
127
|
const hasPendingAuthorization =
|
|
119
128
|
message.metadata?.status === "streaming" &&
|
|
120
129
|
(!isLast || options.error === undefined) &&
|
|
121
|
-
|
|
122
|
-
(
|
|
130
|
+
interactions.some(
|
|
131
|
+
(interaction) =>
|
|
132
|
+
interaction.status === "open" && interaction.request.kind === "sign-in",
|
|
123
133
|
);
|
|
124
134
|
|
|
125
135
|
if (hasPendingApproval) {
|
|
126
136
|
return { type: "requires-action", reason: "tool-calls" };
|
|
127
137
|
}
|
|
128
138
|
|
|
129
|
-
// A
|
|
130
|
-
//
|
|
139
|
+
// A sign-in is answered outside the thread, so the turn is held on external
|
|
140
|
+
// input rather than on a tool call awaiting a result.
|
|
131
141
|
if (hasPendingAuthorization) {
|
|
132
142
|
return { type: "requires-action", reason: "interrupt" };
|
|
133
143
|
}
|
|
@@ -139,8 +149,6 @@ const toMessageStatus = (
|
|
|
139
149
|
if (isLast && options.isRunning === true) {
|
|
140
150
|
return ASSISTANT_RUNNING_STATUS;
|
|
141
151
|
}
|
|
142
|
-
|
|
143
|
-
// Some eve versions leave the streaming marker after cancellation or failure.
|
|
144
152
|
if (message.metadata?.status === "streaming") {
|
|
145
153
|
if (options.isRunning === undefined) {
|
|
146
154
|
return ASSISTANT_RUNNING_STATUS;
|
|
@@ -159,7 +167,7 @@ const toMessageStatus = (
|
|
|
159
167
|
};
|
|
160
168
|
|
|
161
169
|
const toolApprovalOptionsFromInputRequest = (
|
|
162
|
-
inputRequest:
|
|
170
|
+
inputRequest: Interaction["request"] | undefined,
|
|
163
171
|
): readonly ToolApprovalOption[] | undefined => {
|
|
164
172
|
const options = inputRequest?.options;
|
|
165
173
|
if (!options || options.length === 0) return undefined;
|
|
@@ -167,9 +175,9 @@ const toolApprovalOptionsFromInputRequest = (
|
|
|
167
175
|
return options.map((option) => ({
|
|
168
176
|
id: option.id,
|
|
169
177
|
kind:
|
|
170
|
-
option.id ===
|
|
178
|
+
option.id === APPROVE_OPTION_ID
|
|
171
179
|
? "allow-once"
|
|
172
|
-
: option.id ===
|
|
180
|
+
: option.id === DENY_OPTION_ID
|
|
173
181
|
? "reject-once"
|
|
174
182
|
: `_${option.id}`,
|
|
175
183
|
...(option.label && { label: option.label }),
|
|
@@ -178,16 +186,16 @@ const toolApprovalOptionsFromInputRequest = (
|
|
|
178
186
|
};
|
|
179
187
|
|
|
180
188
|
/**
|
|
181
|
-
* Eve resolves
|
|
182
|
-
* shapes it accepts decide both what the renderer may offer and what
|
|
189
|
+
* Eve resolves an interaction the moment any answer for it arrives, so the
|
|
190
|
+
* answer shapes it accepts decide both what the renderer may offer and what
|
|
183
191
|
* {@link toEveInputResponse} may submit. A request with no options and no
|
|
184
192
|
* `display` takes a typed answer, which is why the absence of both is not the
|
|
185
193
|
* same as a confirmation.
|
|
186
194
|
*/
|
|
187
|
-
const isDroppedOptionApproval = (inputRequest:
|
|
188
|
-
inputRequest.kind === "
|
|
195
|
+
const isDroppedOptionApproval = (inputRequest: Interaction["request"]) =>
|
|
196
|
+
inputRequest.kind === "approval" && !inputRequest.options?.length;
|
|
189
197
|
|
|
190
|
-
const acceptsFreeformAnswer = (inputRequest:
|
|
198
|
+
const acceptsFreeformAnswer = (inputRequest: Interaction["request"]): boolean =>
|
|
191
199
|
inputRequest.display !== "confirmation" &&
|
|
192
200
|
!isDroppedOptionApproval(inputRequest) &&
|
|
193
201
|
(inputRequest.display === "text" ||
|
|
@@ -195,7 +203,7 @@ const acceptsFreeformAnswer = (inputRequest: EveMessageInputRequest): boolean =>
|
|
|
195
203
|
!inputRequest.options?.length);
|
|
196
204
|
|
|
197
205
|
const toolApprovalDisplay = (
|
|
198
|
-
inputRequest:
|
|
206
|
+
inputRequest: Interaction["request"],
|
|
199
207
|
): ToolApprovalDisplay => {
|
|
200
208
|
// The mapper answers this shape through its approve/cancel branch whatever
|
|
201
209
|
// the request declares, so the projection has to agree: presenting it as a
|
|
@@ -210,126 +218,126 @@ const toolApprovalDisplay = (
|
|
|
210
218
|
case "text":
|
|
211
219
|
return "text";
|
|
212
220
|
default:
|
|
213
|
-
if (inputRequest.kind === "
|
|
221
|
+
if (inputRequest.kind === "approval") return "decision";
|
|
214
222
|
return inputRequest.options?.length ? "select" : "text";
|
|
215
223
|
}
|
|
216
224
|
};
|
|
217
225
|
|
|
218
226
|
const toApproval = (
|
|
219
|
-
|
|
220
|
-
): ToolCallMessagePart["approval"]
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
const approval = part.approval;
|
|
232
|
-
if (!approval) return undefined;
|
|
233
|
-
const inputRequest = part.toolMetadata?.eve?.inputRequest;
|
|
234
|
-
const options = toolApprovalOptionsFromInputRequest(inputRequest);
|
|
227
|
+
interaction: Interaction,
|
|
228
|
+
): NonNullable<ToolCallMessagePart["approval"]> => {
|
|
229
|
+
const request = interaction.request;
|
|
230
|
+
const options = toolApprovalOptionsFromInputRequest(request);
|
|
231
|
+
const settled = interaction.status === "settled";
|
|
232
|
+
const approved =
|
|
233
|
+
interaction.outcome === "accepted"
|
|
234
|
+
? true
|
|
235
|
+
: interaction.outcome === "declined"
|
|
236
|
+
? false
|
|
237
|
+
: undefined;
|
|
235
238
|
|
|
236
239
|
return {
|
|
237
|
-
id:
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
240
|
+
id: interaction.interactionId,
|
|
241
|
+
prompt: request.prompt,
|
|
242
|
+
display: toolApprovalDisplay(request),
|
|
243
|
+
...(acceptsFreeformAnswer(request) && { allowFreeform: true }),
|
|
244
|
+
...(settled && approved !== undefined && { approved }),
|
|
245
|
+
...(settled &&
|
|
246
|
+
approved === undefined && {
|
|
247
|
+
resolution:
|
|
248
|
+
interaction.outcome === "expired"
|
|
249
|
+
? ("expired" as const)
|
|
250
|
+
: ("cancelled" as const),
|
|
251
|
+
}),
|
|
252
|
+
...(interaction.reason && { reason: interaction.reason }),
|
|
253
|
+
...(settled &&
|
|
254
|
+
interaction.answer?.optionId !== undefined && {
|
|
255
|
+
optionId: interaction.answer.optionId,
|
|
256
|
+
}),
|
|
257
|
+
...(settled &&
|
|
258
|
+
interaction.answer?.text !== undefined && {
|
|
259
|
+
text: interaction.answer.text,
|
|
260
|
+
}),
|
|
248
261
|
...(options && { options }),
|
|
249
262
|
};
|
|
250
263
|
};
|
|
251
264
|
|
|
252
|
-
const toJsonSafeInputRequest = (
|
|
253
|
-
inputRequest: EveMessageInputRequest,
|
|
254
|
-
): PartProviderMetadata[string] => ({
|
|
255
|
-
requestId: inputRequest.requestId,
|
|
256
|
-
prompt: inputRequest.prompt,
|
|
257
|
-
kind: inputRequest.kind,
|
|
258
|
-
...(inputRequest.display !== undefined && { display: inputRequest.display }),
|
|
259
|
-
...(inputRequest.allowFreeform !== undefined && {
|
|
260
|
-
allowFreeform: inputRequest.allowFreeform,
|
|
261
|
-
}),
|
|
262
|
-
...(inputRequest.options && {
|
|
263
|
-
options: inputRequest.options.map((option) => ({
|
|
264
|
-
id: option.id,
|
|
265
|
-
label: option.label,
|
|
266
|
-
...(option.description !== undefined && {
|
|
267
|
-
description: option.description,
|
|
268
|
-
}),
|
|
269
|
-
...(option.style !== undefined && { style: option.style }),
|
|
270
|
-
})),
|
|
271
|
-
}),
|
|
272
|
-
});
|
|
273
|
-
|
|
274
265
|
const toToolProviderMetadata = (
|
|
275
|
-
|
|
276
|
-
): PartProviderMetadata
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
266
|
+
interaction: Interaction,
|
|
267
|
+
): PartProviderMetadata => {
|
|
268
|
+
const request = interaction.request;
|
|
269
|
+
return {
|
|
270
|
+
eve: {
|
|
271
|
+
interaction: {
|
|
272
|
+
interactionId: interaction.interactionId,
|
|
273
|
+
prompt: request.prompt,
|
|
274
|
+
kind: request.kind,
|
|
275
|
+
...(request.display !== undefined && { display: request.display }),
|
|
276
|
+
...(request.allowFreeform !== undefined && {
|
|
277
|
+
allowFreeform: request.allowFreeform,
|
|
278
|
+
}),
|
|
279
|
+
...(request.options && {
|
|
280
|
+
options: request.options.map((option) => ({ ...option })),
|
|
281
|
+
}),
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
};
|
|
280
285
|
};
|
|
281
286
|
|
|
282
|
-
const
|
|
283
|
-
part:
|
|
284
|
-
): ToolCallMessagePart
|
|
285
|
-
|
|
286
|
-
const
|
|
287
|
-
const toolCall: ToolCallMessagePart = {
|
|
287
|
+
const convertToolPart = (
|
|
288
|
+
part: EveToolPart,
|
|
289
|
+
): Omit<ToolCallMessagePart, "args"> &
|
|
290
|
+
Partial<Pick<ToolCallMessagePart, "args">> => {
|
|
291
|
+
const toolCall: ReturnType<typeof convertToolPart> = {
|
|
288
292
|
type: "tool-call",
|
|
289
|
-
toolCallId: part.
|
|
290
|
-
toolName: part.
|
|
291
|
-
args: toJsonObject(part.input),
|
|
292
|
-
argsText:
|
|
293
|
-
|
|
294
|
-
...(
|
|
293
|
+
toolCallId: part.callId,
|
|
294
|
+
toolName: part.name,
|
|
295
|
+
...(part.state !== "streaming" && { args: toJsonObject(part.input) }),
|
|
296
|
+
argsText:
|
|
297
|
+
part.state === "streaming" ? part.inputText : stringifyArgs(part.input),
|
|
298
|
+
...(part.interaction && {
|
|
299
|
+
approval: toApproval(part.interaction),
|
|
300
|
+
providerMetadata: toToolProviderMetadata(part.interaction),
|
|
301
|
+
}),
|
|
295
302
|
};
|
|
296
303
|
|
|
297
304
|
switch (part.state) {
|
|
298
|
-
case "
|
|
305
|
+
case "completed":
|
|
299
306
|
return { ...toolCall, result: part.output };
|
|
300
|
-
|
|
301
|
-
case "
|
|
302
|
-
|
|
303
|
-
...toolCall,
|
|
304
|
-
result: { error: part.errorText },
|
|
305
|
-
isError: true,
|
|
306
|
-
};
|
|
307
|
-
|
|
308
|
-
case "output-denied":
|
|
307
|
+
case "failed":
|
|
308
|
+
case "rejected":
|
|
309
|
+
case "cancelled":
|
|
309
310
|
return {
|
|
310
311
|
...toolCall,
|
|
311
|
-
result: {
|
|
312
|
+
result: {
|
|
313
|
+
error:
|
|
314
|
+
part.error?.message ??
|
|
315
|
+
part.interaction?.reason ??
|
|
316
|
+
(part.state === "rejected"
|
|
317
|
+
? "Tool approval denied"
|
|
318
|
+
: part.state === "cancelled"
|
|
319
|
+
? "Tool call cancelled"
|
|
320
|
+
: "Tool call failed"),
|
|
321
|
+
},
|
|
312
322
|
isError: true,
|
|
313
323
|
};
|
|
314
|
-
|
|
315
324
|
default:
|
|
316
|
-
return
|
|
325
|
+
return part.output === undefined
|
|
326
|
+
? toolCall
|
|
327
|
+
: { ...toolCall, result: part.output, isPreliminary: true };
|
|
317
328
|
}
|
|
318
329
|
};
|
|
319
330
|
|
|
320
331
|
/**
|
|
321
|
-
* Payload of the `authorization` data part the Eve runtime emits for a
|
|
322
|
-
*
|
|
323
|
-
*
|
|
324
|
-
*
|
|
325
|
-
*
|
|
326
|
-
*
|
|
327
|
-
* `turnId` and `stepIndex` part identity is omitted: a renderer receives the
|
|
328
|
-
* one part it renders, in Eve's own step order, so display never has to
|
|
329
|
-
* re-identify a challenge among several.
|
|
332
|
+
* Payload of the `authorization` data part the Eve runtime emits for a sign-in
|
|
333
|
+
* interaction. `state` discriminates a pending sign-in from a settled one;
|
|
334
|
+
* every other field is present only when Eve projected it. This converter
|
|
335
|
+
* drops a `url` whose scheme is not `http(s)`, so a renderer can link to it
|
|
336
|
+
* without checking the scheme itself; Eve itself passes through whatever the
|
|
337
|
+
* connector supplied.
|
|
330
338
|
*/
|
|
331
339
|
export type EveAuthorizationData = {
|
|
332
|
-
readonly state:
|
|
340
|
+
readonly state: "required" | "completed";
|
|
333
341
|
readonly name: string;
|
|
334
342
|
readonly displayName?: string;
|
|
335
343
|
readonly description?: string;
|
|
@@ -337,37 +345,85 @@ export type EveAuthorizationData = {
|
|
|
337
345
|
readonly userCode?: string;
|
|
338
346
|
readonly instructions?: string;
|
|
339
347
|
readonly expiresAt?: string;
|
|
340
|
-
readonly outcome?:
|
|
348
|
+
readonly outcome?:
|
|
349
|
+
| "authorized"
|
|
350
|
+
| "declined"
|
|
351
|
+
| "failed"
|
|
352
|
+
| "timed-out"
|
|
353
|
+
| "cancelled";
|
|
341
354
|
readonly reason?: string;
|
|
342
355
|
};
|
|
343
356
|
|
|
357
|
+
const toAuthorizationOutcome = (
|
|
358
|
+
outcome: InteractionOutcome,
|
|
359
|
+
): NonNullable<EveAuthorizationData["outcome"]> => {
|
|
360
|
+
switch (outcome) {
|
|
361
|
+
case "accepted":
|
|
362
|
+
return "authorized";
|
|
363
|
+
case "declined":
|
|
364
|
+
case "invalid":
|
|
365
|
+
return "declined";
|
|
366
|
+
case "expired":
|
|
367
|
+
return "timed-out";
|
|
368
|
+
case "failed":
|
|
369
|
+
return "failed";
|
|
370
|
+
default:
|
|
371
|
+
return "cancelled";
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
|
|
344
375
|
const convertAuthorizationPart = (
|
|
345
|
-
|
|
346
|
-
): DataMessagePart<EveAuthorizationData> =>
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
url: part.authorization.url,
|
|
376
|
+
interaction: Interaction,
|
|
377
|
+
): DataMessagePart<EveAuthorizationData> => {
|
|
378
|
+
const signIn = interaction.request.signIn;
|
|
379
|
+
return {
|
|
380
|
+
type: "data",
|
|
381
|
+
name: "authorization",
|
|
382
|
+
data: {
|
|
383
|
+
state: interaction.status === "open" ? "required" : "completed",
|
|
384
|
+
name: signIn?.name ?? interaction.request.title ?? "sign-in",
|
|
385
|
+
...(signIn?.displayName !== undefined && {
|
|
386
|
+
displayName: signIn.displayName,
|
|
357
387
|
}),
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
instructions
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
expiresAt:
|
|
388
|
+
description: interaction.request.prompt,
|
|
389
|
+
...(signIn?.url !== undefined &&
|
|
390
|
+
httpUrlPattern.test(signIn.url) && { url: signIn.url }),
|
|
391
|
+
...(signIn?.userCode !== undefined && { userCode: signIn.userCode }),
|
|
392
|
+
...(signIn?.instructions !== undefined && {
|
|
393
|
+
instructions: signIn.instructions,
|
|
394
|
+
}),
|
|
395
|
+
...(signIn?.expiresAt !== undefined && { expiresAt: signIn.expiresAt }),
|
|
396
|
+
...(interaction.outcome !== undefined && {
|
|
397
|
+
outcome: toAuthorizationOutcome(interaction.outcome),
|
|
398
|
+
}),
|
|
399
|
+
...(interaction.reason !== undefined && { reason: interaction.reason }),
|
|
400
|
+
},
|
|
401
|
+
};
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
const convertInteractionPart = (
|
|
405
|
+
interaction: Interaction,
|
|
406
|
+
): ThreadAssistantMessagePart => {
|
|
407
|
+
if (interaction.request.kind === "sign-in")
|
|
408
|
+
return convertAuthorizationPart(interaction);
|
|
409
|
+
const approval = toApproval(interaction);
|
|
410
|
+
return {
|
|
411
|
+
type: "tool-call",
|
|
412
|
+
toolCallId: interaction.interactionId,
|
|
413
|
+
toolName: interaction.call?.name ?? interaction.request.kind,
|
|
414
|
+
args: toJsonObject(interaction.call?.input),
|
|
415
|
+
argsText: stringifyArgs(interaction.call?.input),
|
|
416
|
+
approval,
|
|
417
|
+
providerMetadata: toToolProviderMetadata(interaction),
|
|
418
|
+
...(interaction.status === "settled" && {
|
|
419
|
+
result: interaction.answer ?? {
|
|
420
|
+
outcome: interaction.outcome,
|
|
421
|
+
reason: interaction.reason,
|
|
422
|
+
},
|
|
423
|
+
...(interaction.outcome !== "accepted" && { isError: true }),
|
|
366
424
|
}),
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
},
|
|
370
|
-
});
|
|
425
|
+
};
|
|
426
|
+
};
|
|
371
427
|
|
|
372
428
|
const convertFilePart = (
|
|
373
429
|
part: Extract<EveMessagePart, { type: "file" }>,
|
|
@@ -381,14 +437,6 @@ const convertFilePart = (
|
|
|
381
437
|
...(httpUrlPattern.test(part.url) && { sourceType: "url" as const }),
|
|
382
438
|
};
|
|
383
439
|
};
|
|
384
|
-
|
|
385
|
-
// Eve's `state` is a run marker: the reducer settles a part to "done" only on
|
|
386
|
-
// `reasoning.completed`, `message.completed`, or `turn.cancelled`.
|
|
387
|
-
// `emitStreamContent` does not flush leftover reasoning on a tool-call, so a
|
|
388
|
-
// non-last reasoning part stays `streaming` until the model stream ends and the
|
|
389
|
-
// leftover flush runs. Mapping `streaming` to running would pin that block
|
|
390
|
-
// open. Only `done` is trustworthy; an unsettled part falls back to core's
|
|
391
|
-
// last-part rule.
|
|
392
440
|
const partStateToStatus = (
|
|
393
441
|
state: "done" | "streaming" | undefined,
|
|
394
442
|
): MessagePartStreamStatus | undefined =>
|
|
@@ -396,7 +444,7 @@ const partStateToStatus = (
|
|
|
396
444
|
|
|
397
445
|
const convertAssistantPart = (
|
|
398
446
|
part: EveMessagePart,
|
|
399
|
-
): ThreadAssistantMessagePart | null => {
|
|
447
|
+
): ThreadAssistantMessagePart | ReturnType<typeof convertToolPart> | null => {
|
|
400
448
|
switch (part.type) {
|
|
401
449
|
case "text":
|
|
402
450
|
case "reasoning": {
|
|
@@ -408,14 +456,11 @@ const convertAssistantPart = (
|
|
|
408
456
|
};
|
|
409
457
|
}
|
|
410
458
|
|
|
411
|
-
case "
|
|
412
|
-
return
|
|
413
|
-
|
|
414
|
-
case "dynamic-tool":
|
|
415
|
-
return convertDynamicToolPart(part);
|
|
459
|
+
case "tool":
|
|
460
|
+
return convertToolPart(part);
|
|
416
461
|
|
|
417
|
-
case "
|
|
418
|
-
return
|
|
462
|
+
case "interaction":
|
|
463
|
+
return convertInteractionPart(part);
|
|
419
464
|
|
|
420
465
|
case "file":
|
|
421
466
|
return convertFilePart(part);
|
|
@@ -538,22 +583,18 @@ const convertEveMessageWithInterruptions = (
|
|
|
538
583
|
};
|
|
539
584
|
|
|
540
585
|
export type InterruptedTurnEventCache = {
|
|
541
|
-
lastEvents: readonly
|
|
542
|
-
interruptions: readonly
|
|
586
|
+
lastEvents: readonly SessionStreamEvent[];
|
|
587
|
+
interruptions: readonly SessionStreamEvent[];
|
|
543
588
|
};
|
|
544
589
|
|
|
545
|
-
const isTurnInterruption = (event:
|
|
546
|
-
event.type === "turn.
|
|
590
|
+
const isTurnInterruption = (event: SessionStreamEvent) =>
|
|
591
|
+
(event.type === "turn.settled" && event.data.outcome !== "completed") ||
|
|
592
|
+
(event.type === "session.ended" && event.data.outcome === "failed");
|
|
547
593
|
|
|
548
|
-
/**
|
|
549
|
-
* Collects the `turn.failed` and `turn.cancelled` events of an append-only
|
|
550
|
-
* event log, scanning only the events appended since the cached call. The
|
|
551
|
-
* returned array keeps its identity until a new interruption arrives.
|
|
552
|
-
*/
|
|
553
594
|
export const collectInterruptedTurnEvents = (
|
|
554
|
-
events: readonly
|
|
595
|
+
events: readonly SessionStreamEvent[],
|
|
555
596
|
cache: InterruptedTurnEventCache,
|
|
556
|
-
): readonly
|
|
597
|
+
): readonly SessionStreamEvent[] => {
|
|
557
598
|
if (events === cache.lastEvents) return cache.interruptions;
|
|
558
599
|
|
|
559
600
|
const scanned = cache.lastEvents;
|
|
@@ -561,40 +602,61 @@ export const collectInterruptedTurnEvents = (
|
|
|
561
602
|
scanned.length === 0 ||
|
|
562
603
|
events[scanned.length - 1] === scanned[scanned.length - 1];
|
|
563
604
|
|
|
564
|
-
|
|
605
|
+
const interruptions = resumesScan ? cache.interruptions : [];
|
|
606
|
+
let nextInterruptions: SessionStreamEvent[] | undefined;
|
|
565
607
|
for (let i = resumesScan ? scanned.length : 0; i < events.length; i++) {
|
|
566
608
|
const event = events[i]!;
|
|
567
|
-
if (isTurnInterruption(event))
|
|
609
|
+
if (isTurnInterruption(event)) {
|
|
610
|
+
nextInterruptions ??= [...interruptions];
|
|
611
|
+
nextInterruptions.push(event);
|
|
612
|
+
}
|
|
568
613
|
}
|
|
569
614
|
|
|
570
615
|
cache.lastEvents = events;
|
|
571
|
-
cache.interruptions = interruptions;
|
|
572
|
-
return interruptions;
|
|
616
|
+
cache.interruptions = nextInterruptions ?? interruptions;
|
|
617
|
+
return cache.interruptions;
|
|
573
618
|
};
|
|
574
619
|
|
|
575
620
|
const getInterruptedTurns = (
|
|
576
|
-
events: readonly
|
|
621
|
+
events: readonly SessionStreamEvent[] | undefined,
|
|
577
622
|
) => {
|
|
578
623
|
const interrupted = new Map<string, MessageStatus>();
|
|
624
|
+
const sessionFailures = new Map<string, MessageStatus>();
|
|
579
625
|
for (const event of events ?? []) {
|
|
580
|
-
if (event.type === "turn.failed") {
|
|
626
|
+
if (event.type === "turn.settled" && event.data.outcome === "failed") {
|
|
581
627
|
interrupted.set(event.data.turnId, {
|
|
582
628
|
type: "incomplete",
|
|
583
629
|
reason: "error",
|
|
584
|
-
|
|
630
|
+
...(event.data.error && { error: toAssistantError(event.data.error) }),
|
|
585
631
|
});
|
|
586
|
-
} else if (
|
|
632
|
+
} else if (
|
|
633
|
+
event.type === "turn.settled" &&
|
|
634
|
+
event.data.outcome === "cancelled"
|
|
635
|
+
) {
|
|
587
636
|
interrupted.set(event.data.turnId, ASSISTANT_CANCELLED_STATUS);
|
|
637
|
+
} else if (
|
|
638
|
+
event.type === "session.ended" &&
|
|
639
|
+
event.data.outcome === "failed" &&
|
|
640
|
+
event.data.cause &&
|
|
641
|
+
"turnId" in event.data.cause
|
|
642
|
+
) {
|
|
643
|
+
sessionFailures.set(event.data.cause.turnId, {
|
|
644
|
+
type: "incomplete",
|
|
645
|
+
reason: "error",
|
|
646
|
+
...(event.data.error && { error: toAssistantError(event.data.error) }),
|
|
647
|
+
});
|
|
588
648
|
}
|
|
589
649
|
}
|
|
650
|
+
for (const [turnId, status] of sessionFailures)
|
|
651
|
+
interrupted.set(turnId, status);
|
|
590
652
|
return interrupted;
|
|
591
653
|
};
|
|
592
654
|
|
|
593
655
|
/**
|
|
594
|
-
* Converts
|
|
656
|
+
* Converts Eve's conversation messages into assistant-ui thread messages.
|
|
595
657
|
*/
|
|
596
658
|
export const convertEveMessages = (
|
|
597
|
-
data:
|
|
659
|
+
data: Pick<ConversationState, "messages">,
|
|
598
660
|
options: ConvertEveMessagesOptions = {},
|
|
599
661
|
): ThreadMessage[] => {
|
|
600
662
|
const interruptedTurns = getInterruptedTurns(options.events);
|
|
@@ -610,9 +672,9 @@ export const convertEveMessages = (
|
|
|
610
672
|
};
|
|
611
673
|
|
|
612
674
|
/**
|
|
613
|
-
* Structural subset of the
|
|
614
|
-
*
|
|
615
|
-
*
|
|
675
|
+
* Structural subset of the message content Eve's `send` API accepts. The
|
|
676
|
+
* helpers declare the shapes they produce and leave assignability to the send
|
|
677
|
+
* call site, checked against the installed version.
|
|
616
678
|
*/
|
|
617
679
|
export type EveMessageContent =
|
|
618
680
|
| string
|
|
@@ -680,8 +742,6 @@ export const getEveMessageContent = (
|
|
|
680
742
|
}
|
|
681
743
|
|
|
682
744
|
case "audio": {
|
|
683
|
-
// A data URL's own media type wins over `mediaType` downstream, so the
|
|
684
|
-
// envelope is rebuilt from the typed format rather than forwarded.
|
|
685
745
|
const mediaType = `audio/${part.audio.format}`;
|
|
686
746
|
const data = part.audio.data;
|
|
687
747
|
const source = resolveFilePartSource({ data, mimeType: mediaType });
|
|
@@ -719,53 +779,54 @@ export const getEveMessageContent = (
|
|
|
719
779
|
};
|
|
720
780
|
|
|
721
781
|
/**
|
|
722
|
-
* Finds the
|
|
723
|
-
*
|
|
782
|
+
* Finds the request of the interaction answered by the given approval id, so
|
|
783
|
+
* the answer mapping can honor the request's display mode.
|
|
724
784
|
*/
|
|
725
785
|
export const findEveInputRequest = (
|
|
726
|
-
data:
|
|
786
|
+
data: Pick<ConversationState, "messages">,
|
|
727
787
|
approvalId: string,
|
|
728
|
-
):
|
|
788
|
+
): Interaction["request"] | undefined => {
|
|
729
789
|
for (const message of data.messages) {
|
|
730
790
|
for (const part of message.parts) {
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
791
|
+
const interaction =
|
|
792
|
+
part.type === "interaction"
|
|
793
|
+
? part
|
|
794
|
+
: part.type === "tool"
|
|
795
|
+
? part.interaction
|
|
796
|
+
: undefined;
|
|
797
|
+
if (interaction?.interactionId === approvalId) return interaction.request;
|
|
734
798
|
}
|
|
735
799
|
}
|
|
736
800
|
return undefined;
|
|
737
801
|
};
|
|
738
802
|
|
|
739
803
|
/**
|
|
740
|
-
* Converts an assistant-ui tool approval response into an Eve
|
|
804
|
+
* Converts an assistant-ui tool approval response into an Eve `Answer`.
|
|
741
805
|
*
|
|
742
|
-
* When `inputRequest` is known, every returned
|
|
743
|
-
*
|
|
744
|
-
*
|
|
806
|
+
* When `inputRequest` is known, every returned answer carries an option the
|
|
807
|
+
* request declares, the literal `"approve"` / `"deny"` for an approval that
|
|
808
|
+
* declares no options, or a free-form answer. A literal option match wins,
|
|
809
|
+
* then the `"approve"` / `"deny"` option the response's boolean decision
|
|
745
810
|
* names, then the response's `text` (or its `reason`, for a caller that
|
|
746
811
|
* predates the first-class answer field) when the request takes a free-form
|
|
747
812
|
* answer (`display: "text"`, `allowFreeform`, or no options at all, and never
|
|
748
813
|
* `display: "confirmation"`) and the response is not a refusal.
|
|
749
|
-
* A one-argument call, or a part with no request, still forwards a caller
|
|
750
|
-
* `optionId` as it always has, including ids the request never declared.
|
|
751
814
|
*
|
|
752
815
|
* A response the known request cannot record as the answer it asked for throws
|
|
753
|
-
* instead of being submitted, because
|
|
754
|
-
*
|
|
755
|
-
* content. Not sending leaves the
|
|
816
|
+
* instead of being submitted, because Eve resolves an interaction the moment
|
|
817
|
+
* any answer for it arrives, and an empty one is recorded as an answer with no
|
|
818
|
+
* content. Not sending leaves the interaction open, so the caller can retry or
|
|
756
819
|
* the user can answer it as an ordinary message.
|
|
757
820
|
*
|
|
758
|
-
* With no request to map against
|
|
759
|
-
* `
|
|
760
|
-
*
|
|
761
|
-
* a display mode is what this mapper stopped doing; an unknown request is not
|
|
762
|
-
* a guess about one.
|
|
821
|
+
* With no request to map against, a one-argument call forwards a caller
|
|
822
|
+
* `optionId` as is and otherwise maps the decision to the literal `"approve"` /
|
|
823
|
+
* `"deny"` option every Eve approval declares.
|
|
763
824
|
*/
|
|
764
825
|
export const toEveInputResponse = (
|
|
765
826
|
response: RespondToToolApprovalOptions,
|
|
766
|
-
inputRequest?:
|
|
767
|
-
):
|
|
768
|
-
const
|
|
827
|
+
inputRequest?: Interaction["request"],
|
|
828
|
+
): Answer => {
|
|
829
|
+
const interactionId = response.approvalId;
|
|
769
830
|
const options = inputRequest?.options;
|
|
770
831
|
const text = response.text ?? response.reason;
|
|
771
832
|
|
|
@@ -775,13 +836,13 @@ export const toEveInputResponse = (
|
|
|
775
836
|
options?.some((option) => option.id === response.optionId)
|
|
776
837
|
) {
|
|
777
838
|
return {
|
|
778
|
-
|
|
839
|
+
interactionId,
|
|
779
840
|
optionId: response.optionId,
|
|
780
841
|
...(text && { text }),
|
|
781
842
|
};
|
|
782
843
|
}
|
|
783
844
|
throw new Error(
|
|
784
|
-
`Eve
|
|
845
|
+
`Eve interaction "${interactionId}" has no option with id "${response.optionId}"; respond with one of: ${
|
|
785
846
|
options?.length
|
|
786
847
|
? options.map((option) => option.id).join(", ")
|
|
787
848
|
: "(the request carries no options)"
|
|
@@ -789,29 +850,23 @@ export const toEveInputResponse = (
|
|
|
789
850
|
);
|
|
790
851
|
}
|
|
791
852
|
|
|
792
|
-
const decisionOptionId = response.approved
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
// non-spec approval payload answerable rather than stuck. A tool-approval
|
|
796
|
-
// that declares its own options still requires one of them.
|
|
853
|
+
const decisionOptionId = response.approved
|
|
854
|
+
? APPROVE_OPTION_ID
|
|
855
|
+
: DENY_OPTION_ID;
|
|
797
856
|
if (
|
|
798
857
|
!inputRequest ||
|
|
799
|
-
(inputRequest.kind === "
|
|
858
|
+
(inputRequest.kind === "approval" && !options?.length) ||
|
|
800
859
|
options?.some((option) => option.id === decisionOptionId)
|
|
801
860
|
) {
|
|
802
|
-
return {
|
|
861
|
+
return { interactionId, optionId: decisionOptionId, ...(text && { text }) };
|
|
803
862
|
}
|
|
804
|
-
|
|
805
|
-
// Eve recognises an approval by its literal two-option `approve`/`cancel`
|
|
806
|
-
// shape, so past this point the request is a question and the boolean
|
|
807
|
-
// carries no decision eve would act on.
|
|
808
863
|
const acceptsText = acceptsFreeformAnswer(inputRequest);
|
|
809
864
|
if (acceptsText && text && response.approved !== false) {
|
|
810
|
-
return {
|
|
865
|
+
return { interactionId, text };
|
|
811
866
|
}
|
|
812
867
|
|
|
813
868
|
throw new Error(
|
|
814
|
-
`Eve
|
|
869
|
+
`Eve interaction "${interactionId}" (${inputRequest.prompt}) was not answered by this response; ${
|
|
815
870
|
options?.length
|
|
816
871
|
? `respond with one of: ${options.map((option) => option.id).join(", ")}`
|
|
817
872
|
: !acceptsText
|