@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/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