@aeye/core 0.1.0
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 +568 -0
- package/dist/agent.d.ts +85 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +81 -0
- package/dist/agent.js.map +1 -0
- package/dist/common.d.ts +161 -0
- package/dist/common.d.ts.map +1 -0
- package/dist/common.js +456 -0
- package/dist/common.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/prompt.d.ts +327 -0
- package/dist/prompt.d.ts.map +1 -0
- package/dist/prompt.js +947 -0
- package/dist/prompt.js.map +1 -0
- package/dist/tool.d.ts +139 -0
- package/dist/tool.d.ts.map +1 -0
- package/dist/tool.js +162 -0
- package/dist/tool.js.map +1 -0
- package/dist/types.d.ts +545 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +54 -0
package/dist/prompt.js
ADDED
|
@@ -0,0 +1,947 @@
|
|
|
1
|
+
import Handlebars from "handlebars";
|
|
2
|
+
import z from 'zod';
|
|
3
|
+
import { accumulateUsage, getChunksFromResponse, getInputTokens, getModel, getOutputTokens, getTotalTokens, resolve, resolveFn, yieldAll } from "./common";
|
|
4
|
+
/**
|
|
5
|
+
* A Prompt component that generates AI responses based on input, context, and available tools.
|
|
6
|
+
* Prompts orchestrate interactions with AI models, handle tool calls, and manage streaming responses.
|
|
7
|
+
*
|
|
8
|
+
* @template TContext - The context type needed for the prompt's operation.
|
|
9
|
+
* @template TMetadata - The metadata type needed during execution/streaming.
|
|
10
|
+
* @template TName - The name of the prompt, typed for inference in parent components.
|
|
11
|
+
* @template TInput - The input type for the prompt.
|
|
12
|
+
* @template TOutput - The output type for the prompt.
|
|
13
|
+
* @template TTools - The tools available to the prompt.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* const summarizer = new Prompt({
|
|
17
|
+
* name: 'summarize',
|
|
18
|
+
* description: 'Summarizes text',
|
|
19
|
+
* content: 'Summarize the following text:\n\n{{text}}',
|
|
20
|
+
* input: (input) => ({ text: input.text }),
|
|
21
|
+
* schema: z.object({ summary: z.string() }),
|
|
22
|
+
* });
|
|
23
|
+
*
|
|
24
|
+
* const result = await summarizer.get({ text: 'Long text here...' });
|
|
25
|
+
*/
|
|
26
|
+
export class Prompt {
|
|
27
|
+
input;
|
|
28
|
+
retool;
|
|
29
|
+
schema;
|
|
30
|
+
config;
|
|
31
|
+
translate;
|
|
32
|
+
content;
|
|
33
|
+
metadata;
|
|
34
|
+
/**
|
|
35
|
+
* Compiles the prompt content template.
|
|
36
|
+
* Automatically appends tool instructions section if tools are available.
|
|
37
|
+
*
|
|
38
|
+
* @param content - The prompt content template string.
|
|
39
|
+
* @param hasTools - Whether tools are available.
|
|
40
|
+
* @returns A compiled Handlebars template function.
|
|
41
|
+
*/
|
|
42
|
+
static compileContent(content, hasTools) {
|
|
43
|
+
let template = content;
|
|
44
|
+
if (hasTools && !template.includes('{{tools}}')) {
|
|
45
|
+
template = template + "\n\n<tools>\n{{tools}}\n</tools>";
|
|
46
|
+
}
|
|
47
|
+
return Handlebars.compile(template);
|
|
48
|
+
}
|
|
49
|
+
constructor(input, retool = resolveFn(input.retool), schema = resolveFn(input.schema), config = resolveFn(input.config), translate = resolveFn(input.input), content = Prompt.compileContent(input.content, !!input.tools?.length), metadata = resolveFn(input.metadataFn)) {
|
|
50
|
+
this.input = input;
|
|
51
|
+
this.retool = retool;
|
|
52
|
+
this.schema = schema;
|
|
53
|
+
this.config = config;
|
|
54
|
+
this.translate = translate;
|
|
55
|
+
this.content = content;
|
|
56
|
+
this.metadata = metadata;
|
|
57
|
+
}
|
|
58
|
+
get kind() {
|
|
59
|
+
return 'prompt';
|
|
60
|
+
}
|
|
61
|
+
get name() {
|
|
62
|
+
return this.input.name;
|
|
63
|
+
}
|
|
64
|
+
get description() {
|
|
65
|
+
return this.input.description;
|
|
66
|
+
}
|
|
67
|
+
get refs() {
|
|
68
|
+
return this.input.tools || [];
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Retrieves the prompt output in various modes.
|
|
72
|
+
*
|
|
73
|
+
* - `result`: Returns the final output only
|
|
74
|
+
* - `tools`: Returns all tool outputs only
|
|
75
|
+
* - `stream`: Streams all prompt events
|
|
76
|
+
* - `streamTools`: Streams only tool output events
|
|
77
|
+
* - `streamContent`: Streams only text content events
|
|
78
|
+
*
|
|
79
|
+
* @param mode - The mode of output to retrieve. Defaults to 'result'.
|
|
80
|
+
* @param input - The input parameters for the prompt.
|
|
81
|
+
* @param ctx - The context for the prompt's operation.
|
|
82
|
+
* @returns The prompt output based on the specified mode.
|
|
83
|
+
* @example
|
|
84
|
+
* // Get final result
|
|
85
|
+
* const result = await prompt.get();
|
|
86
|
+
*
|
|
87
|
+
* // Stream content
|
|
88
|
+
* for await (const chunk of prompt.get('streamContent', { text: 'hello' })) {
|
|
89
|
+
* console.log(chunk);
|
|
90
|
+
* }
|
|
91
|
+
*/
|
|
92
|
+
get(mode = 'result', ...[inputMaybe, contextMaybe]) {
|
|
93
|
+
const prompt = this;
|
|
94
|
+
const input = (inputMaybe || {});
|
|
95
|
+
const ctx = (contextMaybe || {});
|
|
96
|
+
const preferStream = mode.startsWith('stream');
|
|
97
|
+
const toolsOnly = mode === 'tools';
|
|
98
|
+
const stream = ctx.runner
|
|
99
|
+
// @ts-ignore
|
|
100
|
+
? ctx.runner(prompt, input, ctx, (innerCtx, events) => this.stream(input, preferStream, toolsOnly, events, innerCtx))
|
|
101
|
+
: this.stream(input, preferStream, toolsOnly, undefined, ctx);
|
|
102
|
+
switch (mode) {
|
|
103
|
+
case 'result':
|
|
104
|
+
return (async function () {
|
|
105
|
+
for await (const event of stream) {
|
|
106
|
+
if (event.type === 'complete') {
|
|
107
|
+
return event.output;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
})();
|
|
111
|
+
case 'tools':
|
|
112
|
+
return (async function () {
|
|
113
|
+
const tools = [];
|
|
114
|
+
for await (const event of stream) {
|
|
115
|
+
if (event.type === 'toolOutput') {
|
|
116
|
+
tools.push({ tool: event.tool.name, result: event.result });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return tools;
|
|
120
|
+
})();
|
|
121
|
+
case 'stream':
|
|
122
|
+
return (async function* () {
|
|
123
|
+
let output = undefined;
|
|
124
|
+
for await (const event of stream) {
|
|
125
|
+
yield event;
|
|
126
|
+
if (event.type === 'complete') {
|
|
127
|
+
output = event.output;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return output;
|
|
131
|
+
})();
|
|
132
|
+
case 'streamTools':
|
|
133
|
+
return (async function* () {
|
|
134
|
+
let output = undefined;
|
|
135
|
+
for await (const event of stream) {
|
|
136
|
+
if (event.type === 'toolOutput') {
|
|
137
|
+
yield { tool: event.tool.name, result: event.result };
|
|
138
|
+
}
|
|
139
|
+
if (event.type === 'complete') {
|
|
140
|
+
output = event.output;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return output;
|
|
144
|
+
})();
|
|
145
|
+
case 'streamContent':
|
|
146
|
+
return (async function* () {
|
|
147
|
+
let output = undefined;
|
|
148
|
+
for await (const event of stream) {
|
|
149
|
+
if (event.type === 'textPartial') {
|
|
150
|
+
yield event.content;
|
|
151
|
+
}
|
|
152
|
+
if (event.type === 'complete') {
|
|
153
|
+
output = event.output;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return output;
|
|
157
|
+
})();
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Runs the prompt with the given context and input.
|
|
162
|
+
*
|
|
163
|
+
* @param ctx - The context for the prompt's operation.
|
|
164
|
+
* @param input - The input parameters for the prompt.
|
|
165
|
+
* @returns An async generator yielding prompt events and ultimately the final output.
|
|
166
|
+
*/
|
|
167
|
+
run(...[inputMaybe, contextMaybe]) {
|
|
168
|
+
const input = (inputMaybe || {});
|
|
169
|
+
const ctx = (contextMaybe || {});
|
|
170
|
+
const prompt = this;
|
|
171
|
+
return ctx.runner
|
|
172
|
+
// @ts-ignore
|
|
173
|
+
? ctx.runner(prompt, input, ctx, (innerCtx, events) => this.stream(input, true, false, events, innerCtx))
|
|
174
|
+
: this.stream(input, true, false, undefined, ctx);
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Determines if the prompt is applicable in the given context.
|
|
178
|
+
* By default, checks retool, schema, and config functions if provided.
|
|
179
|
+
*
|
|
180
|
+
* @param ctx - The context to check applicability against.
|
|
181
|
+
* @returns Whether the prompt is applicable.
|
|
182
|
+
*/
|
|
183
|
+
async applicable(...[contextMaybe]) {
|
|
184
|
+
const ctx = (contextMaybe || {});
|
|
185
|
+
if (this.input.applicable) {
|
|
186
|
+
return this.input.applicable(ctx);
|
|
187
|
+
}
|
|
188
|
+
if (this.input.retool && await this.retool(undefined, ctx) === false) {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
if (this.input.schema && await this.schema(undefined, ctx) === false) {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
if (this.input.config && await this.config(undefined, ctx) === false) {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Streams the prompt execution, yielding events as they occur.
|
|
201
|
+
* This is the core execution method that handles AI interaction, tool calling, and response parsing.
|
|
202
|
+
*
|
|
203
|
+
* @param input - The input parameters for the prompt.
|
|
204
|
+
* @param preferStream - Whether to prefer streaming execution over batch execution.
|
|
205
|
+
* @param events - Optional event handlers for prompt events.
|
|
206
|
+
* @param ctx - The context for the prompt's operation.
|
|
207
|
+
* @returns An async generator yielding prompt events and ultimately the final output.
|
|
208
|
+
*/
|
|
209
|
+
async *stream(...[inputMaybe, preferStream = true, toolsOnly = false, eventsMaybe, contextMaybe]) {
|
|
210
|
+
const input = (inputMaybe || {});
|
|
211
|
+
const events = (eventsMaybe || {});
|
|
212
|
+
const ctx = (contextMaybe || {});
|
|
213
|
+
const streamer = ctx.stream && preferStream
|
|
214
|
+
? ctx.stream
|
|
215
|
+
: ctx.execute
|
|
216
|
+
? this.streamify(ctx.execute)
|
|
217
|
+
: undefined;
|
|
218
|
+
if (!streamer) {
|
|
219
|
+
throw new Error(`No executor or streamer available in context for prompt ${this.input.name}`);
|
|
220
|
+
}
|
|
221
|
+
const resolved = await this.resolve(ctx, input);
|
|
222
|
+
if (!resolved) {
|
|
223
|
+
return undefined;
|
|
224
|
+
}
|
|
225
|
+
const { config, content, tools, toolObjects, responseFormat, schema } = resolved;
|
|
226
|
+
const toolMode = this.input.toolExecution || 'immediate';
|
|
227
|
+
const toolMap = new Map(toolObjects?.map(({ tool, definition }) => [tool.name, { tool, definition }]) || []);
|
|
228
|
+
const onlyTools = toolsOnly || this.input.toolsOnly;
|
|
229
|
+
const request = {
|
|
230
|
+
name: this.name,
|
|
231
|
+
...config,
|
|
232
|
+
maxTokens: config?.maxTokens ?? ctx.maxOutputTokens,
|
|
233
|
+
messages: [
|
|
234
|
+
{ role: 'system', content },
|
|
235
|
+
],
|
|
236
|
+
tools,
|
|
237
|
+
responseFormat,
|
|
238
|
+
};
|
|
239
|
+
const fixedToolChoice = request.toolChoice && (request.toolChoice === 'required' || typeof request.toolChoice === 'object');
|
|
240
|
+
if (fixedToolChoice && (!tools || tools.length === 0)) {
|
|
241
|
+
throw new Error(`Prompt ${this.input.name} is configured to require tools, but no tools are available.`);
|
|
242
|
+
}
|
|
243
|
+
if (!this.input.excludeMessages && ctx.messages) {
|
|
244
|
+
request.messages = request.messages.concat(ctx.messages);
|
|
245
|
+
// Pre-emptively trim context messages if we have a context window limit
|
|
246
|
+
request.messages = this.forget(request, ctx);
|
|
247
|
+
}
|
|
248
|
+
let outputRetries = this.input.outputRetries ?? ctx.outputRetries ?? 2;
|
|
249
|
+
let forgetRetries = this.input.forgetRetries ?? ctx.forgetRetries ?? 1;
|
|
250
|
+
let toolIterations = this.input.toolIterations ?? 3;
|
|
251
|
+
let toolRetries = this.input.toolRetries ?? ctx.toolRetries ?? 2;
|
|
252
|
+
let result = undefined;
|
|
253
|
+
let lastError = undefined;
|
|
254
|
+
let completeText = '';
|
|
255
|
+
let maxIterations = outputRetries + forgetRetries + toolIterations + toolRetries + 1;
|
|
256
|
+
let requestUsageSent = false;
|
|
257
|
+
let usage = undefined;
|
|
258
|
+
let iterations = 0;
|
|
259
|
+
let accumulatedUsage = {};
|
|
260
|
+
// Track stats for reconfig
|
|
261
|
+
let toolParseErrors = 0;
|
|
262
|
+
let toolCallErrors = 0;
|
|
263
|
+
let toolSuccesses = 0;
|
|
264
|
+
const toolsCalled = new Set();
|
|
265
|
+
// Emit is a helper to optionally emit events and return the value passed in so it can be yielded.
|
|
266
|
+
const emit = events?.onPromptEvent && ctx.instance
|
|
267
|
+
? (ev) => {
|
|
268
|
+
// @ts-ignore
|
|
269
|
+
events.onPromptEvent(ctx.instance, ev);
|
|
270
|
+
return ev;
|
|
271
|
+
}
|
|
272
|
+
: (ev) => ev;
|
|
273
|
+
const emitTool = (ev) => emit(ev);
|
|
274
|
+
// Main execution loop!
|
|
275
|
+
while (iterations < maxIterations) {
|
|
276
|
+
const toolCalls = [];
|
|
277
|
+
const toolCallMap = new Map();
|
|
278
|
+
const toolErrorsPrevious = (toolCallErrors + toolParseErrors);
|
|
279
|
+
const toolParseErrorsPrevious = toolParseErrors;
|
|
280
|
+
let finishReason = undefined;
|
|
281
|
+
let refusal = '';
|
|
282
|
+
let reasoning = '';
|
|
283
|
+
let content = '';
|
|
284
|
+
let disableTools = false;
|
|
285
|
+
const streamController = new AbortController();
|
|
286
|
+
const streamAbort = () => streamController.abort();
|
|
287
|
+
ctx.signal?.addEventListener('abort', streamAbort);
|
|
288
|
+
const metadata = {
|
|
289
|
+
...(this.input.metadata || {}),
|
|
290
|
+
...(await this.metadata(input, ctx) || {}),
|
|
291
|
+
};
|
|
292
|
+
const stream = streamer(request, ctx, metadata, streamController.signal);
|
|
293
|
+
for await (const chunk of stream) {
|
|
294
|
+
if (streamController.signal.aborted) {
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
297
|
+
if (chunk.usage) {
|
|
298
|
+
usage = chunk.usage;
|
|
299
|
+
if (!requestUsageSent) {
|
|
300
|
+
// Calculate input tokens from usage structure
|
|
301
|
+
const inputTokens = getInputTokens(chunk.usage);
|
|
302
|
+
yield emit({ type: 'requestUsage', usage: chunk.usage, request });
|
|
303
|
+
requestUsageSent = true;
|
|
304
|
+
}
|
|
305
|
+
accumulateUsage(accumulatedUsage, chunk.usage);
|
|
306
|
+
}
|
|
307
|
+
if (chunk.content) {
|
|
308
|
+
content += chunk.content;
|
|
309
|
+
yield emit({ type: 'textPartial', content: chunk.content, request });
|
|
310
|
+
}
|
|
311
|
+
if (chunk.refusal) {
|
|
312
|
+
refusal += chunk.refusal;
|
|
313
|
+
yield emit({ type: 'textPartial', content: chunk.refusal, request });
|
|
314
|
+
}
|
|
315
|
+
if (chunk.reasoning) {
|
|
316
|
+
reasoning += chunk.reasoning;
|
|
317
|
+
yield emit({ type: 'reasonPartial', content: chunk.reasoning, request });
|
|
318
|
+
}
|
|
319
|
+
// Handle tool calls
|
|
320
|
+
if (chunk.toolCallNamed) {
|
|
321
|
+
const toolCall = newToolExecution(ctx, chunk.toolCallNamed, toolMap.get(chunk.toolCallNamed.name));
|
|
322
|
+
toolCalls.push(toolCall);
|
|
323
|
+
toolCallMap.set(chunk.toolCallNamed.id, toolCall);
|
|
324
|
+
if (toolCall.tool) {
|
|
325
|
+
yield emit({ type: 'toolParseName', tool: toolCall.tool, request });
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
streamController.abort(toolCall.error);
|
|
329
|
+
break;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
if (chunk.toolCallArguments) {
|
|
333
|
+
const toolCall = toolCallMap.get(chunk.toolCallArguments.id);
|
|
334
|
+
toolCall.toolCall = chunk.toolCallArguments;
|
|
335
|
+
yield emit({ type: 'toolParseArguments', tool: toolCall.tool, args: chunk.toolCallArguments.arguments, request });
|
|
336
|
+
}
|
|
337
|
+
if (chunk.toolCall) {
|
|
338
|
+
const toolCall = toolCallMap.get(chunk.toolCall.id);
|
|
339
|
+
toolCall.toolCall = chunk.toolCall;
|
|
340
|
+
if (toolMode === 'immediate') {
|
|
341
|
+
// Start execution immediately
|
|
342
|
+
setImmediate(toolCall.run);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
if (chunk.finishReason) {
|
|
346
|
+
finishReason = chunk.finishReason;
|
|
347
|
+
}
|
|
348
|
+
// In immediate mode we might be getting more chunks while executing, emit events as soon as possible.
|
|
349
|
+
if (toolMode === 'immediate') {
|
|
350
|
+
for (const toolCall of toolCalls) {
|
|
351
|
+
if (toolCall.emitStart()) {
|
|
352
|
+
yield emitTool({ type: 'toolStart', tool: toolCall.tool, args: toolCall.args, request });
|
|
353
|
+
}
|
|
354
|
+
if (toolCall.emitOutput()) {
|
|
355
|
+
yield emitTool({ type: 'toolOutput', tool: toolCall.tool, args: toolCall.args, result: toolCall.result, request });
|
|
356
|
+
}
|
|
357
|
+
if (toolCall.emitError()) {
|
|
358
|
+
yield emitTool({ type: 'toolError', tool: toolCall.tool, args: toolCall.args, error: toolCall.error, request });
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
ctx.signal?.removeEventListener('abort', streamAbort);
|
|
364
|
+
// If the model reasoned, yield it
|
|
365
|
+
if (reasoning) {
|
|
366
|
+
yield emit({ type: 'reason', content: reasoning, request });
|
|
367
|
+
}
|
|
368
|
+
// If the model refused to answer and stop
|
|
369
|
+
if (finishReason === 'refusal' || refusal) {
|
|
370
|
+
yield emit({ type: 'refusal', content: refusal || 'unspecified', request });
|
|
371
|
+
lastError = refusal || 'Model refused to answer.';
|
|
372
|
+
break;
|
|
373
|
+
}
|
|
374
|
+
// If the model was stopped due to content filtering
|
|
375
|
+
if (finishReason === 'content_filter') {
|
|
376
|
+
yield emit({ type: 'refusal', content: 'Content filtered by AI model', request });
|
|
377
|
+
lastError = 'Model response was filtered due to content policy.';
|
|
378
|
+
break;
|
|
379
|
+
}
|
|
380
|
+
// If we sent too much, forget the past homie
|
|
381
|
+
if (finishReason === 'length') {
|
|
382
|
+
if (usage && forgetRetries > 0) {
|
|
383
|
+
request.messages = this.forget(request, ctx, usage);
|
|
384
|
+
forgetRetries--;
|
|
385
|
+
yield emit({ type: 'textReset', reason: 'length', request });
|
|
386
|
+
// Lets retry immediately
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
else {
|
|
390
|
+
// Stop iteration - we can't trim without usage info
|
|
391
|
+
lastError = 'Model indicated length finish reason but no token usage was provided so context cannot be trimmed.';
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
// Yield text event if content exists before processing tool calls
|
|
396
|
+
if (content.length > 0) {
|
|
397
|
+
yield emit({ type: 'text', content, request });
|
|
398
|
+
}
|
|
399
|
+
// If we need to make some tool calls, lets do it!
|
|
400
|
+
// We might not have a finish_reason if we got a bad tool name.
|
|
401
|
+
if (finishReason === 'tool_calls' || toolCalls.length) {
|
|
402
|
+
// Add the assistant's response with tool calls to the conversation
|
|
403
|
+
request.messages.push({
|
|
404
|
+
role: 'assistant',
|
|
405
|
+
content,
|
|
406
|
+
toolCalls: toolCalls.map(tc => tc.toolCall),
|
|
407
|
+
});
|
|
408
|
+
// If there are any error/invalid - just stop and add their errors and retry
|
|
409
|
+
let skip = false;
|
|
410
|
+
for (const toolCall of toolCalls) {
|
|
411
|
+
if (toolCall.error) {
|
|
412
|
+
skip = true;
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
// Non-blocking call, we don't want to hold up execution here. But if we can emit start or error early below this we will try.
|
|
416
|
+
toolCall.parse();
|
|
417
|
+
}
|
|
418
|
+
if (toolCall.emitStart()) {
|
|
419
|
+
yield emitTool({ type: 'toolStart', tool: toolCall.tool, args: toolCall.args, request });
|
|
420
|
+
}
|
|
421
|
+
if (toolCall.emitError()) {
|
|
422
|
+
yield emitTool({ type: 'toolError', tool: toolCall.tool, args: toolCall.args, error: toolCall.error, request });
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
// The execution mode for this iteration.
|
|
426
|
+
const iterationMode = skip ? 'skip' : toolMode;
|
|
427
|
+
// All tool calls are valid, lets start this!
|
|
428
|
+
switch (iterationMode) {
|
|
429
|
+
case 'sequential':
|
|
430
|
+
for (const toolCall of toolCalls) {
|
|
431
|
+
await toolCall.parse();
|
|
432
|
+
if (toolCall.emitStart()) {
|
|
433
|
+
yield emitTool({ type: 'toolStart', tool: toolCall.tool, args: toolCall.args, request });
|
|
434
|
+
}
|
|
435
|
+
await toolCall.run();
|
|
436
|
+
if (toolCall.emitOutput()) {
|
|
437
|
+
yield emitTool({ type: 'toolOutput', tool: toolCall.tool, args: toolCall.args, result: toolCall.result, request });
|
|
438
|
+
}
|
|
439
|
+
if (toolCall.emitError()) {
|
|
440
|
+
yield emitTool({ type: 'toolError', tool: toolCall.tool, args: toolCall.args, error: toolCall.error, request });
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
break;
|
|
444
|
+
case 'parallel':
|
|
445
|
+
case 'immediate':
|
|
446
|
+
const parseRuns = toolCalls.map(tc => [tc.parse(), tc.run()]).flat();
|
|
447
|
+
for await (const { result: toolCallPromise } of yieldAll(parseRuns)) {
|
|
448
|
+
const toolCall = await toolCallPromise;
|
|
449
|
+
if (toolCall.emitStart()) {
|
|
450
|
+
yield emitTool({ type: 'toolStart', tool: toolCall.tool, args: toolCall.args, request });
|
|
451
|
+
}
|
|
452
|
+
if (toolCall.emitOutput()) {
|
|
453
|
+
yield emitTool({ type: 'toolOutput', tool: toolCall.tool, args: toolCall.args, result: toolCall.result, request });
|
|
454
|
+
}
|
|
455
|
+
if (toolCall.emitError()) {
|
|
456
|
+
yield emitTool({ type: 'toolError', tool: toolCall.tool, args: toolCall.args, error: toolCall.error, request });
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
break;
|
|
460
|
+
}
|
|
461
|
+
for (const toolCall of toolCalls) {
|
|
462
|
+
const content = toolCall.error
|
|
463
|
+
? toolCall.error
|
|
464
|
+
: toolCall.result
|
|
465
|
+
? typeof toolCall.result === 'string'
|
|
466
|
+
? toolCall.result
|
|
467
|
+
: JSON.stringify(toolCall.result)
|
|
468
|
+
: '';
|
|
469
|
+
request.messages.push({
|
|
470
|
+
role: 'tool',
|
|
471
|
+
content,
|
|
472
|
+
toolCallId: toolCall.toolCall.id,
|
|
473
|
+
});
|
|
474
|
+
if (toolCall.status === 'invalid') {
|
|
475
|
+
toolParseErrors++;
|
|
476
|
+
}
|
|
477
|
+
else if (toolCall.status === 'error') {
|
|
478
|
+
toolCallErrors++;
|
|
479
|
+
}
|
|
480
|
+
else if (toolCall.status === 'success') {
|
|
481
|
+
toolSuccesses++;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
if ((toolCallErrors + toolParseErrors) > toolErrorsPrevious) {
|
|
485
|
+
if (toolRetries > 0) {
|
|
486
|
+
toolRetries--;
|
|
487
|
+
}
|
|
488
|
+
else {
|
|
489
|
+
disableTools = true;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
const hadToolErrors = toolParseErrorsPrevious !== toolParseErrors;
|
|
494
|
+
const hitMax = this.input.toolsMax && toolSuccesses >= this.input.toolsMax;
|
|
495
|
+
// If if there are only tool calls wanted...
|
|
496
|
+
if (onlyTools) {
|
|
497
|
+
const successWithoutNewErrors = toolSuccesses > 0 && !hadToolErrors;
|
|
498
|
+
const noTools = toolCalls.length === 0;
|
|
499
|
+
// If we met our max tool calls, or had some successes with no new errors, or there are no more tools to call, end it.
|
|
500
|
+
if (hitMax || successWithoutNewErrors || noTools) {
|
|
501
|
+
// got what we needed!
|
|
502
|
+
lastError = undefined;
|
|
503
|
+
break;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
else {
|
|
507
|
+
// We don't only want tools, but if we had some successes and no new parse errors, remove tool requirement
|
|
508
|
+
if (fixedToolChoice && toolSuccesses > 0 && !hadToolErrors) {
|
|
509
|
+
delete request.toolChoice;
|
|
510
|
+
}
|
|
511
|
+
// If we met our max tool calls, remove the tools from the request
|
|
512
|
+
if (hitMax) {
|
|
513
|
+
// No more tools for you!
|
|
514
|
+
disableTools = true;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
// Accumulate text content from this iteration
|
|
518
|
+
if (content.length > 0) {
|
|
519
|
+
completeText += content;
|
|
520
|
+
}
|
|
521
|
+
// If we are finished, parse the output
|
|
522
|
+
if (finishReason === 'stop') {
|
|
523
|
+
if (!schema || (schema instanceof z.ZodString)) {
|
|
524
|
+
result = content;
|
|
525
|
+
break; // All good!
|
|
526
|
+
}
|
|
527
|
+
else {
|
|
528
|
+
// Grab the JSON part from the content just in case...
|
|
529
|
+
const potentialJSON = content.substring(content.indexOf('{'), content.lastIndexOf('}') + 1);
|
|
530
|
+
let errorMessage = '';
|
|
531
|
+
let resetReason = '';
|
|
532
|
+
try {
|
|
533
|
+
const parsedJSON = JSON.parse(potentialJSON);
|
|
534
|
+
const parsedSafe = schema.safeParse(parsedJSON);
|
|
535
|
+
if (!parsedSafe.success) {
|
|
536
|
+
const issueSummary = parsedSafe.error.issues
|
|
537
|
+
.map(i => `- ${i.path.join('.')}: ${i.message}${['string', 'boolean', 'number'].includes(typeof i.input) ? ` (input: ${i.input})` : ''}`)
|
|
538
|
+
.join('\n');
|
|
539
|
+
errorMessage = `The output was an invalid format:\n${issueSummary}\n\nPlease adhere to the output schema:\n${z.toJSONSchema(schema)}`;
|
|
540
|
+
resetReason = 'schema-parsing';
|
|
541
|
+
}
|
|
542
|
+
else {
|
|
543
|
+
result = parsedSafe.data;
|
|
544
|
+
try {
|
|
545
|
+
await this.input.validate?.(result, ctx);
|
|
546
|
+
}
|
|
547
|
+
catch (validationError) {
|
|
548
|
+
errorMessage = `The output failed validation:\n${validationError.message}`;
|
|
549
|
+
resetReason = 'validation';
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
catch (parseError) {
|
|
554
|
+
errorMessage = `The output was not valid JSON:\n${parseError.message}`;
|
|
555
|
+
resetReason = 'json-parsing';
|
|
556
|
+
}
|
|
557
|
+
if (errorMessage) {
|
|
558
|
+
if (outputRetries > 0) {
|
|
559
|
+
outputRetries--;
|
|
560
|
+
yield emit({ type: 'textReset', reason: resetReason, request });
|
|
561
|
+
request.messages.push({
|
|
562
|
+
role: 'user',
|
|
563
|
+
content: errorMessage,
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
else {
|
|
567
|
+
lastError = errorMessage;
|
|
568
|
+
break; // No more retries left
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
else {
|
|
572
|
+
// A result was successfully parsed and validated!
|
|
573
|
+
lastError = undefined;
|
|
574
|
+
break;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
// Call reconfig if provided
|
|
579
|
+
if (this.input.reconfig) {
|
|
580
|
+
const stats = {
|
|
581
|
+
iteration: iterations,
|
|
582
|
+
maxIterations,
|
|
583
|
+
toolParseErrors,
|
|
584
|
+
toolCallErrors,
|
|
585
|
+
toolSuccesses,
|
|
586
|
+
toolRetries,
|
|
587
|
+
outputRetries,
|
|
588
|
+
forgetRetries,
|
|
589
|
+
tools: Array.from(toolsCalled),
|
|
590
|
+
};
|
|
591
|
+
const reconfigResult = await this.input.reconfig(stats, ctx);
|
|
592
|
+
if (reconfigResult) {
|
|
593
|
+
// Apply custom config if provided
|
|
594
|
+
if (reconfigResult.config) {
|
|
595
|
+
delete reconfigResult.config.messages;
|
|
596
|
+
Object.assign(request, reconfigResult.config);
|
|
597
|
+
}
|
|
598
|
+
// Update maxIterations if provided
|
|
599
|
+
if (reconfigResult.maxIterations !== undefined) {
|
|
600
|
+
if (reconfigResult.maxIterations === 0) {
|
|
601
|
+
// Stop immediately
|
|
602
|
+
break;
|
|
603
|
+
}
|
|
604
|
+
else if (reconfigResult.maxIterations > 0) {
|
|
605
|
+
maxIterations = iterations + reconfigResult.maxIterations;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
if (reconfigResult.outputRetries !== undefined) {
|
|
609
|
+
outputRetries = reconfigResult.outputRetries;
|
|
610
|
+
}
|
|
611
|
+
if (reconfigResult.forgetRetries !== undefined) {
|
|
612
|
+
forgetRetries = reconfigResult.forgetRetries;
|
|
613
|
+
}
|
|
614
|
+
if (reconfigResult.toolRetries !== undefined) {
|
|
615
|
+
toolRetries = reconfigResult.toolRetries;
|
|
616
|
+
if (toolRetries === 0) {
|
|
617
|
+
disableTools = true;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
// Dynamic resolve - re-resolve prompt at end of iteration if enabled
|
|
623
|
+
if (this.input.dynamic) {
|
|
624
|
+
const dynamicResolved = await this.resolve(ctx, input);
|
|
625
|
+
if (dynamicResolved === undefined) {
|
|
626
|
+
// Prompt is no longer compatible with context, end iteration
|
|
627
|
+
break;
|
|
628
|
+
}
|
|
629
|
+
const { config: dynamicConfig, content: dynamicContent, tools: dynamicTools, toolObjects: dynamicToolObjects, responseFormat: dynamicResponseFormat, schema: dynamicSchema } = dynamicResolved;
|
|
630
|
+
// Update request with new resolved state
|
|
631
|
+
Object.assign(request, dynamicConfig);
|
|
632
|
+
request.messages[0].content = dynamicContent;
|
|
633
|
+
request.tools = dynamicTools;
|
|
634
|
+
request.responseFormat = dynamicResponseFormat;
|
|
635
|
+
// Update toolMap with new tool objects
|
|
636
|
+
toolMap.clear();
|
|
637
|
+
if (dynamicToolObjects) {
|
|
638
|
+
for (const { tool, definition } of dynamicToolObjects) {
|
|
639
|
+
toolMap.set(tool.name, { tool, definition });
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
// If we disabled tools because of hitting retry limits or max tool calls desired, remove them!
|
|
644
|
+
if (disableTools) {
|
|
645
|
+
delete request.tools;
|
|
646
|
+
delete request.toolChoice;
|
|
647
|
+
delete request.toolsOneAtATime;
|
|
648
|
+
}
|
|
649
|
+
// Lets go again!
|
|
650
|
+
// We are hungry for valid tool calls and output!
|
|
651
|
+
iterations++;
|
|
652
|
+
}
|
|
653
|
+
yield emit({ type: 'textComplete', content: completeText, request });
|
|
654
|
+
// Yield token usage if available
|
|
655
|
+
const outputTokens = getOutputTokens(usage);
|
|
656
|
+
if (outputTokens > 0) {
|
|
657
|
+
yield emit({ type: 'responseTokens', tokens: outputTokens, request });
|
|
658
|
+
}
|
|
659
|
+
yield emit({ type: 'usage', usage: accumulatedUsage, request });
|
|
660
|
+
// We don't emit complete without a valid result unless toolsOnly is set
|
|
661
|
+
if (result === undefined && !onlyTools) {
|
|
662
|
+
if (!lastError && iterations === maxIterations) {
|
|
663
|
+
lastError = `Maximum iterations (${maxIterations}) reached without a valid response.`;
|
|
664
|
+
}
|
|
665
|
+
if (!lastError) {
|
|
666
|
+
lastError = `Prompt ${this.input.name} failed without a specified error.`;
|
|
667
|
+
}
|
|
668
|
+
throw new Error(`Prompt ${this.input.name} failed: ${lastError}`);
|
|
669
|
+
}
|
|
670
|
+
yield emit({ type: 'complete', output: result, request });
|
|
671
|
+
return result;
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* Prepares the prompt for execution by resolving all configuration, tools, and templates.
|
|
675
|
+
* Returns undefined if the prompt is not compatible with the given context.
|
|
676
|
+
*
|
|
677
|
+
* @param ctx - The context to prepare against.
|
|
678
|
+
* @param input - The input to the prompt.
|
|
679
|
+
* @returns The resolved prompt components or undefined if not compatible.
|
|
680
|
+
*/
|
|
681
|
+
async resolve(ctx, input) {
|
|
682
|
+
// Get config, if false is returned context is not compatible with prompt
|
|
683
|
+
const config = await this.config(input, ctx);
|
|
684
|
+
if (config === false) {
|
|
685
|
+
return undefined;
|
|
686
|
+
}
|
|
687
|
+
// Get prompt response schema, if false is returned context is not compatible with prompt
|
|
688
|
+
const schema = await this.schema(input, ctx);
|
|
689
|
+
if (schema === false) {
|
|
690
|
+
return undefined;
|
|
691
|
+
}
|
|
692
|
+
// Determine if prompt can run based on tool compatibility with the context
|
|
693
|
+
const retooling = await this.retool(input, ctx);
|
|
694
|
+
if (retooling === false) {
|
|
695
|
+
return undefined;
|
|
696
|
+
}
|
|
697
|
+
// Extract tools, their instructions, and schemas.
|
|
698
|
+
const toolNames = this.input.retool && retooling
|
|
699
|
+
? new Set(retooling)
|
|
700
|
+
: new Set(this.input.tools?.map(t => t.name) || []);
|
|
701
|
+
let selectedTools = this.input.tools?.filter(t => toolNames.has(t.name));
|
|
702
|
+
// Check tool applicability
|
|
703
|
+
if (selectedTools) {
|
|
704
|
+
const applicabilityResults = await Promise.all(selectedTools.map(async (tool) => ({
|
|
705
|
+
tool,
|
|
706
|
+
applicable: await tool.applicable(ctx)
|
|
707
|
+
})));
|
|
708
|
+
selectedTools = applicabilityResults
|
|
709
|
+
.filter(r => r.applicable)
|
|
710
|
+
.map(r => r.tool);
|
|
711
|
+
}
|
|
712
|
+
const toolInstructions = selectedTools
|
|
713
|
+
? (await Promise.all(selectedTools.map(t => t.compile(ctx)))).filter(t => !!t)
|
|
714
|
+
: undefined;
|
|
715
|
+
const instructions = toolInstructions
|
|
716
|
+
? toolInstructions.map(t => t[0]).join("\n\n")
|
|
717
|
+
: undefined;
|
|
718
|
+
const tools = toolInstructions
|
|
719
|
+
? toolInstructions.map(t => t[1])
|
|
720
|
+
: undefined;
|
|
721
|
+
// Create toolObjects as array of { tool, definition } pairs
|
|
722
|
+
const toolObjects = selectedTools && toolInstructions
|
|
723
|
+
? selectedTools.map((tool, i) => ({ tool, definition: toolInstructions[i][1] }))
|
|
724
|
+
: [];
|
|
725
|
+
// Compute the input that is fed to the prompt's prompt content
|
|
726
|
+
let contentInput = input;
|
|
727
|
+
const translated = await this.translate(input, ctx);
|
|
728
|
+
if (translated) {
|
|
729
|
+
contentInput = translated;
|
|
730
|
+
}
|
|
731
|
+
contentInput.tools = instructions;
|
|
732
|
+
// Compute content using the compiled template
|
|
733
|
+
const content = this.content(contentInput);
|
|
734
|
+
// Determine response format
|
|
735
|
+
const responseFormat = schema && !(schema instanceof z.ZodString)
|
|
736
|
+
? schema
|
|
737
|
+
: 'text';
|
|
738
|
+
return { config, content, tools, toolObjects, responseFormat, schema };
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Converts a non-streaming executor into a streamer by yielding response parts.
|
|
742
|
+
* This allows uniform handling of streaming and non-streaming AI providers.
|
|
743
|
+
*
|
|
744
|
+
* @param execute - The executor function to convert.
|
|
745
|
+
* @returns A streamer function that yields parts of the executor's response.
|
|
746
|
+
*/
|
|
747
|
+
streamify(execute) {
|
|
748
|
+
return async function* (request, ctx, metadata, signal) {
|
|
749
|
+
const response = await execute(request, ctx, metadata, signal);
|
|
750
|
+
for (const chunk of getChunksFromResponse(response)) {
|
|
751
|
+
yield chunk;
|
|
752
|
+
}
|
|
753
|
+
return response;
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
/**
|
|
757
|
+
* Trims messages from the request to fit within token limits.
|
|
758
|
+
*
|
|
759
|
+
* This is called:
|
|
760
|
+
* - Before a request is made to ensure the prompt fits within the model's context window if it's specified
|
|
761
|
+
* - After a response with a 'length' finish reason to allow retrying with trimmed context
|
|
762
|
+
* - After a provider catches an early context window error and emits amn artificial length event.
|
|
763
|
+
*
|
|
764
|
+
* Scenarios that support trimming:
|
|
765
|
+
* 1. Token usage is provided from a previous request (we can use this to infer token counts)
|
|
766
|
+
* 2. A token estimation function is provided in the context (we can estimate token counts)
|
|
767
|
+
* 3. Messages already have token counts assigned (we can use these directly)
|
|
768
|
+
*
|
|
769
|
+
* @param request - The original request with messages.
|
|
770
|
+
* @param ctx - The context containing message history and token estimation.
|
|
771
|
+
* @param usage - The current token usage.
|
|
772
|
+
* @returns The trimmed array of messages.
|
|
773
|
+
*/
|
|
774
|
+
forget(request, ctx, usage) {
|
|
775
|
+
const model = getModel(request.model);
|
|
776
|
+
// Calculate total tokens from usage structure
|
|
777
|
+
const totalTokens = usage ? getTotalTokens(usage) : undefined;
|
|
778
|
+
const contextWindow = model?.contextWindow ?? ctx.contextWindow ?? totalTokens;
|
|
779
|
+
// We can't forget our past if we don't know the context window
|
|
780
|
+
if (contextWindow === undefined) {
|
|
781
|
+
return request.messages;
|
|
782
|
+
}
|
|
783
|
+
// Calculate max input tokens allowed
|
|
784
|
+
const maxOutput = request.maxTokens ?? ctx.maxOutputTokens ?? 4096; // Default completion buffer
|
|
785
|
+
const maxInput = contextWindow - maxOutput;
|
|
786
|
+
// ctx.messages structure: system -> (user -> assistant)[] -> user? -> assistant.tool_calls -> tool[]
|
|
787
|
+
// If we have any tokens defined, spread them out
|
|
788
|
+
// If we have no tokens defined & estimateUsage, estimate them
|
|
789
|
+
// If we have no tokens defined & no estimateUsage but we have usage.text.input, spread them out
|
|
790
|
+
// If we have no tokens defined & no estimateUsage & no usage.text.input, we can't trim
|
|
791
|
+
let messageTokens = [];
|
|
792
|
+
const totalMessageTokens = request.messages.reduce((sum, t) => sum + (t.tokens || 0), 0);
|
|
793
|
+
if (totalMessageTokens > 0) {
|
|
794
|
+
const chunks = [];
|
|
795
|
+
const chunkTokens = [];
|
|
796
|
+
let currentChunk = [];
|
|
797
|
+
for (let i = request.messages.length - 1; i >= 0; i--) {
|
|
798
|
+
const msg = request.messages[i];
|
|
799
|
+
currentChunk.push(msg);
|
|
800
|
+
if (msg.tokens) {
|
|
801
|
+
chunks.unshift(currentChunk);
|
|
802
|
+
chunkTokens.unshift(msg.tokens);
|
|
803
|
+
currentChunk = [];
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
if (currentChunk.length) {
|
|
807
|
+
chunks[0].unshift(...currentChunk);
|
|
808
|
+
}
|
|
809
|
+
// Distribute tokens across messages in each chunk
|
|
810
|
+
// If we have usage input tokens, we add them to the last chunk (usage.text.input - totalMessageTokens)
|
|
811
|
+
if (usage) {
|
|
812
|
+
const usageInputTokens = getInputTokens(usage);
|
|
813
|
+
if (usageInputTokens > 0) {
|
|
814
|
+
const overage = totalMessageTokens - usageInputTokens;
|
|
815
|
+
if (overage > 0) {
|
|
816
|
+
chunkTokens[chunkTokens.length - 1] += overage;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
messageTokens = chunks.map((c, i) => c.map(() => chunkTokens[i] / c.length)).flat();
|
|
821
|
+
}
|
|
822
|
+
else if (ctx.estimateUsage) {
|
|
823
|
+
for (const msg of request.messages) {
|
|
824
|
+
const msgUsage = ctx.estimateUsage(msg);
|
|
825
|
+
// Calculate total tokens from structured usage
|
|
826
|
+
msg.tokens = getTotalTokens(msgUsage);
|
|
827
|
+
}
|
|
828
|
+
messageTokens = request.messages.map(m => m.tokens);
|
|
829
|
+
}
|
|
830
|
+
else if (usage?.text?.input) {
|
|
831
|
+
const spreadTokens = usage.text.input;
|
|
832
|
+
const perMessage = Math.floor(spreadTokens / request.messages.length);
|
|
833
|
+
messageTokens = request.messages.map(() => perMessage);
|
|
834
|
+
}
|
|
835
|
+
else {
|
|
836
|
+
// we have no way to know token counts, so we can't trim
|
|
837
|
+
return request.messages;
|
|
838
|
+
}
|
|
839
|
+
const totalMessageTokensFinal = messageTokens.reduce((sum, t) => sum + t, 0);
|
|
840
|
+
if (totalMessageTokensFinal <= maxInput) {
|
|
841
|
+
// No trimming needed
|
|
842
|
+
return request.messages;
|
|
843
|
+
}
|
|
844
|
+
const removeTokens = totalMessageTokensFinal - maxInput;
|
|
845
|
+
// Calculate where to start trimming and where to stop
|
|
846
|
+
const messageMinIndex = request.messages.findIndex(m => m.role === 'system') + 1; // inclusive
|
|
847
|
+
let messageMaxIndex = request.messages.findLastIndex(m => m.role === 'user'); // exclusive
|
|
848
|
+
if (messageMaxIndex === -1) {
|
|
849
|
+
messageMaxIndex = request.messages.length;
|
|
850
|
+
}
|
|
851
|
+
const trimmedMessages = request.messages.slice(0, messageMinIndex);
|
|
852
|
+
let removesRemaining = removeTokens;
|
|
853
|
+
let messageIndex = messageMinIndex;
|
|
854
|
+
while (removesRemaining > 0 && messageIndex < messageMaxIndex) {
|
|
855
|
+
const message = request.messages[messageIndex];
|
|
856
|
+
if (message.role === 'system') {
|
|
857
|
+
trimmedMessages.push(message);
|
|
858
|
+
messageIndex++;
|
|
859
|
+
}
|
|
860
|
+
else {
|
|
861
|
+
removesRemaining -= messageTokens[messageIndex] || 0;
|
|
862
|
+
messageIndex++;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
trimmedMessages.push(...request.messages.slice(messageIndex));
|
|
866
|
+
return trimmedMessages;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
function once(fn) {
|
|
870
|
+
let promise;
|
|
871
|
+
return () => {
|
|
872
|
+
if (!promise) {
|
|
873
|
+
promise = fn();
|
|
874
|
+
}
|
|
875
|
+
return promise;
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
function emitter() {
|
|
879
|
+
const emitter = {
|
|
880
|
+
called: false,
|
|
881
|
+
ready: false,
|
|
882
|
+
emit: () => {
|
|
883
|
+
const emit = emitter.ready && !emitter.called;
|
|
884
|
+
if (emit) {
|
|
885
|
+
emitter.called = true;
|
|
886
|
+
}
|
|
887
|
+
return emit;
|
|
888
|
+
},
|
|
889
|
+
};
|
|
890
|
+
return emitter;
|
|
891
|
+
}
|
|
892
|
+
function newToolExecution(ctx, toolCall, toolInfo) {
|
|
893
|
+
const start = emitter();
|
|
894
|
+
const output = emitter();
|
|
895
|
+
const error = emitter();
|
|
896
|
+
if (!toolInfo) {
|
|
897
|
+
error.ready = true;
|
|
898
|
+
}
|
|
899
|
+
const execution = {
|
|
900
|
+
toolCall: toolCall,
|
|
901
|
+
tool: toolInfo?.tool,
|
|
902
|
+
definition: toolInfo?.definition,
|
|
903
|
+
status: toolInfo ? 'ready' : 'error',
|
|
904
|
+
error: toolInfo ? undefined : `Tool not found: ${toolCall.name}`,
|
|
905
|
+
emitStart: start.emit,
|
|
906
|
+
emitOutput: output.emit,
|
|
907
|
+
emitError: error.emit,
|
|
908
|
+
parse: once(async () => {
|
|
909
|
+
// Already ran or failed earlier?
|
|
910
|
+
if (execution.status !== 'ready') {
|
|
911
|
+
return execution;
|
|
912
|
+
}
|
|
913
|
+
try {
|
|
914
|
+
execution.args = await toolInfo.tool.parse(ctx, toolCall.arguments, toolInfo.definition.parameters);
|
|
915
|
+
execution.status = 'parsed';
|
|
916
|
+
start.ready = true;
|
|
917
|
+
}
|
|
918
|
+
catch (e) {
|
|
919
|
+
execution.status = 'invalid';
|
|
920
|
+
execution.error = `Error parsing tool arguments: ${e.message}, args: ${toolCall.arguments}`;
|
|
921
|
+
error.ready = true;
|
|
922
|
+
}
|
|
923
|
+
return execution;
|
|
924
|
+
}),
|
|
925
|
+
run: once(async () => {
|
|
926
|
+
await execution.parse();
|
|
927
|
+
if (execution.status !== 'parsed') {
|
|
928
|
+
return execution;
|
|
929
|
+
}
|
|
930
|
+
try {
|
|
931
|
+
execution.status = 'executing';
|
|
932
|
+
execution.result = await resolve(toolInfo.tool.run(execution.args, ctx));
|
|
933
|
+
execution.status = 'success';
|
|
934
|
+
output.ready = true;
|
|
935
|
+
}
|
|
936
|
+
catch (e) {
|
|
937
|
+
execution.status = 'error';
|
|
938
|
+
execution.error = `Error executing tool: ${e.message}, args: ${JSON.stringify(execution.args)}`;
|
|
939
|
+
error.ready = true;
|
|
940
|
+
}
|
|
941
|
+
return execution;
|
|
942
|
+
}),
|
|
943
|
+
};
|
|
944
|
+
return execution;
|
|
945
|
+
}
|
|
946
|
+
;
|
|
947
|
+
//# sourceMappingURL=prompt.js.map
|