@ai-sdk/tui 0.0.0 → 1.0.0-beta.12
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/CHANGELOG.md +154 -0
- package/LICENSE +201 -0
- package/README.md +79 -0
- package/dist/index.d.ts +69 -0
- package/dist/index.js +1924 -0
- package/dist/index.js.map +1 -0
- package/package.json +75 -1
- package/src/agent-tui-runner.ts +666 -0
- package/src/index.ts +7 -0
- package/src/run-agent-tui.ts +85 -0
- package/src/test/mock-terminal.ts +207 -0
- package/src/tui/layout.ts +260 -0
- package/src/tui/markdown.ts +240 -0
- package/src/tui/terminal-frame-buffer.ts +111 -0
- package/src/tui/terminal-renderer.ts +1411 -0
- package/src/tui/terminal-text.ts +158 -0
- package/src/util/deferred.ts +10 -0
- package/src/util/print-stream.ts +30 -0
|
@@ -0,0 +1,666 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AgentTUIAgent,
|
|
3
|
+
ResponseStatisticsMode,
|
|
4
|
+
RunAgentTUIOptions,
|
|
5
|
+
TerminalPartDisplayMode,
|
|
6
|
+
} from './run-agent-tui';
|
|
7
|
+
import {
|
|
8
|
+
TerminalRenderer,
|
|
9
|
+
type TerminalInput,
|
|
10
|
+
type TerminalOutput,
|
|
11
|
+
} from './tui/terminal-renderer';
|
|
12
|
+
import {
|
|
13
|
+
convertToModelMessages,
|
|
14
|
+
getToolName,
|
|
15
|
+
isToolUIPart,
|
|
16
|
+
type StepResultPerformance,
|
|
17
|
+
type LanguageModelUsage,
|
|
18
|
+
type TextStreamPart,
|
|
19
|
+
type ToolSet,
|
|
20
|
+
type UIMessage,
|
|
21
|
+
type UIMessageChunk,
|
|
22
|
+
} from 'ai';
|
|
23
|
+
|
|
24
|
+
const defaultResponseStatistics: ResponseStatisticsMode =
|
|
25
|
+
'outputTokensPerSecond';
|
|
26
|
+
|
|
27
|
+
export type AgentTUIStreamResult = {
|
|
28
|
+
uiMessageStream:
|
|
29
|
+
| AsyncIterable<UIMessageChunk>
|
|
30
|
+
| ReadableStream<UIMessageChunk>;
|
|
31
|
+
message?: UIMessage;
|
|
32
|
+
abort?: () => void;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type AgentTUIStreamOptions = {
|
|
36
|
+
messages: UIMessage[];
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type AgentTUISessionOptions = {
|
|
40
|
+
title?: string;
|
|
41
|
+
initialPrompt?: string;
|
|
42
|
+
submittedPrompt?: string;
|
|
43
|
+
waitForExit?: boolean;
|
|
44
|
+
continueSession?: boolean;
|
|
45
|
+
tools?: TerminalPartDisplayMode;
|
|
46
|
+
reasoning?: TerminalPartDisplayMode;
|
|
47
|
+
responseStatistics?: ResponseStatisticsMode;
|
|
48
|
+
contextSize?: number;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export type AgentTUIToolApprovalRequest = {
|
|
52
|
+
approvalId: string;
|
|
53
|
+
toolCallId: string;
|
|
54
|
+
toolName: string;
|
|
55
|
+
title?: string;
|
|
56
|
+
input: unknown;
|
|
57
|
+
providerExecuted?: boolean;
|
|
58
|
+
messageId: string;
|
|
59
|
+
partIndex: number;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export type AgentTUIToolApprovalResponse = {
|
|
63
|
+
approved: boolean;
|
|
64
|
+
reason?: string;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export type AgentTUIRenderer = {
|
|
68
|
+
readPrompt?(options?: AgentTUISessionOptions): Promise<string | undefined>;
|
|
69
|
+
readToolApproval?(
|
|
70
|
+
request: AgentTUIToolApprovalRequest,
|
|
71
|
+
options?: AgentTUISessionOptions,
|
|
72
|
+
): Promise<AgentTUIToolApprovalResponse>;
|
|
73
|
+
renderStream(
|
|
74
|
+
result: AgentTUIStreamResult,
|
|
75
|
+
options?: AgentTUISessionOptions,
|
|
76
|
+
): Promise<UIMessage | undefined>;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export type AgentTUIRunnerOptions<
|
|
80
|
+
TAgent extends AgentTUIAgent = AgentTUIAgent,
|
|
81
|
+
> = Omit<RunAgentTUIOptions, 'agent'> & {
|
|
82
|
+
agent: TAgent;
|
|
83
|
+
renderer?: AgentTUIRenderer;
|
|
84
|
+
screen?: TerminalOutput;
|
|
85
|
+
userInput?: TerminalInput;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export class AgentTUIRunner<TAgent extends AgentTUIAgent = AgentTUIAgent> {
|
|
89
|
+
private readonly agent: TAgent;
|
|
90
|
+
private readonly renderer: AgentTUIRenderer;
|
|
91
|
+
private readonly title?: string;
|
|
92
|
+
private readonly tools: TerminalPartDisplayMode;
|
|
93
|
+
private readonly reasoning: TerminalPartDisplayMode;
|
|
94
|
+
private readonly responseStatistics: ResponseStatisticsMode;
|
|
95
|
+
private readonly contextSize?: number;
|
|
96
|
+
|
|
97
|
+
constructor(options: AgentTUIRunnerOptions<TAgent>) {
|
|
98
|
+
this.agent = options.agent;
|
|
99
|
+
this.renderer = createRenderer(options) ?? createDefaultRenderer(options);
|
|
100
|
+
this.title = options.title;
|
|
101
|
+
this.tools = options.tools ?? 'auto-collapsed';
|
|
102
|
+
this.reasoning = options.reasoning ?? 'auto-collapsed';
|
|
103
|
+
this.responseStatistics =
|
|
104
|
+
options.responseStatistics ?? defaultResponseStatistics;
|
|
105
|
+
this.contextSize = options.contextSize;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async run() {
|
|
109
|
+
const title = this.title;
|
|
110
|
+
const messages: UIMessage[] = [];
|
|
111
|
+
let nextMessageIndex = 0;
|
|
112
|
+
const generateMessageId = () => `message-${++nextMessageIndex}`;
|
|
113
|
+
let prompt: string | undefined;
|
|
114
|
+
let hasRunTurn = false;
|
|
115
|
+
let streamWithoutPrompt = false;
|
|
116
|
+
|
|
117
|
+
while (true) {
|
|
118
|
+
if (!streamWithoutPrompt) {
|
|
119
|
+
if (prompt == null) {
|
|
120
|
+
if (!this.renderer.readPrompt) {
|
|
121
|
+
if (hasRunTurn) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
throw new Error(
|
|
126
|
+
'No prompt was provided and the renderer does not support prompt input.',
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
prompt = await this.renderer.readPrompt({ title });
|
|
132
|
+
} catch (error) {
|
|
133
|
+
if (isInterruptedError(error)) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (prompt == null) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
messages.push(createUserMessage(generateMessageId(), prompt));
|
|
146
|
+
hasRunTurn = true;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const result = await this.streamMessages(
|
|
150
|
+
[...messages],
|
|
151
|
+
generateMessageId,
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
const responseMessage = await this.renderer.renderStream(result, {
|
|
156
|
+
title,
|
|
157
|
+
submittedPrompt: prompt,
|
|
158
|
+
continueSession: Boolean(this.renderer.readPrompt),
|
|
159
|
+
tools: this.tools,
|
|
160
|
+
reasoning: this.reasoning,
|
|
161
|
+
responseStatistics: this.responseStatistics,
|
|
162
|
+
contextSize: this.contextSize,
|
|
163
|
+
waitForExit: false,
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
if (responseMessage && responseMessage.parts.length > 0) {
|
|
167
|
+
const approvalRequests =
|
|
168
|
+
findPendingToolApprovalRequests(responseMessage);
|
|
169
|
+
|
|
170
|
+
if (approvalRequests.length > 0) {
|
|
171
|
+
if (!this.renderer.readToolApproval) {
|
|
172
|
+
throw new Error(
|
|
173
|
+
'Tool approval was requested, but the renderer does not support tool approval input.',
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
for (const request of approvalRequests) {
|
|
178
|
+
const response = await this.renderer.readToolApproval(request, {
|
|
179
|
+
title,
|
|
180
|
+
});
|
|
181
|
+
applyToolApprovalResponse(responseMessage, request, response);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
upsertResponseMessage(
|
|
185
|
+
messages,
|
|
186
|
+
responseMessage,
|
|
187
|
+
streamWithoutPrompt,
|
|
188
|
+
);
|
|
189
|
+
streamWithoutPrompt = true;
|
|
190
|
+
prompt = undefined;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
upsertResponseMessage(messages, responseMessage, streamWithoutPrompt);
|
|
195
|
+
}
|
|
196
|
+
} catch (error) {
|
|
197
|
+
if (isInterruptedError(error)) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
throw error;
|
|
202
|
+
}
|
|
203
|
+
streamWithoutPrompt = false;
|
|
204
|
+
prompt = undefined;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private async streamMessages(
|
|
209
|
+
messages: UIMessage[],
|
|
210
|
+
generateMessageId: () => string,
|
|
211
|
+
): Promise<AgentTUIStreamResult> {
|
|
212
|
+
const abortController = new AbortController();
|
|
213
|
+
const result = await this.agent.stream({
|
|
214
|
+
prompt: await convertToModelMessages(messages, {
|
|
215
|
+
tools: this.agent.tools as ToolSet,
|
|
216
|
+
}),
|
|
217
|
+
abortSignal: abortController.signal,
|
|
218
|
+
options: undefined,
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
uiMessageStream: textStreamToUIMessageStream(
|
|
223
|
+
result.fullStream as AsyncIterable<TextStreamPart<ToolSet>>,
|
|
224
|
+
generateMessageId,
|
|
225
|
+
messages,
|
|
226
|
+
),
|
|
227
|
+
message: lastAssistantMessage(messages),
|
|
228
|
+
abort: () => abortController.abort(),
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function createDefaultRenderer(options: AgentTUIRunnerOptions) {
|
|
234
|
+
return options.tools === undefined &&
|
|
235
|
+
options.reasoning === undefined &&
|
|
236
|
+
options.responseStatistics === undefined &&
|
|
237
|
+
options.contextSize === undefined
|
|
238
|
+
? new TerminalRenderer()
|
|
239
|
+
: new TerminalRenderer({
|
|
240
|
+
tools: options.tools,
|
|
241
|
+
reasoning: options.reasoning,
|
|
242
|
+
responseStatistics: options.responseStatistics,
|
|
243
|
+
contextSize: options.contextSize,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function createRenderer(
|
|
248
|
+
options: AgentTUIRunnerOptions,
|
|
249
|
+
): AgentTUIRenderer | undefined {
|
|
250
|
+
if (options.renderer) {
|
|
251
|
+
return options.renderer;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (!options.screen && !options.userInput) {
|
|
255
|
+
return undefined;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return new TerminalRenderer({
|
|
259
|
+
tools: options.tools,
|
|
260
|
+
reasoning: options.reasoning,
|
|
261
|
+
responseStatistics: options.responseStatistics,
|
|
262
|
+
contextSize: options.contextSize,
|
|
263
|
+
input: options.userInput,
|
|
264
|
+
output: options.screen,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async function* textStreamToUIMessageStream(
|
|
269
|
+
stream: AsyncIterable<TextStreamPart<ToolSet>>,
|
|
270
|
+
generateMessageId: () => string,
|
|
271
|
+
originalMessages: UIMessage[] = [],
|
|
272
|
+
): AsyncIterable<UIMessageChunk> {
|
|
273
|
+
const openTextParts = new Set<string>();
|
|
274
|
+
const openReasoningParts = new Set<string>();
|
|
275
|
+
const openToolCalls = new Set<string>();
|
|
276
|
+
let latestStepUsage: LanguageModelUsage | undefined;
|
|
277
|
+
let latestPerformance: StepResultPerformance | undefined;
|
|
278
|
+
let sentFinish = false;
|
|
279
|
+
|
|
280
|
+
yield {
|
|
281
|
+
type: 'start',
|
|
282
|
+
messageId:
|
|
283
|
+
lastAssistantMessage(originalMessages)?.id ?? generateMessageId(),
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
for await (const part of stream) {
|
|
287
|
+
switch (part.type) {
|
|
288
|
+
case 'text-start':
|
|
289
|
+
openTextParts.add(part.id);
|
|
290
|
+
yield {
|
|
291
|
+
type: 'text-start',
|
|
292
|
+
id: part.id,
|
|
293
|
+
providerMetadata: part.providerMetadata,
|
|
294
|
+
};
|
|
295
|
+
break;
|
|
296
|
+
case 'text-delta':
|
|
297
|
+
if (!openTextParts.has(part.id)) {
|
|
298
|
+
openTextParts.add(part.id);
|
|
299
|
+
yield {
|
|
300
|
+
type: 'text-start',
|
|
301
|
+
id: part.id,
|
|
302
|
+
providerMetadata: part.providerMetadata,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
yield {
|
|
306
|
+
type: 'text-delta',
|
|
307
|
+
id: part.id,
|
|
308
|
+
delta: part.text,
|
|
309
|
+
providerMetadata: part.providerMetadata,
|
|
310
|
+
};
|
|
311
|
+
break;
|
|
312
|
+
case 'text-end':
|
|
313
|
+
openTextParts.delete(part.id);
|
|
314
|
+
yield {
|
|
315
|
+
type: 'text-end',
|
|
316
|
+
id: part.id,
|
|
317
|
+
providerMetadata: part.providerMetadata,
|
|
318
|
+
};
|
|
319
|
+
break;
|
|
320
|
+
case 'reasoning-start':
|
|
321
|
+
openReasoningParts.add(part.id);
|
|
322
|
+
yield {
|
|
323
|
+
type: 'reasoning-start',
|
|
324
|
+
id: part.id,
|
|
325
|
+
providerMetadata: part.providerMetadata,
|
|
326
|
+
};
|
|
327
|
+
break;
|
|
328
|
+
case 'reasoning-delta':
|
|
329
|
+
if (!openReasoningParts.has(part.id)) {
|
|
330
|
+
openReasoningParts.add(part.id);
|
|
331
|
+
yield {
|
|
332
|
+
type: 'reasoning-start',
|
|
333
|
+
id: part.id,
|
|
334
|
+
providerMetadata: part.providerMetadata,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
yield {
|
|
338
|
+
type: 'reasoning-delta',
|
|
339
|
+
id: part.id,
|
|
340
|
+
delta: part.text,
|
|
341
|
+
providerMetadata: part.providerMetadata,
|
|
342
|
+
};
|
|
343
|
+
break;
|
|
344
|
+
case 'reasoning-end':
|
|
345
|
+
openReasoningParts.delete(part.id);
|
|
346
|
+
yield {
|
|
347
|
+
type: 'reasoning-end',
|
|
348
|
+
id: part.id,
|
|
349
|
+
providerMetadata: part.providerMetadata,
|
|
350
|
+
};
|
|
351
|
+
break;
|
|
352
|
+
case 'tool-input-start':
|
|
353
|
+
yield {
|
|
354
|
+
type: 'tool-input-start',
|
|
355
|
+
toolCallId: part.id,
|
|
356
|
+
toolName: part.toolName,
|
|
357
|
+
providerExecuted: part.providerExecuted,
|
|
358
|
+
providerMetadata: part.providerMetadata,
|
|
359
|
+
toolMetadata: part.toolMetadata,
|
|
360
|
+
dynamic: part.dynamic,
|
|
361
|
+
title: part.title,
|
|
362
|
+
};
|
|
363
|
+
break;
|
|
364
|
+
case 'tool-input-delta':
|
|
365
|
+
yield {
|
|
366
|
+
type: 'tool-input-delta',
|
|
367
|
+
toolCallId: part.id,
|
|
368
|
+
inputTextDelta: part.delta,
|
|
369
|
+
};
|
|
370
|
+
break;
|
|
371
|
+
case 'tool-call':
|
|
372
|
+
openToolCalls.add(part.toolCallId);
|
|
373
|
+
yield {
|
|
374
|
+
type: 'tool-input-available',
|
|
375
|
+
toolCallId: part.toolCallId,
|
|
376
|
+
toolName: part.toolName,
|
|
377
|
+
input: part.input,
|
|
378
|
+
providerExecuted: part.providerExecuted,
|
|
379
|
+
providerMetadata: part.providerMetadata,
|
|
380
|
+
toolMetadata: part.toolMetadata,
|
|
381
|
+
dynamic: part.dynamic,
|
|
382
|
+
title: part.title,
|
|
383
|
+
};
|
|
384
|
+
break;
|
|
385
|
+
case 'tool-approval-request':
|
|
386
|
+
if (!openToolCalls.has(part.toolCall.toolCallId)) {
|
|
387
|
+
openToolCalls.add(part.toolCall.toolCallId);
|
|
388
|
+
yield {
|
|
389
|
+
type: 'tool-input-available',
|
|
390
|
+
toolCallId: part.toolCall.toolCallId,
|
|
391
|
+
toolName: part.toolCall.toolName,
|
|
392
|
+
input: part.toolCall.input,
|
|
393
|
+
providerExecuted: part.toolCall.providerExecuted,
|
|
394
|
+
providerMetadata: part.toolCall.providerMetadata,
|
|
395
|
+
toolMetadata: part.toolCall.toolMetadata,
|
|
396
|
+
dynamic: part.toolCall.dynamic,
|
|
397
|
+
title: part.toolCall.title,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
yield {
|
|
401
|
+
type: 'tool-approval-request',
|
|
402
|
+
approvalId: part.approvalId,
|
|
403
|
+
toolCallId: part.toolCall.toolCallId,
|
|
404
|
+
isAutomatic: part.isAutomatic,
|
|
405
|
+
};
|
|
406
|
+
break;
|
|
407
|
+
case 'tool-approval-response':
|
|
408
|
+
yield {
|
|
409
|
+
type: 'tool-approval-response',
|
|
410
|
+
approvalId: part.approvalId,
|
|
411
|
+
approved: part.approved,
|
|
412
|
+
reason: part.reason,
|
|
413
|
+
providerExecuted: part.providerExecuted,
|
|
414
|
+
};
|
|
415
|
+
break;
|
|
416
|
+
case 'tool-result':
|
|
417
|
+
yield {
|
|
418
|
+
type: 'tool-output-available',
|
|
419
|
+
toolCallId: part.toolCallId,
|
|
420
|
+
output: part.output,
|
|
421
|
+
providerExecuted: part.providerExecuted,
|
|
422
|
+
providerMetadata: part.providerMetadata,
|
|
423
|
+
toolMetadata: part.toolMetadata,
|
|
424
|
+
dynamic: part.dynamic,
|
|
425
|
+
preliminary: part.preliminary,
|
|
426
|
+
};
|
|
427
|
+
break;
|
|
428
|
+
case 'tool-error':
|
|
429
|
+
yield {
|
|
430
|
+
type: 'tool-output-error',
|
|
431
|
+
toolCallId: part.toolCallId,
|
|
432
|
+
errorText: formatStreamError(part.error),
|
|
433
|
+
providerExecuted: part.providerExecuted,
|
|
434
|
+
providerMetadata: part.providerMetadata,
|
|
435
|
+
toolMetadata: part.toolMetadata,
|
|
436
|
+
dynamic: part.dynamic,
|
|
437
|
+
};
|
|
438
|
+
break;
|
|
439
|
+
case 'tool-output-denied':
|
|
440
|
+
yield { type: 'tool-output-denied', toolCallId: part.toolCallId };
|
|
441
|
+
break;
|
|
442
|
+
case 'source':
|
|
443
|
+
if (part.sourceType === 'url') {
|
|
444
|
+
yield {
|
|
445
|
+
type: 'source-url',
|
|
446
|
+
sourceId: part.id,
|
|
447
|
+
url: part.url,
|
|
448
|
+
title: part.title,
|
|
449
|
+
providerMetadata: part.providerMetadata,
|
|
450
|
+
};
|
|
451
|
+
} else {
|
|
452
|
+
yield {
|
|
453
|
+
type: 'source-document',
|
|
454
|
+
sourceId: part.id,
|
|
455
|
+
mediaType: part.mediaType,
|
|
456
|
+
title: part.title,
|
|
457
|
+
filename: part.filename,
|
|
458
|
+
providerMetadata: part.providerMetadata,
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
break;
|
|
462
|
+
case 'file':
|
|
463
|
+
yield {
|
|
464
|
+
type: 'file',
|
|
465
|
+
url: fileToDataUrl(part.file.mediaType, part.file.base64),
|
|
466
|
+
mediaType: part.file.mediaType,
|
|
467
|
+
providerMetadata: part.providerMetadata,
|
|
468
|
+
};
|
|
469
|
+
break;
|
|
470
|
+
case 'reasoning-file':
|
|
471
|
+
yield {
|
|
472
|
+
type: 'reasoning-file',
|
|
473
|
+
url: fileToDataUrl(part.file.mediaType, part.file.base64),
|
|
474
|
+
mediaType: part.file.mediaType,
|
|
475
|
+
providerMetadata: part.providerMetadata,
|
|
476
|
+
};
|
|
477
|
+
break;
|
|
478
|
+
case 'start-step':
|
|
479
|
+
yield { type: 'start-step' };
|
|
480
|
+
break;
|
|
481
|
+
case 'finish-step':
|
|
482
|
+
latestStepUsage = part.usage;
|
|
483
|
+
latestPerformance = part.performance;
|
|
484
|
+
yield { type: 'finish-step' };
|
|
485
|
+
break;
|
|
486
|
+
case 'finish':
|
|
487
|
+
yield* closeOpenParts(openTextParts, openReasoningParts);
|
|
488
|
+
sentFinish = true;
|
|
489
|
+
yield {
|
|
490
|
+
type: 'finish',
|
|
491
|
+
finishReason: part.finishReason,
|
|
492
|
+
messageMetadata: createResponseMetadata(
|
|
493
|
+
latestStepUsage ?? part.totalUsage,
|
|
494
|
+
latestPerformance,
|
|
495
|
+
),
|
|
496
|
+
};
|
|
497
|
+
break;
|
|
498
|
+
case 'abort':
|
|
499
|
+
yield { type: 'abort', reason: part.reason };
|
|
500
|
+
break;
|
|
501
|
+
case 'error':
|
|
502
|
+
yield { type: 'error', errorText: formatStreamError(part.error) };
|
|
503
|
+
break;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (!sentFinish) {
|
|
508
|
+
yield* closeOpenParts(openTextParts, openReasoningParts);
|
|
509
|
+
yield { type: 'finish' };
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function createResponseMetadata(
|
|
514
|
+
usage: LanguageModelUsage | undefined,
|
|
515
|
+
performance?: StepResultPerformance,
|
|
516
|
+
): ResponseMetadata | undefined {
|
|
517
|
+
if (
|
|
518
|
+
usage?.totalTokens == null &&
|
|
519
|
+
usage?.outputTokens == null &&
|
|
520
|
+
performance?.outputTokensPerSecond == null
|
|
521
|
+
) {
|
|
522
|
+
return undefined;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return {
|
|
526
|
+
...(usage?.totalTokens == null && usage?.outputTokens == null
|
|
527
|
+
? {}
|
|
528
|
+
: {
|
|
529
|
+
usage: {
|
|
530
|
+
...(usage.totalTokens == null
|
|
531
|
+
? {}
|
|
532
|
+
: { totalTokens: usage.totalTokens }),
|
|
533
|
+
...(usage.outputTokens == null
|
|
534
|
+
? {}
|
|
535
|
+
: { outputTokens: usage.outputTokens }),
|
|
536
|
+
},
|
|
537
|
+
}),
|
|
538
|
+
...(performance?.outputTokensPerSecond == null
|
|
539
|
+
? {}
|
|
540
|
+
: {
|
|
541
|
+
performance: {
|
|
542
|
+
outputTokensPerSecond: performance.outputTokensPerSecond,
|
|
543
|
+
},
|
|
544
|
+
}),
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
type ResponseMetadata = {
|
|
549
|
+
usage?: {
|
|
550
|
+
totalTokens?: number;
|
|
551
|
+
outputTokens?: number;
|
|
552
|
+
};
|
|
553
|
+
performance?: Pick<StepResultPerformance, 'outputTokensPerSecond'>;
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
function* closeOpenParts(
|
|
557
|
+
textPartIds: Set<string>,
|
|
558
|
+
reasoningPartIds: Set<string>,
|
|
559
|
+
) {
|
|
560
|
+
for (const id of textPartIds) {
|
|
561
|
+
yield { type: 'text-end', id } satisfies UIMessageChunk;
|
|
562
|
+
}
|
|
563
|
+
textPartIds.clear();
|
|
564
|
+
|
|
565
|
+
for (const id of reasoningPartIds) {
|
|
566
|
+
yield { type: 'reasoning-end', id } satisfies UIMessageChunk;
|
|
567
|
+
}
|
|
568
|
+
reasoningPartIds.clear();
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function createUserMessage(id: string, text: string): UIMessage {
|
|
572
|
+
return {
|
|
573
|
+
id,
|
|
574
|
+
role: 'user',
|
|
575
|
+
parts: [{ type: 'text', text }],
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function upsertResponseMessage(
|
|
580
|
+
messages: UIMessage[],
|
|
581
|
+
responseMessage: UIMessage,
|
|
582
|
+
replaceLast: boolean,
|
|
583
|
+
) {
|
|
584
|
+
if (replaceLast && messages.at(-1)?.role === 'assistant') {
|
|
585
|
+
messages[messages.length - 1] = responseMessage;
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
messages.push(responseMessage);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function lastAssistantMessage(messages: UIMessage[]) {
|
|
593
|
+
const message = messages.at(-1);
|
|
594
|
+
|
|
595
|
+
return message?.role === 'assistant' ? message : undefined;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function findPendingToolApprovalRequests(
|
|
599
|
+
message: UIMessage,
|
|
600
|
+
): AgentTUIToolApprovalRequest[] {
|
|
601
|
+
const requests: AgentTUIToolApprovalRequest[] = [];
|
|
602
|
+
|
|
603
|
+
for (const [index, part] of message.parts.entries()) {
|
|
604
|
+
if (
|
|
605
|
+
!isToolUIPart(part) ||
|
|
606
|
+
part.state !== 'approval-requested' ||
|
|
607
|
+
part.approval.isAutomatic === true
|
|
608
|
+
) {
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
requests.push({
|
|
613
|
+
approvalId: part.approval.id,
|
|
614
|
+
toolCallId: part.toolCallId,
|
|
615
|
+
toolName: getToolName(part),
|
|
616
|
+
title: part.title,
|
|
617
|
+
input: part.input,
|
|
618
|
+
providerExecuted: part.providerExecuted,
|
|
619
|
+
messageId: message.id,
|
|
620
|
+
partIndex: index,
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
return requests;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function applyToolApprovalResponse(
|
|
628
|
+
message: UIMessage,
|
|
629
|
+
request: AgentTUIToolApprovalRequest,
|
|
630
|
+
response: AgentTUIToolApprovalResponse,
|
|
631
|
+
) {
|
|
632
|
+
const part = message.parts[request.partIndex];
|
|
633
|
+
|
|
634
|
+
if (!part || !isToolUIPart(part) || part.toolCallId !== request.toolCallId) {
|
|
635
|
+
throw new Error(
|
|
636
|
+
`Could not find tool approval request ${request.approvalId}.`,
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
part.state = 'approval-responded';
|
|
641
|
+
part.approval = {
|
|
642
|
+
id: request.approvalId,
|
|
643
|
+
approved: response.approved,
|
|
644
|
+
...(response.reason ? { reason: response.reason } : {}),
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function formatStreamError(error: unknown) {
|
|
649
|
+
if (error instanceof Error) {
|
|
650
|
+
return error.message;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
if (typeof error === 'string') {
|
|
654
|
+
return error;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
return JSON.stringify(error);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function fileToDataUrl(mediaType: string, base64: string) {
|
|
661
|
+
return `data:${mediaType};base64,${base64}`;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function isInterruptedError(error: unknown) {
|
|
665
|
+
return error instanceof Error && error.message === 'Interrupted';
|
|
666
|
+
}
|