@jmoyers/harness 0.1.8 → 0.1.10
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/README.md +33 -155
- package/package.json +5 -1
- package/packages/harness-ai/src/anthropic-client.ts +99 -0
- package/packages/harness-ai/src/anthropic-protocol.ts +581 -0
- package/packages/harness-ai/src/anthropic-provider.ts +82 -0
- package/packages/harness-ai/src/async-iterable-stream.ts +65 -0
- package/packages/harness-ai/src/index.ts +36 -0
- package/packages/harness-ai/src/json-parse.ts +66 -0
- package/packages/harness-ai/src/sse.ts +80 -0
- package/packages/harness-ai/src/stream-object.ts +96 -0
- package/packages/harness-ai/src/stream-text.ts +1340 -0
- package/packages/harness-ai/src/types.ts +330 -0
- package/packages/harness-ai/src/ui-stream.ts +217 -0
- package/scripts/codex-live-mux-runtime.ts +123 -7
- package/scripts/control-plane-daemon.ts +20 -3
- package/scripts/harness.ts +566 -133
- package/src/cli/gateway-record.ts +16 -1
- package/src/control-plane/agent-realtime-api.ts +4 -0
- package/src/control-plane/prompt/agent-prompt-extractor.ts +191 -0
- package/src/control-plane/prompt/extractors/claude-prompt-extractor.ts +53 -0
- package/src/control-plane/prompt/extractors/codex-prompt-extractor.ts +50 -0
- package/src/control-plane/prompt/extractors/cursor-prompt-extractor.ts +56 -0
- package/src/control-plane/prompt/session-prompt-engine.ts +69 -0
- package/src/control-plane/prompt/thread-title-namer.ts +290 -0
- package/src/control-plane/stream-command-parser.ts +12 -0
- package/src/control-plane/stream-protocol.ts +109 -0
- package/src/control-plane/stream-server-command.ts +14 -0
- package/src/control-plane/stream-server-session-runtime.ts +12 -0
- package/src/control-plane/stream-server.ts +485 -19
- package/src/mux/input-shortcuts.ts +9 -0
- package/src/mux/live-mux/critique-review.ts +5 -1
- package/src/mux/live-mux/git-parsing.ts +24 -0
- package/src/mux/live-mux/global-shortcut-handlers.ts +8 -0
- package/src/mux/render-frame.ts +1 -1
- package/src/pty/pty_host.ts +46 -1
- package/src/services/control-plane.ts +22 -0
- package/src/services/runtime-control-actions.ts +69 -0
- package/src/services/runtime-navigation-input.ts +4 -0
- package/src/services/runtime-rail-input.ts +4 -0
- package/src/services/runtime-workspace-actions.ts +5 -0
- package/src/ui/global-shortcut-input.ts +2 -0
|
@@ -0,0 +1,1340 @@
|
|
|
1
|
+
import {
|
|
2
|
+
collectReadableStream,
|
|
3
|
+
consumeReadableStream,
|
|
4
|
+
toAsyncIterableStream,
|
|
5
|
+
} from './async-iterable-stream.ts';
|
|
6
|
+
import {
|
|
7
|
+
postAnthropicMessagesStream,
|
|
8
|
+
type AnthropicMessagesRequestBody,
|
|
9
|
+
} from './anthropic-client.ts';
|
|
10
|
+
import {
|
|
11
|
+
mapAnthropicStopReason,
|
|
12
|
+
type AnthropicContentBlock,
|
|
13
|
+
type AnthropicUsage,
|
|
14
|
+
} from './anthropic-protocol.ts';
|
|
15
|
+
import { safeJsonParse } from './json-parse.ts';
|
|
16
|
+
import { createUIMessageStream, createUIMessageStreamResponse } from './ui-stream.ts';
|
|
17
|
+
import type {
|
|
18
|
+
AssistantToolCallPart,
|
|
19
|
+
FinishReason,
|
|
20
|
+
GenerateTextResult,
|
|
21
|
+
HarnessAnthropicModel,
|
|
22
|
+
LanguageModelRequestMetadata,
|
|
23
|
+
LanguageModelResponseMetadata,
|
|
24
|
+
LanguageModelUsage,
|
|
25
|
+
ModelMessage,
|
|
26
|
+
ProviderMetadata,
|
|
27
|
+
StreamTextOptions,
|
|
28
|
+
StreamTextPart,
|
|
29
|
+
StreamTextResult,
|
|
30
|
+
TextContentPart,
|
|
31
|
+
ToolDefinition,
|
|
32
|
+
ToolModelMessage,
|
|
33
|
+
ToolResultContentPart,
|
|
34
|
+
ToolSet,
|
|
35
|
+
TypedToolCall,
|
|
36
|
+
TypedToolError,
|
|
37
|
+
TypedToolResult,
|
|
38
|
+
} from './types.ts';
|
|
39
|
+
|
|
40
|
+
interface Deferred<T> {
|
|
41
|
+
readonly promise: Promise<T>;
|
|
42
|
+
resolve(value: T): void;
|
|
43
|
+
reject(error: unknown): void;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function createDeferred<T>(): Deferred<T> {
|
|
47
|
+
let resolveFn: ((value: T) => void) | null = null;
|
|
48
|
+
let rejectFn: ((error: unknown) => void) | null = null;
|
|
49
|
+
const promise = new Promise<T>((resolve, reject) => {
|
|
50
|
+
resolveFn = resolve;
|
|
51
|
+
rejectFn = reject;
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
promise,
|
|
56
|
+
resolve(value) {
|
|
57
|
+
resolveFn?.(value);
|
|
58
|
+
},
|
|
59
|
+
reject(error) {
|
|
60
|
+
rejectFn?.(error);
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface ContentBlockTextState {
|
|
66
|
+
readonly kind: 'text' | 'reasoning';
|
|
67
|
+
readonly id: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
interface ContentBlockToolState {
|
|
71
|
+
readonly kind: 'tool';
|
|
72
|
+
readonly id: string;
|
|
73
|
+
readonly toolCallId: string;
|
|
74
|
+
readonly toolName: string;
|
|
75
|
+
readonly providerExecuted: boolean;
|
|
76
|
+
inputText: string;
|
|
77
|
+
readonly providerMetadata?: ProviderMetadata;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
type ContentBlockState = ContentBlockTextState | ContentBlockToolState;
|
|
81
|
+
|
|
82
|
+
interface ToolNameMap {
|
|
83
|
+
readonly providerToCustom: Map<string, string>;
|
|
84
|
+
readonly customToProvider: Map<string, string>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function addUsage(left: LanguageModelUsage, right: LanguageModelUsage): LanguageModelUsage {
|
|
88
|
+
return {
|
|
89
|
+
inputTokens: left.inputTokens + right.inputTokens,
|
|
90
|
+
outputTokens: left.outputTokens + right.outputTokens,
|
|
91
|
+
totalTokens: left.totalTokens + right.totalTokens,
|
|
92
|
+
reasoningTokens: (left.reasoningTokens ?? 0) + (right.reasoningTokens ?? 0),
|
|
93
|
+
cachedInputTokens: (left.cachedInputTokens ?? 0) + (right.cachedInputTokens ?? 0),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function usageFromAnthropic(usage: AnthropicUsage | undefined): LanguageModelUsage {
|
|
98
|
+
const inputTokens = usage?.input_tokens ?? 0;
|
|
99
|
+
const outputTokens = usage?.output_tokens ?? 0;
|
|
100
|
+
return {
|
|
101
|
+
inputTokens,
|
|
102
|
+
outputTokens,
|
|
103
|
+
totalTokens: inputTokens + outputTokens,
|
|
104
|
+
cachedInputTokens:
|
|
105
|
+
(usage?.cache_read_input_tokens ?? 0) + (usage?.cache_creation_input_tokens ?? 0),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function normalizeMessages(
|
|
110
|
+
prompt: string | undefined,
|
|
111
|
+
messages: ModelMessage[] | undefined,
|
|
112
|
+
system: string | undefined,
|
|
113
|
+
): { readonly system: string | undefined; readonly messages: ModelMessage[] } {
|
|
114
|
+
if (prompt !== undefined && messages !== undefined) {
|
|
115
|
+
throw new Error('provide either prompt or messages, not both');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (prompt !== undefined) {
|
|
119
|
+
return {
|
|
120
|
+
system,
|
|
121
|
+
messages: [{ role: 'user', content: prompt }],
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (messages === undefined || messages.length === 0) {
|
|
126
|
+
throw new Error('messages or prompt is required');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const copied = [...messages];
|
|
130
|
+
if (system === undefined) {
|
|
131
|
+
const first = copied[0];
|
|
132
|
+
if (first?.role === 'system') {
|
|
133
|
+
return {
|
|
134
|
+
system: first.content,
|
|
135
|
+
messages: copied.slice(1),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
system,
|
|
142
|
+
messages: copied,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function toAnthropicMessageContent(message: ModelMessage): Array<Record<string, unknown>> {
|
|
147
|
+
if (message.role === 'system') {
|
|
148
|
+
return [{ type: 'text', text: message.content }];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (typeof message.content === 'string') {
|
|
152
|
+
return [{ type: 'text', text: message.content }];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (message.role === 'tool') {
|
|
156
|
+
return message.content.map((part) => ({
|
|
157
|
+
type: 'tool_result',
|
|
158
|
+
tool_use_id: part.toolCallId,
|
|
159
|
+
content:
|
|
160
|
+
typeof part.output === 'string'
|
|
161
|
+
? [{ type: 'text', text: part.output }]
|
|
162
|
+
: [{ type: 'text', text: JSON.stringify(part.output) }],
|
|
163
|
+
is_error: part.isError === true,
|
|
164
|
+
}));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return message.content.map((part) => {
|
|
168
|
+
if (part.type === 'text') {
|
|
169
|
+
return {
|
|
170
|
+
type: 'text',
|
|
171
|
+
text: part.text,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
type: 'tool_use',
|
|
177
|
+
id: part.toolCallId,
|
|
178
|
+
name: part.toolName,
|
|
179
|
+
input: part.input,
|
|
180
|
+
};
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function toAnthropicMessages(messages: ModelMessage[]): Array<Record<string, unknown>> {
|
|
185
|
+
const output: Array<Record<string, unknown>> = [];
|
|
186
|
+
for (const message of messages) {
|
|
187
|
+
if (message.role === 'system') {
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const role = message.role === 'tool' ? 'user' : message.role;
|
|
192
|
+
output.push({
|
|
193
|
+
role,
|
|
194
|
+
content: toAnthropicMessageContent(message),
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
return output;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function normalizeToolDefinition(
|
|
201
|
+
name: string,
|
|
202
|
+
definition: ToolDefinition<unknown, unknown>,
|
|
203
|
+
): {
|
|
204
|
+
readonly requestTool: Record<string, unknown>;
|
|
205
|
+
readonly providerName: string;
|
|
206
|
+
readonly dynamic?: boolean;
|
|
207
|
+
readonly title?: string;
|
|
208
|
+
} {
|
|
209
|
+
if (definition.type === 'provider') {
|
|
210
|
+
const metadata: {
|
|
211
|
+
dynamic?: boolean;
|
|
212
|
+
title?: string;
|
|
213
|
+
} = {};
|
|
214
|
+
if (definition.dynamic !== undefined) {
|
|
215
|
+
metadata.dynamic = definition.dynamic;
|
|
216
|
+
}
|
|
217
|
+
if (definition.title !== undefined) {
|
|
218
|
+
metadata.title = definition.title;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
requestTool: {
|
|
223
|
+
type: definition.anthropicType,
|
|
224
|
+
name: definition.name,
|
|
225
|
+
...(definition.description !== undefined ? { description: definition.description } : {}),
|
|
226
|
+
...(definition.inputSchema !== undefined ? { input_schema: definition.inputSchema } : {}),
|
|
227
|
+
...(definition.settings !== undefined ? definition.settings : {}),
|
|
228
|
+
},
|
|
229
|
+
providerName: definition.name,
|
|
230
|
+
...metadata,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const metadata: {
|
|
235
|
+
dynamic?: boolean;
|
|
236
|
+
title?: string;
|
|
237
|
+
} = {};
|
|
238
|
+
if (definition.dynamic !== undefined) {
|
|
239
|
+
metadata.dynamic = definition.dynamic;
|
|
240
|
+
}
|
|
241
|
+
if (definition.title !== undefined) {
|
|
242
|
+
metadata.title = definition.title;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
requestTool: {
|
|
247
|
+
name,
|
|
248
|
+
description: definition.description ?? name,
|
|
249
|
+
input_schema: definition.inputSchema ?? { type: 'object', properties: {} },
|
|
250
|
+
},
|
|
251
|
+
providerName: name,
|
|
252
|
+
...metadata,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function buildTools<TOOLS extends ToolSet>(
|
|
257
|
+
tools: TOOLS | undefined,
|
|
258
|
+
): {
|
|
259
|
+
readonly requestTools: Array<Record<string, unknown>> | undefined;
|
|
260
|
+
readonly nameMap: ToolNameMap;
|
|
261
|
+
readonly toolMeta: Map<string, { readonly dynamic?: boolean; readonly title?: string }>;
|
|
262
|
+
} {
|
|
263
|
+
if (tools === undefined) {
|
|
264
|
+
return {
|
|
265
|
+
requestTools: undefined,
|
|
266
|
+
nameMap: {
|
|
267
|
+
providerToCustom: new Map<string, string>(),
|
|
268
|
+
customToProvider: new Map<string, string>(),
|
|
269
|
+
},
|
|
270
|
+
toolMeta: new Map<string, { readonly dynamic?: boolean; readonly title?: string }>(),
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const requestTools: Array<Record<string, unknown>> = [];
|
|
275
|
+
const providerToCustom = new Map<string, string>();
|
|
276
|
+
const customToProvider = new Map<string, string>();
|
|
277
|
+
const toolMeta = new Map<string, { readonly dynamic?: boolean; readonly title?: string }>();
|
|
278
|
+
|
|
279
|
+
for (const [customName, definition] of Object.entries(tools)) {
|
|
280
|
+
const normalized = normalizeToolDefinition(customName, definition);
|
|
281
|
+
requestTools.push(normalized.requestTool);
|
|
282
|
+
providerToCustom.set(normalized.providerName, customName);
|
|
283
|
+
customToProvider.set(customName, normalized.providerName);
|
|
284
|
+
const metadata: {
|
|
285
|
+
dynamic?: boolean;
|
|
286
|
+
title?: string;
|
|
287
|
+
} = {};
|
|
288
|
+
if (normalized.dynamic !== undefined) {
|
|
289
|
+
metadata.dynamic = normalized.dynamic;
|
|
290
|
+
}
|
|
291
|
+
if (normalized.title !== undefined) {
|
|
292
|
+
metadata.title = normalized.title;
|
|
293
|
+
}
|
|
294
|
+
toolMeta.set(customName, metadata);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return {
|
|
298
|
+
requestTools,
|
|
299
|
+
nameMap: {
|
|
300
|
+
providerToCustom,
|
|
301
|
+
customToProvider,
|
|
302
|
+
},
|
|
303
|
+
toolMeta,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function resolveToolName(rawName: string, nameMap: ToolNameMap): string {
|
|
308
|
+
return nameMap.providerToCustom.get(rawName) ?? rawName;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
interface StepRunResult<TOOLS extends ToolSet> {
|
|
312
|
+
readonly finishReason: FinishReason;
|
|
313
|
+
readonly rawFinishReason?: string;
|
|
314
|
+
readonly usage: LanguageModelUsage;
|
|
315
|
+
readonly response: LanguageModelResponseMetadata;
|
|
316
|
+
readonly providerMetadata?: ProviderMetadata;
|
|
317
|
+
readonly toolCalls: TypedToolCall<TOOLS>[];
|
|
318
|
+
readonly providerResults: Array<TypedToolResult<TOOLS> | TypedToolError<TOOLS>>;
|
|
319
|
+
readonly assistantText: string;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
323
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
return value as Record<string, unknown>;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function mapWebSearchResult(result: AnthropicContentBlock):
|
|
330
|
+
| {
|
|
331
|
+
readonly ok: true;
|
|
332
|
+
readonly output: unknown[];
|
|
333
|
+
readonly sources: Array<{ url: string; title?: string; pageAge?: string }>;
|
|
334
|
+
}
|
|
335
|
+
| { readonly ok: false; readonly error: unknown } {
|
|
336
|
+
if (result.type !== 'web_search_tool_result') {
|
|
337
|
+
return {
|
|
338
|
+
ok: false,
|
|
339
|
+
error: { message: 'invalid web search result payload' },
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (Array.isArray(result.content)) {
|
|
344
|
+
const mapped = result.content.map((entry) => ({
|
|
345
|
+
type: entry.type,
|
|
346
|
+
url: entry.url,
|
|
347
|
+
...(entry.title !== undefined ? { title: entry.title } : {}),
|
|
348
|
+
...(entry.page_age !== undefined ? { pageAge: entry.page_age } : {}),
|
|
349
|
+
...(entry.encrypted_content !== undefined
|
|
350
|
+
? { encryptedContent: entry.encrypted_content }
|
|
351
|
+
: {}),
|
|
352
|
+
}));
|
|
353
|
+
return {
|
|
354
|
+
ok: true,
|
|
355
|
+
output: mapped,
|
|
356
|
+
sources: result.content.map((entry) => ({
|
|
357
|
+
url: entry.url,
|
|
358
|
+
...(entry.title !== undefined ? { title: entry.title } : {}),
|
|
359
|
+
...(entry.page_age !== undefined ? { pageAge: entry.page_age } : {}),
|
|
360
|
+
})),
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return {
|
|
365
|
+
ok: false,
|
|
366
|
+
error: {
|
|
367
|
+
type: result.content.type,
|
|
368
|
+
errorCode: result.content.error_code,
|
|
369
|
+
},
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function mapWebFetchResult(
|
|
374
|
+
result: AnthropicContentBlock,
|
|
375
|
+
):
|
|
376
|
+
| { readonly ok: true; readonly output: unknown }
|
|
377
|
+
| { readonly ok: false; readonly error: unknown } {
|
|
378
|
+
if (result.type !== 'web_fetch_tool_result') {
|
|
379
|
+
return {
|
|
380
|
+
ok: false,
|
|
381
|
+
error: { message: 'invalid web fetch result payload' },
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if ('url' in result.content && 'content' in result.content) {
|
|
386
|
+
const content = result.content.content;
|
|
387
|
+
return {
|
|
388
|
+
ok: true,
|
|
389
|
+
output: {
|
|
390
|
+
type: 'web_fetch_result',
|
|
391
|
+
url: result.content.url,
|
|
392
|
+
...(result.content.retrieved_at !== undefined
|
|
393
|
+
? { retrievedAt: result.content.retrieved_at }
|
|
394
|
+
: {}),
|
|
395
|
+
content: {
|
|
396
|
+
type: content.type,
|
|
397
|
+
...(content.title !== undefined ? { title: content.title } : {}),
|
|
398
|
+
source: {
|
|
399
|
+
type: content.source.type,
|
|
400
|
+
mediaType: content.source.media_type,
|
|
401
|
+
data: content.source.data,
|
|
402
|
+
},
|
|
403
|
+
...(content.citations !== undefined ? { citations: content.citations } : {}),
|
|
404
|
+
},
|
|
405
|
+
},
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return {
|
|
410
|
+
ok: false,
|
|
411
|
+
error: {
|
|
412
|
+
type: result.content.type,
|
|
413
|
+
errorCode: 'error_code' in result.content ? result.content.error_code : undefined,
|
|
414
|
+
},
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function parseToolInput(inputText: string): {
|
|
419
|
+
readonly input: unknown;
|
|
420
|
+
readonly invalid: boolean;
|
|
421
|
+
readonly error?: string;
|
|
422
|
+
} {
|
|
423
|
+
const parsed = safeJsonParse(inputText);
|
|
424
|
+
if (parsed !== undefined) {
|
|
425
|
+
return { input: parsed, invalid: false };
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return {
|
|
429
|
+
input: inputText,
|
|
430
|
+
invalid: true,
|
|
431
|
+
error: 'Invalid JSON tool input',
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
async function runSingleStep<TOOLS extends ToolSet>(
|
|
436
|
+
model: HarnessAnthropicModel,
|
|
437
|
+
requestBody: AnthropicMessagesRequestBody,
|
|
438
|
+
options: {
|
|
439
|
+
readonly includeRawChunks: boolean;
|
|
440
|
+
readonly abortSignal?: AbortSignal;
|
|
441
|
+
readonly nameMap: ToolNameMap;
|
|
442
|
+
readonly toolMeta: Map<string, { readonly dynamic?: boolean; readonly title?: string }>;
|
|
443
|
+
readonly emit: (part: StreamTextPart<TOOLS>) => void;
|
|
444
|
+
},
|
|
445
|
+
): Promise<StepRunResult<TOOLS>> {
|
|
446
|
+
const streamResponse = await postAnthropicMessagesStream(model, requestBody, options.abortSignal);
|
|
447
|
+
|
|
448
|
+
const blockStates = new Map<number, ContentBlockState>();
|
|
449
|
+
const toolCalls: TypedToolCall<TOOLS>[] = [];
|
|
450
|
+
const providerResults: Array<TypedToolResult<TOOLS> | TypedToolError<TOOLS>> = [];
|
|
451
|
+
let finishReason: FinishReason = 'other';
|
|
452
|
+
let rawFinishReason: string | undefined;
|
|
453
|
+
let usage = usageFromAnthropic(undefined);
|
|
454
|
+
let response: LanguageModelResponseMetadata = {
|
|
455
|
+
timestamp: new Date(),
|
|
456
|
+
headers: streamResponse.responseHeaders,
|
|
457
|
+
};
|
|
458
|
+
let assistantText = '';
|
|
459
|
+
|
|
460
|
+
const emitToolCallFromState = (state: ContentBlockToolState): TypedToolCall<TOOLS> => {
|
|
461
|
+
const finalInputText = state.inputText.length === 0 ? '{}' : state.inputText;
|
|
462
|
+
const parsed = parseToolInput(finalInputText);
|
|
463
|
+
const meta = options.toolMeta.get(state.toolName);
|
|
464
|
+
|
|
465
|
+
const call: TypedToolCall<TOOLS> = {
|
|
466
|
+
toolCallId: state.toolCallId,
|
|
467
|
+
toolName: state.toolName,
|
|
468
|
+
input: parsed.input,
|
|
469
|
+
providerExecuted: state.providerExecuted,
|
|
470
|
+
...(meta?.dynamic !== undefined ? { dynamic: meta.dynamic } : {}),
|
|
471
|
+
...(meta?.title !== undefined ? { title: meta.title } : {}),
|
|
472
|
+
...(state.providerMetadata !== undefined ? { providerMetadata: state.providerMetadata } : {}),
|
|
473
|
+
...(parsed.invalid ? { invalid: true, error: parsed.error } : {}),
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
options.emit({ type: 'tool-call', ...call });
|
|
477
|
+
toolCalls.push(call);
|
|
478
|
+
return call;
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
const reader = streamResponse.stream.getReader();
|
|
482
|
+
try {
|
|
483
|
+
while (true) {
|
|
484
|
+
const { value, done } = await reader.read();
|
|
485
|
+
if (done) {
|
|
486
|
+
break;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
if (value === undefined) {
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (options.includeRawChunks) {
|
|
494
|
+
options.emit({ type: 'raw', rawValue: value.rawValue });
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if (value.parseError !== undefined) {
|
|
498
|
+
options.emit({
|
|
499
|
+
type: 'error',
|
|
500
|
+
error: new Error(`failed to parse anthropic event: ${value.parseError}`),
|
|
501
|
+
});
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const chunk = value.chunk;
|
|
506
|
+
if (chunk === null) {
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (chunk.type === 'ping') {
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
if (chunk.type === 'message_start') {
|
|
515
|
+
usage = usageFromAnthropic(chunk.message.usage);
|
|
516
|
+
response = {
|
|
517
|
+
timestamp: new Date(),
|
|
518
|
+
headers: streamResponse.responseHeaders,
|
|
519
|
+
...(chunk.message.id !== undefined ? { id: chunk.message.id } : {}),
|
|
520
|
+
...(chunk.message.model !== undefined ? { modelId: chunk.message.model } : {}),
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
if (chunk.message.stop_reason !== undefined) {
|
|
524
|
+
rawFinishReason = chunk.message.stop_reason ?? undefined;
|
|
525
|
+
finishReason = mapAnthropicStopReason(chunk.message.stop_reason);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (chunk.message.content !== undefined) {
|
|
529
|
+
for (const part of chunk.message.content) {
|
|
530
|
+
if (part.type !== 'tool_use' && part.type !== 'server_tool_use') {
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const toolName = resolveToolName(part.name, options.nameMap);
|
|
535
|
+
const meta = options.toolMeta.get(toolName);
|
|
536
|
+
const inputText = JSON.stringify(part.input ?? {});
|
|
537
|
+
|
|
538
|
+
options.emit({
|
|
539
|
+
type: 'tool-input-start',
|
|
540
|
+
id: part.id,
|
|
541
|
+
toolName,
|
|
542
|
+
providerExecuted: part.type === 'server_tool_use',
|
|
543
|
+
...(meta?.dynamic !== undefined ? { dynamic: meta.dynamic } : {}),
|
|
544
|
+
...(meta?.title !== undefined ? { title: meta.title } : {}),
|
|
545
|
+
});
|
|
546
|
+
options.emit({
|
|
547
|
+
type: 'tool-input-delta',
|
|
548
|
+
id: part.id,
|
|
549
|
+
delta: inputText,
|
|
550
|
+
});
|
|
551
|
+
options.emit({
|
|
552
|
+
type: 'tool-input-end',
|
|
553
|
+
id: part.id,
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
const state: ContentBlockToolState = {
|
|
557
|
+
kind: 'tool',
|
|
558
|
+
id: String(part.id),
|
|
559
|
+
toolCallId: part.id,
|
|
560
|
+
toolName,
|
|
561
|
+
providerExecuted: part.type === 'server_tool_use',
|
|
562
|
+
inputText,
|
|
563
|
+
};
|
|
564
|
+
emitToolCallFromState(state);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
if (chunk.type === 'message_delta') {
|
|
572
|
+
usage = usageFromAnthropic(chunk.usage);
|
|
573
|
+
if (chunk.delta?.stop_reason !== undefined) {
|
|
574
|
+
rawFinishReason = chunk.delta.stop_reason ?? undefined;
|
|
575
|
+
finishReason = mapAnthropicStopReason(chunk.delta.stop_reason);
|
|
576
|
+
}
|
|
577
|
+
continue;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
if (chunk.type === 'content_block_start') {
|
|
581
|
+
const block = chunk.content_block;
|
|
582
|
+
|
|
583
|
+
if (block.type === 'text') {
|
|
584
|
+
const state: ContentBlockTextState = {
|
|
585
|
+
kind: 'text',
|
|
586
|
+
id: String(chunk.index),
|
|
587
|
+
};
|
|
588
|
+
blockStates.set(chunk.index, state);
|
|
589
|
+
options.emit({ type: 'text-start', id: state.id });
|
|
590
|
+
if (block.text !== undefined && block.text.length > 0) {
|
|
591
|
+
assistantText += block.text;
|
|
592
|
+
options.emit({ type: 'text-delta', id: state.id, text: block.text });
|
|
593
|
+
}
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
if (block.type === 'thinking' || block.type === 'redacted_thinking') {
|
|
598
|
+
const state: ContentBlockTextState = {
|
|
599
|
+
kind: 'reasoning',
|
|
600
|
+
id: String(chunk.index),
|
|
601
|
+
};
|
|
602
|
+
blockStates.set(chunk.index, state);
|
|
603
|
+
options.emit({
|
|
604
|
+
type: 'reasoning-start',
|
|
605
|
+
id: state.id,
|
|
606
|
+
...(block.type === 'redacted_thinking' && block.data !== undefined
|
|
607
|
+
? {
|
|
608
|
+
providerMetadata: {
|
|
609
|
+
anthropic: {
|
|
610
|
+
redactedData: block.data,
|
|
611
|
+
},
|
|
612
|
+
},
|
|
613
|
+
}
|
|
614
|
+
: {}),
|
|
615
|
+
});
|
|
616
|
+
if (block.thinking !== undefined && block.thinking.length > 0) {
|
|
617
|
+
options.emit({ type: 'reasoning-delta', id: state.id, text: block.thinking });
|
|
618
|
+
}
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
if (block.type === 'tool_use' || block.type === 'server_tool_use') {
|
|
623
|
+
const toolName = resolveToolName(block.name, options.nameMap);
|
|
624
|
+
const meta = options.toolMeta.get(toolName);
|
|
625
|
+
const state: ContentBlockToolState = {
|
|
626
|
+
kind: 'tool',
|
|
627
|
+
id: String(chunk.index),
|
|
628
|
+
toolCallId: block.id,
|
|
629
|
+
toolName,
|
|
630
|
+
providerExecuted: block.type === 'server_tool_use',
|
|
631
|
+
inputText: '',
|
|
632
|
+
};
|
|
633
|
+
|
|
634
|
+
blockStates.set(chunk.index, state);
|
|
635
|
+
|
|
636
|
+
options.emit({
|
|
637
|
+
type: 'tool-input-start',
|
|
638
|
+
id: state.toolCallId,
|
|
639
|
+
toolName,
|
|
640
|
+
providerExecuted: state.providerExecuted,
|
|
641
|
+
...(meta?.dynamic !== undefined ? { dynamic: meta.dynamic } : {}),
|
|
642
|
+
...(meta?.title !== undefined ? { title: meta.title } : {}),
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
const hasStartInput = block.input !== undefined && Object.keys(block.input).length > 0;
|
|
646
|
+
if (hasStartInput) {
|
|
647
|
+
const delta = JSON.stringify(block.input);
|
|
648
|
+
state.inputText += delta;
|
|
649
|
+
options.emit({ type: 'tool-input-delta', id: state.toolCallId, delta });
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
if (block.type === 'web_search_tool_result') {
|
|
656
|
+
const toolName = resolveToolName('web_search', options.nameMap);
|
|
657
|
+
const mapped = mapWebSearchResult(block);
|
|
658
|
+
if (mapped.ok) {
|
|
659
|
+
const result: TypedToolResult<TOOLS> = {
|
|
660
|
+
toolCallId: block.tool_use_id,
|
|
661
|
+
toolName,
|
|
662
|
+
output: mapped.output,
|
|
663
|
+
providerExecuted: true,
|
|
664
|
+
};
|
|
665
|
+
providerResults.push(result);
|
|
666
|
+
options.emit({ type: 'tool-result', ...result });
|
|
667
|
+
|
|
668
|
+
for (const source of mapped.sources) {
|
|
669
|
+
options.emit({
|
|
670
|
+
type: 'source',
|
|
671
|
+
id: `${block.tool_use_id}:${source.url}`,
|
|
672
|
+
sourceType: 'url',
|
|
673
|
+
url: source.url,
|
|
674
|
+
...(source.title !== undefined ? { title: source.title } : {}),
|
|
675
|
+
...(source.pageAge !== undefined
|
|
676
|
+
? {
|
|
677
|
+
providerMetadata: {
|
|
678
|
+
anthropic: {
|
|
679
|
+
pageAge: source.pageAge,
|
|
680
|
+
},
|
|
681
|
+
},
|
|
682
|
+
}
|
|
683
|
+
: {}),
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
} else {
|
|
687
|
+
const error: TypedToolError<TOOLS> = {
|
|
688
|
+
toolCallId: block.tool_use_id,
|
|
689
|
+
toolName,
|
|
690
|
+
error: mapped.error,
|
|
691
|
+
providerExecuted: true,
|
|
692
|
+
};
|
|
693
|
+
providerResults.push(error);
|
|
694
|
+
options.emit({ type: 'tool-error', ...error });
|
|
695
|
+
}
|
|
696
|
+
continue;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
if (block.type === 'web_fetch_tool_result') {
|
|
700
|
+
const toolName = resolveToolName('web_fetch', options.nameMap);
|
|
701
|
+
const mapped = mapWebFetchResult(block);
|
|
702
|
+
if (mapped.ok) {
|
|
703
|
+
const result: TypedToolResult<TOOLS> = {
|
|
704
|
+
toolCallId: block.tool_use_id,
|
|
705
|
+
toolName,
|
|
706
|
+
output: mapped.output,
|
|
707
|
+
providerExecuted: true,
|
|
708
|
+
};
|
|
709
|
+
providerResults.push(result);
|
|
710
|
+
options.emit({ type: 'tool-result', ...result });
|
|
711
|
+
} else {
|
|
712
|
+
const error: TypedToolError<TOOLS> = {
|
|
713
|
+
toolCallId: block.tool_use_id,
|
|
714
|
+
toolName,
|
|
715
|
+
error: mapped.error,
|
|
716
|
+
providerExecuted: true,
|
|
717
|
+
};
|
|
718
|
+
providerResults.push(error);
|
|
719
|
+
options.emit({ type: 'tool-error', ...error });
|
|
720
|
+
}
|
|
721
|
+
continue;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
continue;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
if (chunk.type === 'content_block_delta') {
|
|
728
|
+
const state = blockStates.get(chunk.index);
|
|
729
|
+
if (state === undefined) {
|
|
730
|
+
continue;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
if (chunk.delta.type === 'text_delta' && state.kind === 'text') {
|
|
734
|
+
assistantText += chunk.delta.text;
|
|
735
|
+
options.emit({ type: 'text-delta', id: state.id, text: chunk.delta.text });
|
|
736
|
+
continue;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
if (chunk.delta.type === 'thinking_delta' && state.kind === 'reasoning') {
|
|
740
|
+
options.emit({ type: 'reasoning-delta', id: state.id, text: chunk.delta.thinking });
|
|
741
|
+
continue;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
if (chunk.delta.type === 'signature_delta' && state.kind === 'reasoning') {
|
|
745
|
+
options.emit({
|
|
746
|
+
type: 'reasoning-delta',
|
|
747
|
+
id: state.id,
|
|
748
|
+
text: '',
|
|
749
|
+
providerMetadata: {
|
|
750
|
+
anthropic: {
|
|
751
|
+
signature: chunk.delta.signature,
|
|
752
|
+
},
|
|
753
|
+
},
|
|
754
|
+
});
|
|
755
|
+
continue;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
if (chunk.delta.type === 'input_json_delta' && state.kind === 'tool') {
|
|
759
|
+
if (chunk.delta.partial_json.length === 0) {
|
|
760
|
+
continue;
|
|
761
|
+
}
|
|
762
|
+
state.inputText += chunk.delta.partial_json;
|
|
763
|
+
options.emit({
|
|
764
|
+
type: 'tool-input-delta',
|
|
765
|
+
id: state.toolCallId,
|
|
766
|
+
delta: chunk.delta.partial_json,
|
|
767
|
+
});
|
|
768
|
+
continue;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
continue;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
if (chunk.type === 'content_block_stop') {
|
|
775
|
+
const state = blockStates.get(chunk.index);
|
|
776
|
+
if (state === undefined) {
|
|
777
|
+
continue;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
if (state.kind === 'text') {
|
|
781
|
+
options.emit({ type: 'text-end', id: state.id });
|
|
782
|
+
blockStates.delete(chunk.index);
|
|
783
|
+
continue;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
if (state.kind === 'reasoning') {
|
|
787
|
+
options.emit({ type: 'reasoning-end', id: state.id });
|
|
788
|
+
blockStates.delete(chunk.index);
|
|
789
|
+
continue;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
if (state.kind === 'tool') {
|
|
793
|
+
options.emit({ type: 'tool-input-end', id: state.toolCallId });
|
|
794
|
+
emitToolCallFromState(state);
|
|
795
|
+
blockStates.delete(chunk.index);
|
|
796
|
+
}
|
|
797
|
+
continue;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
if (chunk.type === 'error') {
|
|
801
|
+
const errorRecord = asRecord(chunk.error);
|
|
802
|
+
const message = errorRecord?.['message'];
|
|
803
|
+
options.emit({
|
|
804
|
+
type: 'error',
|
|
805
|
+
error:
|
|
806
|
+
typeof message === 'string' ? new Error(message) : new Error('anthropic stream error'),
|
|
807
|
+
});
|
|
808
|
+
finishReason = 'error';
|
|
809
|
+
continue;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
if (chunk.type === 'message_stop') {
|
|
813
|
+
break;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
} finally {
|
|
817
|
+
reader.releaseLock();
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
return {
|
|
821
|
+
finishReason,
|
|
822
|
+
...(rawFinishReason !== undefined ? { rawFinishReason } : {}),
|
|
823
|
+
usage,
|
|
824
|
+
response,
|
|
825
|
+
...(rawFinishReason !== undefined
|
|
826
|
+
? {
|
|
827
|
+
providerMetadata: {
|
|
828
|
+
anthropic: {
|
|
829
|
+
rawFinishReason,
|
|
830
|
+
},
|
|
831
|
+
},
|
|
832
|
+
}
|
|
833
|
+
: {}),
|
|
834
|
+
toolCalls,
|
|
835
|
+
providerResults,
|
|
836
|
+
assistantText,
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
function ensureAbort(signal: AbortSignal | undefined): void {
|
|
841
|
+
if (signal?.aborted === true) {
|
|
842
|
+
throw new Error('operation aborted');
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
function callRequestBody(
|
|
847
|
+
model: HarnessAnthropicModel,
|
|
848
|
+
stepMessages: ModelMessage[],
|
|
849
|
+
system: string | undefined,
|
|
850
|
+
tools: Array<Record<string, unknown>> | undefined,
|
|
851
|
+
options: {
|
|
852
|
+
readonly maxOutputTokens?: number;
|
|
853
|
+
readonly temperature?: number;
|
|
854
|
+
readonly topP?: number;
|
|
855
|
+
readonly stopSequences?: string[];
|
|
856
|
+
},
|
|
857
|
+
): AnthropicMessagesRequestBody {
|
|
858
|
+
const body: AnthropicMessagesRequestBody = {
|
|
859
|
+
model: model.modelId,
|
|
860
|
+
stream: true,
|
|
861
|
+
messages: toAnthropicMessages(stepMessages),
|
|
862
|
+
...(system !== undefined ? { system } : {}),
|
|
863
|
+
...(options.maxOutputTokens !== undefined ? { max_tokens: options.maxOutputTokens } : {}),
|
|
864
|
+
...(options.temperature !== undefined ? { temperature: options.temperature } : {}),
|
|
865
|
+
...(options.topP !== undefined ? { top_p: options.topP } : {}),
|
|
866
|
+
...(options.stopSequences !== undefined ? { stop_sequences: options.stopSequences } : {}),
|
|
867
|
+
...(tools !== undefined ? { tools } : {}),
|
|
868
|
+
};
|
|
869
|
+
|
|
870
|
+
return body;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
interface ExecutionResult<TOOLS extends ToolSet> {
|
|
874
|
+
readonly emittedResults: Array<TypedToolResult<TOOLS> | TypedToolError<TOOLS>>;
|
|
875
|
+
readonly toolResultMessageParts: ToolResultContentPart[];
|
|
876
|
+
readonly assistantToolCallParts: AssistantToolCallPart[];
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
async function executeToolCalls<TOOLS extends ToolSet>(
|
|
880
|
+
calls: TypedToolCall<TOOLS>[],
|
|
881
|
+
tools: TOOLS | undefined,
|
|
882
|
+
emit: (part: StreamTextPart<TOOLS>) => void,
|
|
883
|
+
): Promise<ExecutionResult<TOOLS>> {
|
|
884
|
+
const emittedResults: Array<TypedToolResult<TOOLS> | TypedToolError<TOOLS>> = [];
|
|
885
|
+
const toolResultMessageParts: ToolResultContentPart[] = [];
|
|
886
|
+
const assistantToolCallParts: AssistantToolCallPart[] = [];
|
|
887
|
+
|
|
888
|
+
for (const call of calls) {
|
|
889
|
+
if (call.providerExecuted === true) {
|
|
890
|
+
continue;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
assistantToolCallParts.push({
|
|
894
|
+
type: 'tool-call',
|
|
895
|
+
toolCallId: call.toolCallId,
|
|
896
|
+
toolName: call.toolName,
|
|
897
|
+
input: call.input,
|
|
898
|
+
});
|
|
899
|
+
|
|
900
|
+
const definition = tools?.[call.toolName as keyof TOOLS];
|
|
901
|
+
if (definition === undefined || definition.type === 'provider') {
|
|
902
|
+
const error: TypedToolError<TOOLS> = {
|
|
903
|
+
toolCallId: call.toolCallId,
|
|
904
|
+
toolName: call.toolName,
|
|
905
|
+
input: call.input,
|
|
906
|
+
error: `No executable tool found for ${call.toolName}`,
|
|
907
|
+
};
|
|
908
|
+
emittedResults.push(error);
|
|
909
|
+
toolResultMessageParts.push({
|
|
910
|
+
type: 'tool-result',
|
|
911
|
+
toolCallId: call.toolCallId,
|
|
912
|
+
toolName: call.toolName,
|
|
913
|
+
output: { error: String(error.error) },
|
|
914
|
+
isError: true,
|
|
915
|
+
});
|
|
916
|
+
emit({ type: 'tool-error', ...error });
|
|
917
|
+
continue;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
if (call.invalid) {
|
|
921
|
+
const error: TypedToolError<TOOLS> = {
|
|
922
|
+
toolCallId: call.toolCallId,
|
|
923
|
+
toolName: call.toolName,
|
|
924
|
+
input: call.input,
|
|
925
|
+
error: call.error ?? 'Invalid tool call',
|
|
926
|
+
};
|
|
927
|
+
emittedResults.push(error);
|
|
928
|
+
toolResultMessageParts.push({
|
|
929
|
+
type: 'tool-result',
|
|
930
|
+
toolCallId: call.toolCallId,
|
|
931
|
+
toolName: call.toolName,
|
|
932
|
+
output: { error: String(error.error) },
|
|
933
|
+
isError: true,
|
|
934
|
+
});
|
|
935
|
+
emit({ type: 'tool-error', ...error });
|
|
936
|
+
continue;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
if (definition.execute === undefined) {
|
|
940
|
+
const error: TypedToolError<TOOLS> = {
|
|
941
|
+
toolCallId: call.toolCallId,
|
|
942
|
+
toolName: call.toolName,
|
|
943
|
+
input: call.input,
|
|
944
|
+
error: `Tool ${call.toolName} is missing execute()`,
|
|
945
|
+
};
|
|
946
|
+
emittedResults.push(error);
|
|
947
|
+
toolResultMessageParts.push({
|
|
948
|
+
type: 'tool-result',
|
|
949
|
+
toolCallId: call.toolCallId,
|
|
950
|
+
toolName: call.toolName,
|
|
951
|
+
output: { error: String(error.error) },
|
|
952
|
+
isError: true,
|
|
953
|
+
});
|
|
954
|
+
emit({ type: 'tool-error', ...error });
|
|
955
|
+
continue;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
try {
|
|
959
|
+
const output = await definition.execute(call.input);
|
|
960
|
+
const result: TypedToolResult<TOOLS> = {
|
|
961
|
+
toolCallId: call.toolCallId,
|
|
962
|
+
toolName: call.toolName,
|
|
963
|
+
input: call.input,
|
|
964
|
+
output,
|
|
965
|
+
};
|
|
966
|
+
emittedResults.push(result);
|
|
967
|
+
toolResultMessageParts.push({
|
|
968
|
+
type: 'tool-result',
|
|
969
|
+
toolCallId: call.toolCallId,
|
|
970
|
+
toolName: call.toolName,
|
|
971
|
+
output,
|
|
972
|
+
});
|
|
973
|
+
emit({ type: 'tool-result', ...result });
|
|
974
|
+
} catch (error) {
|
|
975
|
+
const toolError: TypedToolError<TOOLS> = {
|
|
976
|
+
toolCallId: call.toolCallId,
|
|
977
|
+
toolName: call.toolName,
|
|
978
|
+
input: call.input,
|
|
979
|
+
error,
|
|
980
|
+
};
|
|
981
|
+
emittedResults.push(toolError);
|
|
982
|
+
toolResultMessageParts.push({
|
|
983
|
+
type: 'tool-result',
|
|
984
|
+
toolCallId: call.toolCallId,
|
|
985
|
+
toolName: call.toolName,
|
|
986
|
+
output: {
|
|
987
|
+
error: error instanceof Error ? error.message : String(error),
|
|
988
|
+
},
|
|
989
|
+
isError: true,
|
|
990
|
+
});
|
|
991
|
+
emit({ type: 'tool-error', ...toolError });
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
return {
|
|
996
|
+
emittedResults,
|
|
997
|
+
toolResultMessageParts,
|
|
998
|
+
assistantToolCallParts,
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
interface CollectedStreamResult<TOOLS extends ToolSet> {
|
|
1003
|
+
readonly text: string;
|
|
1004
|
+
readonly toolCalls: TypedToolCall<TOOLS>[];
|
|
1005
|
+
readonly toolResults: Array<TypedToolResult<TOOLS> | TypedToolError<TOOLS>>;
|
|
1006
|
+
readonly finishReason: FinishReason;
|
|
1007
|
+
readonly usage: LanguageModelUsage;
|
|
1008
|
+
readonly response: LanguageModelResponseMetadata;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
async function collectResultFromStream<TOOLS extends ToolSet>(
|
|
1012
|
+
stream: ReadableStream<StreamTextPart<TOOLS>>,
|
|
1013
|
+
): Promise<CollectedStreamResult<TOOLS>> {
|
|
1014
|
+
const reader = stream.getReader();
|
|
1015
|
+
const toolCalls: TypedToolCall<TOOLS>[] = [];
|
|
1016
|
+
const toolResults: Array<TypedToolResult<TOOLS> | TypedToolError<TOOLS>> = [];
|
|
1017
|
+
let text = '';
|
|
1018
|
+
let finishReason: FinishReason = 'other';
|
|
1019
|
+
let usage: LanguageModelUsage = {
|
|
1020
|
+
inputTokens: 0,
|
|
1021
|
+
outputTokens: 0,
|
|
1022
|
+
totalTokens: 0,
|
|
1023
|
+
};
|
|
1024
|
+
let response: LanguageModelResponseMetadata = {
|
|
1025
|
+
timestamp: new Date(),
|
|
1026
|
+
};
|
|
1027
|
+
|
|
1028
|
+
try {
|
|
1029
|
+
while (true) {
|
|
1030
|
+
const { value, done } = await reader.read();
|
|
1031
|
+
if (done) {
|
|
1032
|
+
break;
|
|
1033
|
+
}
|
|
1034
|
+
if (value === undefined) {
|
|
1035
|
+
continue;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
if (value.type === 'text-delta') {
|
|
1039
|
+
text += value.text;
|
|
1040
|
+
} else if (value.type === 'tool-call') {
|
|
1041
|
+
toolCalls.push(value);
|
|
1042
|
+
} else if (value.type === 'tool-result' || value.type === 'tool-error') {
|
|
1043
|
+
toolResults.push(value);
|
|
1044
|
+
} else if (value.type === 'finish-step') {
|
|
1045
|
+
response = value.response;
|
|
1046
|
+
usage = value.usage;
|
|
1047
|
+
} else if (value.type === 'finish') {
|
|
1048
|
+
finishReason = value.finishReason;
|
|
1049
|
+
usage = value.totalUsage;
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
} finally {
|
|
1053
|
+
reader.releaseLock();
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
return {
|
|
1057
|
+
text,
|
|
1058
|
+
toolCalls,
|
|
1059
|
+
toolResults,
|
|
1060
|
+
finishReason,
|
|
1061
|
+
usage,
|
|
1062
|
+
response,
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
export function streamText<TOOLS extends ToolSet>(
|
|
1067
|
+
options: StreamTextOptions<TOOLS>,
|
|
1068
|
+
): StreamTextResult<TOOLS> {
|
|
1069
|
+
const normalized = normalizeMessages(options.prompt, options.messages, options.system);
|
|
1070
|
+
const maxToolRoundtrips = options.maxToolRoundtrips ?? 10;
|
|
1071
|
+
const builtTools = buildTools(options.tools);
|
|
1072
|
+
|
|
1073
|
+
const baseStream = new ReadableStream<StreamTextPart<TOOLS>>({
|
|
1074
|
+
async start(controller) {
|
|
1075
|
+
const emit = (part: StreamTextPart<TOOLS>) => {
|
|
1076
|
+
controller.enqueue(part);
|
|
1077
|
+
};
|
|
1078
|
+
|
|
1079
|
+
const requestWarnings: string[] = [];
|
|
1080
|
+
let conversationMessages = [...normalized.messages];
|
|
1081
|
+
let stepCount = 0;
|
|
1082
|
+
let totalUsage: LanguageModelUsage = {
|
|
1083
|
+
inputTokens: 0,
|
|
1084
|
+
outputTokens: 0,
|
|
1085
|
+
totalTokens: 0,
|
|
1086
|
+
};
|
|
1087
|
+
|
|
1088
|
+
emit({ type: 'start' });
|
|
1089
|
+
|
|
1090
|
+
try {
|
|
1091
|
+
while (true) {
|
|
1092
|
+
ensureAbort(options.abortSignal);
|
|
1093
|
+
if (stepCount >= maxToolRoundtrips) {
|
|
1094
|
+
emit({
|
|
1095
|
+
type: 'error',
|
|
1096
|
+
error: new Error(`maxToolRoundtrips (${maxToolRoundtrips}) exceeded`),
|
|
1097
|
+
});
|
|
1098
|
+
emit({
|
|
1099
|
+
type: 'finish',
|
|
1100
|
+
finishReason: 'error',
|
|
1101
|
+
rawFinishReason: 'max_tool_roundtrips',
|
|
1102
|
+
totalUsage,
|
|
1103
|
+
});
|
|
1104
|
+
break;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
stepCount += 1;
|
|
1108
|
+
const requestOptions: {
|
|
1109
|
+
maxOutputTokens?: number;
|
|
1110
|
+
temperature?: number;
|
|
1111
|
+
topP?: number;
|
|
1112
|
+
stopSequences?: string[];
|
|
1113
|
+
} = {};
|
|
1114
|
+
if (options.maxOutputTokens !== undefined) {
|
|
1115
|
+
requestOptions.maxOutputTokens = options.maxOutputTokens;
|
|
1116
|
+
}
|
|
1117
|
+
if (options.temperature !== undefined) {
|
|
1118
|
+
requestOptions.temperature = options.temperature;
|
|
1119
|
+
}
|
|
1120
|
+
if (options.topP !== undefined) {
|
|
1121
|
+
requestOptions.topP = options.topP;
|
|
1122
|
+
}
|
|
1123
|
+
if (options.stopSequences !== undefined) {
|
|
1124
|
+
requestOptions.stopSequences = options.stopSequences;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
const requestBody = callRequestBody(
|
|
1128
|
+
options.model,
|
|
1129
|
+
conversationMessages,
|
|
1130
|
+
normalized.system,
|
|
1131
|
+
builtTools.requestTools,
|
|
1132
|
+
requestOptions,
|
|
1133
|
+
);
|
|
1134
|
+
|
|
1135
|
+
const requestMeta: LanguageModelRequestMetadata = {
|
|
1136
|
+
body: requestBody as unknown as Record<string, unknown>,
|
|
1137
|
+
};
|
|
1138
|
+
|
|
1139
|
+
emit({
|
|
1140
|
+
type: 'start-step',
|
|
1141
|
+
request: requestMeta,
|
|
1142
|
+
warnings: requestWarnings,
|
|
1143
|
+
});
|
|
1144
|
+
|
|
1145
|
+
const stepRunOptions: {
|
|
1146
|
+
includeRawChunks: boolean;
|
|
1147
|
+
abortSignal?: AbortSignal;
|
|
1148
|
+
nameMap: ToolNameMap;
|
|
1149
|
+
toolMeta: Map<string, { readonly dynamic?: boolean; readonly title?: string }>;
|
|
1150
|
+
emit: (part: StreamTextPart<TOOLS>) => void;
|
|
1151
|
+
} = {
|
|
1152
|
+
includeRawChunks: options.includeRawChunks === true,
|
|
1153
|
+
nameMap: builtTools.nameMap,
|
|
1154
|
+
toolMeta: builtTools.toolMeta,
|
|
1155
|
+
emit,
|
|
1156
|
+
};
|
|
1157
|
+
if (options.abortSignal !== undefined) {
|
|
1158
|
+
stepRunOptions.abortSignal = options.abortSignal;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
const stepResult = await runSingleStep(options.model, requestBody, stepRunOptions);
|
|
1162
|
+
|
|
1163
|
+
const localToolExecution = await executeToolCalls(
|
|
1164
|
+
stepResult.toolCalls,
|
|
1165
|
+
options.tools,
|
|
1166
|
+
emit,
|
|
1167
|
+
);
|
|
1168
|
+
|
|
1169
|
+
const stepUsage = stepResult.usage;
|
|
1170
|
+
totalUsage = addUsage(totalUsage, stepUsage);
|
|
1171
|
+
|
|
1172
|
+
emit({
|
|
1173
|
+
type: 'finish-step',
|
|
1174
|
+
response: stepResult.response,
|
|
1175
|
+
usage: stepUsage,
|
|
1176
|
+
finishReason: stepResult.finishReason,
|
|
1177
|
+
...(stepResult.rawFinishReason !== undefined
|
|
1178
|
+
? { rawFinishReason: stepResult.rawFinishReason }
|
|
1179
|
+
: {}),
|
|
1180
|
+
...(stepResult.providerMetadata !== undefined
|
|
1181
|
+
? { providerMetadata: stepResult.providerMetadata }
|
|
1182
|
+
: {}),
|
|
1183
|
+
});
|
|
1184
|
+
|
|
1185
|
+
const nonProviderCalls = stepResult.toolCalls.filter(
|
|
1186
|
+
(call) => call.providerExecuted !== true,
|
|
1187
|
+
);
|
|
1188
|
+
if (
|
|
1189
|
+
stepResult.finishReason === 'tool-calls' &&
|
|
1190
|
+
nonProviderCalls.length > 0 &&
|
|
1191
|
+
localToolExecution.toolResultMessageParts.length > 0
|
|
1192
|
+
) {
|
|
1193
|
+
const assistantParts: Array<TextContentPart | AssistantToolCallPart> = [];
|
|
1194
|
+
if (stepResult.assistantText.length > 0) {
|
|
1195
|
+
assistantParts.push({
|
|
1196
|
+
type: 'text',
|
|
1197
|
+
text: stepResult.assistantText,
|
|
1198
|
+
});
|
|
1199
|
+
}
|
|
1200
|
+
assistantParts.push(...localToolExecution.assistantToolCallParts);
|
|
1201
|
+
|
|
1202
|
+
conversationMessages = [
|
|
1203
|
+
...conversationMessages,
|
|
1204
|
+
{
|
|
1205
|
+
role: 'assistant',
|
|
1206
|
+
content: assistantParts,
|
|
1207
|
+
},
|
|
1208
|
+
{
|
|
1209
|
+
role: 'tool',
|
|
1210
|
+
content: localToolExecution.toolResultMessageParts,
|
|
1211
|
+
} satisfies ToolModelMessage,
|
|
1212
|
+
];
|
|
1213
|
+
|
|
1214
|
+
continue;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
emit({
|
|
1218
|
+
type: 'finish',
|
|
1219
|
+
finishReason: stepResult.finishReason,
|
|
1220
|
+
...(stepResult.rawFinishReason !== undefined
|
|
1221
|
+
? { rawFinishReason: stepResult.rawFinishReason }
|
|
1222
|
+
: {}),
|
|
1223
|
+
totalUsage,
|
|
1224
|
+
});
|
|
1225
|
+
break;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
controller.close();
|
|
1229
|
+
} catch (error) {
|
|
1230
|
+
if (options.abortSignal?.aborted === true) {
|
|
1231
|
+
emit({ type: 'abort', reason: 'aborted' });
|
|
1232
|
+
emit({ type: 'finish', finishReason: 'error', rawFinishReason: 'aborted', totalUsage });
|
|
1233
|
+
controller.close();
|
|
1234
|
+
return;
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
emit({ type: 'error', error });
|
|
1238
|
+
emit({ type: 'finish', finishReason: 'error', rawFinishReason: 'exception', totalUsage });
|
|
1239
|
+
controller.close();
|
|
1240
|
+
}
|
|
1241
|
+
},
|
|
1242
|
+
});
|
|
1243
|
+
|
|
1244
|
+
const [collectorBranch, branchForConsumers] = baseStream.tee();
|
|
1245
|
+
const [fullBranch, remainingBranch] = branchForConsumers.tee();
|
|
1246
|
+
const [textBranchSource, uiBranchSource] = remainingBranch.tee();
|
|
1247
|
+
const [uiBranchA, uiBranchB] = uiBranchSource.tee();
|
|
1248
|
+
|
|
1249
|
+
const fullStream = toAsyncIterableStream(fullBranch);
|
|
1250
|
+
const textStream = toAsyncIterableStream(
|
|
1251
|
+
textBranchSource.pipeThrough(
|
|
1252
|
+
new TransformStream<StreamTextPart<TOOLS>, string>({
|
|
1253
|
+
transform(part, controller) {
|
|
1254
|
+
if (part.type === 'text-delta' && part.text.length > 0) {
|
|
1255
|
+
controller.enqueue(part.text);
|
|
1256
|
+
}
|
|
1257
|
+
},
|
|
1258
|
+
}),
|
|
1259
|
+
),
|
|
1260
|
+
);
|
|
1261
|
+
|
|
1262
|
+
const uiMessageStream = createUIMessageStream(uiBranchA);
|
|
1263
|
+
const uiResponseStream = createUIMessageStream(uiBranchB);
|
|
1264
|
+
|
|
1265
|
+
const textDeferred = createDeferred<string>();
|
|
1266
|
+
const toolCallsDeferred = createDeferred<TypedToolCall<TOOLS>[]>();
|
|
1267
|
+
const toolResultsDeferred =
|
|
1268
|
+
createDeferred<Array<TypedToolResult<TOOLS> | TypedToolError<TOOLS>>>();
|
|
1269
|
+
const finishReasonDeferred = createDeferred<FinishReason>();
|
|
1270
|
+
const usageDeferred = createDeferred<LanguageModelUsage>();
|
|
1271
|
+
const responseDeferred = createDeferred<LanguageModelResponseMetadata>();
|
|
1272
|
+
|
|
1273
|
+
collectResultFromStream(collectorBranch)
|
|
1274
|
+
.then((collected) => {
|
|
1275
|
+
textDeferred.resolve(collected.text);
|
|
1276
|
+
toolCallsDeferred.resolve(collected.toolCalls);
|
|
1277
|
+
toolResultsDeferred.resolve(collected.toolResults);
|
|
1278
|
+
finishReasonDeferred.resolve(collected.finishReason);
|
|
1279
|
+
usageDeferred.resolve(collected.usage);
|
|
1280
|
+
responseDeferred.resolve(collected.response);
|
|
1281
|
+
})
|
|
1282
|
+
.catch((error) => {
|
|
1283
|
+
textDeferred.reject(error);
|
|
1284
|
+
toolCallsDeferred.reject(error);
|
|
1285
|
+
toolResultsDeferred.reject(error);
|
|
1286
|
+
finishReasonDeferred.reject(error);
|
|
1287
|
+
usageDeferred.reject(error);
|
|
1288
|
+
responseDeferred.reject(error);
|
|
1289
|
+
});
|
|
1290
|
+
|
|
1291
|
+
return {
|
|
1292
|
+
fullStream,
|
|
1293
|
+
textStream,
|
|
1294
|
+
text: textDeferred.promise,
|
|
1295
|
+
toolCalls: toolCallsDeferred.promise,
|
|
1296
|
+
toolResults: toolResultsDeferred.promise,
|
|
1297
|
+
finishReason: finishReasonDeferred.promise,
|
|
1298
|
+
usage: usageDeferred.promise,
|
|
1299
|
+
response: responseDeferred.promise,
|
|
1300
|
+
toUIMessageStream() {
|
|
1301
|
+
return uiMessageStream;
|
|
1302
|
+
},
|
|
1303
|
+
toUIMessageStreamResponse(init?: ResponseInit): Response {
|
|
1304
|
+
return createUIMessageStreamResponse(uiResponseStream, init);
|
|
1305
|
+
},
|
|
1306
|
+
async consumeStream(): Promise<void> {
|
|
1307
|
+
await consumeReadableStream(fullStream);
|
|
1308
|
+
},
|
|
1309
|
+
};
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
export async function generateText<TOOLS extends ToolSet>(
|
|
1313
|
+
options: StreamTextOptions<TOOLS>,
|
|
1314
|
+
): Promise<GenerateTextResult<TOOLS>> {
|
|
1315
|
+
const result = streamText(options);
|
|
1316
|
+
const [text, finishReason, usage, response, toolCalls, toolResults] = await Promise.all([
|
|
1317
|
+
result.text,
|
|
1318
|
+
result.finishReason,
|
|
1319
|
+
result.usage,
|
|
1320
|
+
result.response,
|
|
1321
|
+
result.toolCalls,
|
|
1322
|
+
result.toolResults,
|
|
1323
|
+
]);
|
|
1324
|
+
|
|
1325
|
+
return {
|
|
1326
|
+
text,
|
|
1327
|
+
finishReason,
|
|
1328
|
+
usage,
|
|
1329
|
+
response,
|
|
1330
|
+
toolCalls,
|
|
1331
|
+
toolResults,
|
|
1332
|
+
};
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
export async function collectFullStream<TOOLS extends ToolSet>(
|
|
1336
|
+
options: StreamTextOptions<TOOLS>,
|
|
1337
|
+
): Promise<StreamTextPart<TOOLS>[]> {
|
|
1338
|
+
const result = streamText(options);
|
|
1339
|
+
return collectReadableStream(result.fullStream);
|
|
1340
|
+
}
|