@j-o-r/hello-dave 0.0.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.
Files changed (53) hide show
  1. package/LICENSE +73 -0
  2. package/README.md +207 -0
  3. package/bin/hdAsk.js +103 -0
  4. package/bin/hdClear.js +13 -0
  5. package/bin/hdCode.js +110 -0
  6. package/bin/hdConnect.js +230 -0
  7. package/bin/hdInspect.js +28 -0
  8. package/bin/hdNpm.js +114 -0
  9. package/bin/hdPrompt.js +108 -0
  10. package/examples/claude-test.js +89 -0
  11. package/examples/claude.js +143 -0
  12. package/examples/gpt.js +127 -0
  13. package/examples/gpt_code.js +125 -0
  14. package/examples/gpt_note_keeping.js +117 -0
  15. package/examples/grok.js +119 -0
  16. package/examples/grok_code.js +114 -0
  17. package/examples/grok_note_keeping.js +111 -0
  18. package/lib/API/anthropic.com/text.js +402 -0
  19. package/lib/API/brave.com/search.js +239 -0
  20. package/lib/API/openai.com/README.md +1 -0
  21. package/lib/API/openai.com/reponses/MESSAGES.md +69 -0
  22. package/lib/API/openai.com/reponses/text.js +416 -0
  23. package/lib/API/x.ai/text.js +415 -0
  24. package/lib/AgentClient.js +197 -0
  25. package/lib/AgentManager.js +144 -0
  26. package/lib/AgentServer.js +336 -0
  27. package/lib/Cli.js +256 -0
  28. package/lib/Prompt.js +728 -0
  29. package/lib/Session.js +231 -0
  30. package/lib/ToolSet.js +186 -0
  31. package/lib/fafs.js +93 -0
  32. package/lib/genericToolset.js +170 -0
  33. package/lib/index.js +34 -0
  34. package/lib/promptHelpers.js +132 -0
  35. package/lib/testToolset.js +42 -0
  36. package/module.md +189 -0
  37. package/package.json +49 -0
  38. package/types/API/anthropic.com/text.d.ts +207 -0
  39. package/types/API/brave.com/search.d.ts +156 -0
  40. package/types/API/openai.com/reponses/text.d.ts +225 -0
  41. package/types/API/x.ai/text.d.ts +286 -0
  42. package/types/AgentClient.d.ts +70 -0
  43. package/types/AgentManager.d.ts +112 -0
  44. package/types/AgentServer.d.ts +38 -0
  45. package/types/Cli.d.ts +52 -0
  46. package/types/Prompt.d.ts +298 -0
  47. package/types/Session.d.ts +31 -0
  48. package/types/ToolSet.d.ts +95 -0
  49. package/types/fafs.d.ts +47 -0
  50. package/types/genericToolset.d.ts +3 -0
  51. package/types/index.d.ts +23 -0
  52. package/types/promptHelpers.d.ts +1 -0
  53. package/types/testToolset.d.ts +3 -0
package/lib/Prompt.js ADDED
@@ -0,0 +1,728 @@
1
+ /**
2
+ * Create a uniform prompt/message AI format
3
+ * An attempt to make working with messages/conversations easier and interchangeable
4
+ * across AI platforms/LLMs.
5
+ * The initial format/structure is inspired by the OPENAI messages format (but not compatible)
6
+ */
7
+ import tokens from 'gpt-3-encoder';
8
+ import { EventEmitter } from 'node:events';
9
+ import { pruneResolvedToolIOByCallIdExceptLast as pruneResolvedToolIOByCallIdExceptLastHelper } from './promptHelpers.js';
10
+
11
+ /**
12
+ * @typedef {import('./API/openai.com/reponses/text.js').request} OARequest
13
+ * @typedef {import('./API/x.ai/text.js').request} XRequest
14
+ * @typedef {import('./API/anthropic.com/text.js').request} ANTHRequest
15
+ *
16
+ * @typedef {import('./API/x.ai/text.js').XOptions} XOptions
17
+ * @typedef {import('./API/openai.com/reponses/text.js').OAOptions} OAOptions
18
+ * @typedef {import('./API/anthropic.com/text.js').ANTHOptions} ANTHOptions
19
+ *
20
+ * @typedef {import('./ToolSet.js').default} ToolSet
21
+ * @typedef {OARequest|XRequest|ANTHRequest} request - The AI model to use for chat functionality.
22
+ * @typedef {OAOptions|XOptions|ANTHOptions} options - Model options
23
+ */
24
+
25
+ /**
26
+ * @typedef {'system'|'assistant'|'user'|'tool'|'log'|'reasoning'} Roles
27
+ */
28
+ /**
29
+ * @typedef {Object} Record
30
+ * @property {string} id - remote request id or generated requestId
31
+ * @property {string} isoDate - Date in ISO string format
32
+ * @property {number} duration - Execution time in MS of the model / method
33
+ * @property {string} environment - Execution context endpoint_name / server_name / interpreter_name
34
+ * @property {string} model - LLM model, method , funtion name
35
+ * @property {number} tokensIn - Number of tokens to process
36
+ * @property {number} tokensOut - Number of tokens in the response
37
+ */
38
+ /**
39
+ * @typedef {Object} TextMessage
40
+ * @property {string} type="text" - The type of the message, should be "text".
41
+ * @property {string} text - The content of the text message.
42
+ */
43
+ /**
44
+ * @typedef {Object} ImageMessage
45
+ * @property {string} type="image_url" - The type of the message, should be "image_url".
46
+ * @property {string} url - URL or "data:image/png;base64,{base64_image}"
47
+ */
48
+ /**
49
+ * @typedef {Object} AudioMessage
50
+ * @property {string} type="url" - The type of the message, should be "image_url".
51
+ * @property {string} url - URL or "data:image/png;base64,{base64_image}"
52
+ */
53
+ /**
54
+ * @typedef {Object} FunctionRequest
55
+ * The `PRMessagerole.role` must be 'assistant'
56
+ * @property {'function_request'} type - The type of the message, should be "function_request".
57
+ * @property {object} function_request - Function object with properties
58
+ * @property {string} function_request.name - function "name" to call
59
+ * @property {string} function_request.id - The given ID of the function call
60
+ * @property {string} function_request.call_id - function call id
61
+ * @property {string} function_request.parameters - JSON string of the parameters e.g. "{\"unit\":\"celsius\"}"
62
+ */
63
+ /**
64
+ * @typedef {Object} FunctionResponse
65
+ * The `PRMessage.role` must 'tool'
66
+ * @property {'function_response'} type - The type of the message, should be "function_response".
67
+ * @property {object} function_response - Function object with properties
68
+ * @property {string} function_response.name - function "name" called
69
+ * @property {string} function_response.id - The given ID of the function call
70
+ * @property {string} function_response.call_id - the function call id
71
+ * @property {string} function_response.response - JSON string of the reponse e.g. "{\"temperature\":24}"
72
+ */
73
+
74
+ /**
75
+ * @typedef {TextMessage|ImageMessage|AudioMessage|FunctionRequest|FunctionResponse} Content
76
+ */
77
+ /**
78
+ * @typedef {object} Message
79
+ * @property {Roles} role - Message role
80
+ * @property {Content[]} content -
81
+ * @property {string} [name] - optional author name
82
+ * @property {boolean} [sticky] - This message object is part of the base prompt (reuse after reset) and is not truncated
83
+ * @property {number} [ts] - A timestamp in MS for reconstruction truncated prompts (for creating large context embeddings)
84
+ */
85
+ const MESSAGE_ROLES = {
86
+ SYSTEM: 'system', // system prompt
87
+ ASSISTANT: 'assistant', // assistent response
88
+ REASONING: 'reasoning', // assistant reasoning: https://docs.x.ai/docs/guides/reasoning
89
+ USER: 'user', // user input
90
+ TOOL: 'tool', // function calls
91
+ LOG: 'log' // Additional log info
92
+ }
93
+ const MESSAGE_TYPES = {
94
+ TEXT: 'text',
95
+ IMAGE_URL: 'image_url',
96
+ AUDIO_URL: 'audio_url',
97
+ FUNCTION_REQUEST: 'function_request',
98
+ FUNCTION_RESPONSE: 'function_response'
99
+ };
100
+
101
+ const EVENTS = {
102
+ start: 'start', // starts a request
103
+ retrigger: 'retrigger', // retriggers start AFTER an import
104
+ finished: 'finished', // finished requests
105
+ message: 'message', // received a message
106
+ record: 'record', // received a record
107
+ truncated: 'truncated', // Prompt messages are truncated
108
+ reset: 'reset', // Prompt reset
109
+ ready: 'ready', // Call or trigger has finished
110
+ error: 'error', // An error of some kind
111
+ warning: 'warning', // An warning of some kind
112
+ tool_request: 'tool_request', // Start a function call
113
+ tool_response: 'tool_response', // Exit a function call
114
+ tool_error: 'tool_error', // Error in function call
115
+ http_request: 'http_request', // Start a function call
116
+ http_response: 'http_response', // Exit a function call
117
+ }
118
+ /**
119
+ * Converts an array of text messages to a string.
120
+ * @param {Content[]} content - An array of messages.
121
+ * @returns {string} - The concatenated text messages.
122
+ */
123
+ const messagesToString = (content) => {
124
+ let result = '';
125
+ content.forEach((mesg) => {
126
+ if (mesg.type === MESSAGE_TYPES.TEXT) {
127
+ result += mesg[MESSAGE_TYPES.TEXT] + '\n';
128
+ }
129
+ if (mesg.type === MESSAGE_TYPES.IMAGE_URL) {
130
+ result += mesg.url + '\n';
131
+ }
132
+ if (mesg.type === MESSAGE_TYPES.AUDIO_URL) {
133
+ result += mesg.url + '\n';
134
+ }
135
+ if (mesg.type === MESSAGE_TYPES.FUNCTION_REQUEST) {
136
+ result += `function_request: ${JSON.stringify(mesg[MESSAGE_TYPES.FUNCTION_REQUEST])}\n`;
137
+ }
138
+ if (mesg.type === MESSAGE_TYPES.FUNCTION_RESPONSE) {
139
+ result += `function_response: ${JSON.stringify(mesg[MESSAGE_TYPES.FUNCTION_RESPONSE])}\n`;
140
+ }
141
+ });
142
+ return result.trim();
143
+ };
144
+
145
+ /**
146
+ * Check if a record is valid
147
+ * @param {Record} record
148
+ * @throws {Error} If the record is invalid.
149
+ */
150
+ const isValidRecord = (record) => {
151
+ if (
152
+ typeof record['id'] === 'string' &&
153
+ record.id.length > 0 &&
154
+ typeof record['isoDate'] === 'string' &&
155
+ record.isoDate.length > 9 &&
156
+ typeof record['environment'] === 'string' &&
157
+ record.environment.length > 0 &&
158
+ typeof record['model'] === 'string' &&
159
+ record.model.length > 0 &&
160
+ typeof record['duration'] === 'number' &&
161
+ typeof record['tokensIn'] === 'number' &&
162
+ typeof record['tokensOut'] === 'number'
163
+ ) {
164
+ return;
165
+ }
166
+ throw new Error('Invalid record');
167
+ }
168
+
169
+ /**
170
+ * Checks if a message is valid based on its role and type.
171
+ * @param {Roles} role - The role of the message (e.g., 'system', 'assistant', 'user', 'tool').
172
+ * @param {Content} msg - The message object.
173
+ * @throws {Error} If the message is invalid.
174
+ */
175
+ const isValidMessage = (role, msg) => {
176
+ if (!Object.values(MESSAGE_ROLES).includes(role)) {
177
+ throw new Error('Unkown role detected');
178
+ }
179
+ switch (msg.type) {
180
+ case MESSAGE_TYPES.TEXT:
181
+ if (!isValidTextMessage(msg)) throw new Error('Invalid text message');
182
+ if (role === 'tool') throw new Error('Invalid role for text message: role tool is not valid');
183
+ break;
184
+ case MESSAGE_TYPES.IMAGE_URL:
185
+ if (!isValidUrlMessage(msg)) throw new Error('Invalid image_url message');
186
+ if (role !== 'user') throw new Error('Invalid role for image_url: use user');
187
+ break;
188
+ case MESSAGE_TYPES.AUDIO_URL:
189
+ if (!isValidUrlMessage(msg)) throw new Error('Invalid audio_url message');
190
+ if (role !== 'user') throw new Error('Invalid role for audio_url: use user');
191
+ break;
192
+ case MESSAGE_TYPES.FUNCTION_REQUEST:
193
+ if (!isValidFunctionRequest(msg)) throw new Error('Invalid function_request message');
194
+ if (role !== 'assistant') throw new Error('Invalid role for function_request: use assistant');
195
+ break;
196
+ case MESSAGE_TYPES.FUNCTION_RESPONSE:
197
+ if (!isValidFunctionResponse(msg)) throw new Error('Invalid function_reponse message');
198
+ if (role !== 'tool') throw new Error('Invalid role for function_response: use tool');
199
+ break;
200
+ default:
201
+ throw new Error('Invalid message type detected');
202
+ }
203
+ }
204
+
205
+ /**
206
+ * Checks if a URL or data URL is valid.
207
+ * @param {string} str - The URL or data URL string.
208
+ * @returns {boolean} - True if the URL or data URL is valid, false otherwise.
209
+ */
210
+ const isValidUrlOrDataUrl = (str) => {
211
+ try {
212
+ // Check if it's a valid URL
213
+ new URL(str);
214
+ return true;
215
+ } catch (_) {
216
+ // Check if it's a valid data URL
217
+ const dataUrlPattern = /^data:[a-z]+\/[a-z0-9-+.]+;base64,[a-z0-9+/]+={0,2}$/i;
218
+ return dataUrlPattern.test(str);
219
+ }
220
+ }
221
+ /**
222
+ * Checks if this a text message
223
+ * However, the text may be empty, this can happen with empty assistant reponses
224
+ * @param {Content} msg - The object to check.
225
+ * @returns {boolean} - True if valid, false otherwise.
226
+ */
227
+ const isValidTextMessage = (msg) => {
228
+ if (
229
+ MESSAGE_TYPES.TEXT === msg['type'] &&
230
+ typeof msg['text'] === 'string'
231
+ ) {
232
+ return true
233
+ }
234
+ return false
235
+ }
236
+
237
+ /**
238
+ * Checks if this a img_url message
239
+ * @param {Content} msg - The object to check.
240
+ * @returns {boolean} - True if valid, false otherwise.
241
+ */
242
+ const isValidUrlMessage = (msg) => {
243
+ if (
244
+ [MESSAGE_TYPES.IMAGE_URL, MESSAGE_TYPES.AUDIO_URL].includes(msg['type']) &&
245
+ isValidUrlOrDataUrl(msg['url'])
246
+ ) {
247
+ return true
248
+ }
249
+ return false
250
+ }
251
+
252
+ /**
253
+ * Checks if this a function_request message
254
+ * @param {Content} msg - The object to check.
255
+ * @returns {boolean} - True if valid, false otherwise.
256
+ */
257
+ const isValidFunctionRequest = (msg) => {
258
+ if (
259
+ MESSAGE_TYPES.FUNCTION_REQUEST === msg['type'] &&
260
+ typeof msg['function_request'] === 'object' &&
261
+ typeof msg['function_request']['name'] === 'string' &&
262
+ typeof msg['function_request']['id'] === 'string' &&
263
+ typeof msg['function_request']['parameters'] === 'string' &&
264
+ msg['function_request']['name'].trim().length > 0 &&
265
+ msg['function_request']['id'].trim().length > 0
266
+ ) {
267
+ return true
268
+ }
269
+ return false
270
+ }
271
+
272
+ /**
273
+ * Checks if this a function_response message
274
+ * @param {Content} msg - The object to check.
275
+ * @returns {boolean} - True if valid, false otherwise.
276
+ */
277
+ const isValidFunctionResponse = (msg) => {
278
+ if (
279
+ MESSAGE_TYPES.FUNCTION_RESPONSE === msg['type'] &&
280
+ typeof msg['function_response'] === 'object' &&
281
+ typeof msg['function_response']['name'] === 'string' &&
282
+ typeof msg['function_response']['id'] === 'string' &&
283
+ typeof msg['function_response']['response'] === 'string' &&
284
+ msg['function_response']['name'].trim().length > 0 &&
285
+ msg['function_response']['id'].trim().length > 0
286
+ ) {
287
+ return true
288
+ }
289
+ return false
290
+ }
291
+ /**
292
+ *
293
+ * @fires Prompt#add
294
+ * Event: message added
295
+ *
296
+ * @event Prompt#add
297
+ * @type {Message}
298
+ */
299
+ class Prompt extends EventEmitter {
300
+ #tokenCount = 0;
301
+ /** Max prompt size in tokens to post */
302
+ #contextWindow = 0;
303
+ /** @type {Array<Message>} */
304
+ #messages = [];
305
+ #adapter = {
306
+ /** @type {request} */
307
+ request: async () => {
308
+ throw new Error('Adapapter not set');
309
+ },
310
+ /** @type {ToolSet|void} */
311
+ toolset: null,
312
+ /** @type {options|void} */
313
+ options: null
314
+ }
315
+ /**
316
+ * push a message to the messages array
317
+ * @param {Message} msg
318
+ * @fires {Prompt#message}
319
+ */
320
+ #add(msg) {
321
+ msg.ts = new Date().getTime();
322
+ this.#messages.push(msg);
323
+ this.emit(EVENTS.message, msg);
324
+ }
325
+
326
+ /**
327
+ * Basic check for the order of messages.
328
+ * @param {Roles} role - The role of the message.
329
+ * @param {boolean} [sticky] - Whether the message is part of the base prompt.
330
+ * @throws {Error} If the message order is invalid.
331
+ */
332
+ #basicMessageOrderCheck(role, sticky) {
333
+ if (!Object.values(MESSAGE_ROLES).includes(role)) {
334
+ throw new Error('Unknown role detected');
335
+ }
336
+ if (role === MESSAGE_ROLES.SYSTEM && !sticky) {
337
+ throw new Error('System prompt needs to be sticky');
338
+ }
339
+ if (role === MESSAGE_ROLES.SYSTEM && this.length > 0) {
340
+ throw new Error('System prompt needs to be the first message');
341
+ }
342
+ if (this.length > 0 && sticky) {
343
+ if (this.#messages[this.length - 1].sticky === false) {
344
+ throw new Error('Sticky messages can only be added after other sticky messages');
345
+ }
346
+ }
347
+ }
348
+ /**
349
+ * Constructs a new Prompt instance.
350
+ * If contextWindow = 0 (defaut) the prompt will have no context building up (ONE_SHOT)
351
+ * @param {number} [contextWindow] - The max size of a total prompt in tokens. (context)
352
+ */
353
+ constructor(contextWindow) {
354
+ super();
355
+ if (typeof contextWindow === 'number') {
356
+ this.#contextWindow = contextWindow;
357
+ }
358
+ this.EVENTS = Object.freeze(EVENTS);
359
+ }
360
+ /**
361
+ * @param {request} handler
362
+ * @param {ToolSet} toolset
363
+ * @param {options} options
364
+ */
365
+ setAdaptor(handler, toolset, options) {
366
+ this.#adapter.request = handler
367
+ this.#adapter.toolset = toolset
368
+ this.#adapter.options = options
369
+ }
370
+ /**
371
+ * Get the names of the available funtion calls
372
+ * @returns {string[]}
373
+ */
374
+ toolsetFunctions() {
375
+ if (!this.#adapter.toolset) return [];
376
+ return this.#adapter.toolset.list().map(t => t.name);
377
+ }
378
+ /**
379
+ * @returns {ToolSet|void}
380
+ */
381
+ get toolset() {
382
+ return this.#adapter.toolset;
383
+ }
384
+
385
+ /**
386
+ * Start a requests
387
+ * @param {string|Content} content
388
+ * @returns {Promise<string>}
389
+ */
390
+ async call(content) {
391
+ if (typeof content === 'string') {
392
+ if (content.trim() === '') {
393
+ throw 'Input is empty';
394
+ }
395
+ this.add('user', content, false);
396
+ } else if (Array.isArray(content)) {
397
+ this.addMultiModal('user', content)
398
+ } else {
399
+ throw 'Not a valid input';
400
+ }
401
+ this.emit(EVENTS.start, true);
402
+ // @ts-ignore
403
+ const msg = await this.#adapter.request(this, this.#adapter.toolset, this.#adapter.options);
404
+ if (this.#tokenCount >= this.#contextWindow) {
405
+ this.truncate();
406
+ }
407
+ this.emit(EVENTS.finished, true);
408
+ return this.contentToString(msg.content)
409
+ }
410
+ /**
411
+ * Trigger a request when the last added message is a user or tool message
412
+ * Exit on error
413
+ */
414
+ async triggerRequest() {
415
+ if (this.#adapter.toolset) {
416
+ // See if we need to execute something first
417
+ await this.#adapter.toolset.execute(this);
418
+ }
419
+ const last = this.getLastMessage();
420
+ if (!last) return;
421
+ if (last.role === 'tool' || last.role === 'user') {
422
+ this.emit(EVENTS.retrigger, true);
423
+ // @ts-ignore
424
+ await this.#adapter.request(this, this.#adapter.toolset, this.#adapter.options)
425
+ }
426
+ if (this.#tokenCount >= this.#contextWindow) {
427
+ this.truncate();
428
+ }
429
+ this.emit(EVENTS.finished, true);
430
+ }
431
+ /**
432
+ * Get the maximum token length for this prompt
433
+ * @returns {number}
434
+ */
435
+ get contextLength() {
436
+ return this.#contextWindow;
437
+ }
438
+ /**
439
+ * Returns a copy of the messages array.
440
+ * @returns {Message[]} - A copy of the messages array.
441
+ */
442
+ get messages() {
443
+ return JSON.parse(JSON.stringify(this.#messages));
444
+ }
445
+
446
+ /**
447
+ * Imports messages.
448
+ * Bypasses the event emitter
449
+ * @param {Message[]} messages
450
+ * @throws {Error} If the input string is invalid.
451
+ */
452
+ set messages(messages) {
453
+ // Copy
454
+ messages = JSON.parse(JSON.stringify(messages));
455
+ messages.forEach((ob) => {
456
+ if (typeof ob.sticky !== 'boolean') {
457
+ throw new Error(`Missing 'sticky' property`);
458
+ }
459
+ if (typeof ob.role !== 'string') {
460
+ throw new Error(`Missing 'role' property`);
461
+ }
462
+ if (Array.isArray(ob.content) === false) {
463
+ throw new Error(`Missing 'content' property`);
464
+ }
465
+ const role = ob.role;
466
+ const list = ob.content;
467
+ list.forEach((msg) => {
468
+ isValidMessage(role, msg);
469
+ });
470
+ });
471
+ // Refresh token count
472
+ this.#tokenCount = 0;
473
+ this.#messages = messages;
474
+ }
475
+ /**
476
+ * Return the most recent, the LAST message added
477
+ * @returns {Message|void}
478
+ */
479
+ getLastMessage() {
480
+ if (this.#messages.length > 0) {
481
+ return this.#messages[this.length - 1];
482
+ }
483
+ }
484
+ /**
485
+ * Return the readable content (type == text)
486
+ * @param {Content[]} content
487
+ * @return {string}
488
+ */
489
+ contentToString(content) {
490
+ return messagesToString(content);
491
+ }
492
+ /**
493
+ * Count the number of tokens on a given string
494
+ * Or the recorded tokencount of the last roundtrip
495
+ * @param {string} [str] - the string to count
496
+ * @param returns {number}
497
+ */
498
+ countTokens(str) {
499
+ if (str && str !== '') {
500
+ return tokens.encode(str).length;
501
+ }
502
+ return this.#tokenCount;
503
+ }
504
+
505
+
506
+ /**
507
+ * reduce the prompt length to fit in the contextWindow
508
+ * or simply reset when there is no contextWindow
509
+ * @returns {boolean} true when the prompt has been altered/reduced
510
+ */
511
+ truncate() {
512
+ const length = this.length;
513
+ if (length === 0) {
514
+ return false;
515
+ }
516
+ // just reset when there is no room for context
517
+ if (this.#contextWindow === 0) {
518
+ this.reset();
519
+ this.emit(EVENTS.truncated, EVENTS.truncated)
520
+ return (length !== this.length);
521
+ }
522
+ pruneResolvedToolIOByCallIdExceptLastHelper(this.#messages, 'lastTurn');
523
+ let totalTokens = 0;
524
+ let messagesToRemove = [];
525
+ let block = false;
526
+ let blockScope = [];
527
+ let blockTokens = 0;
528
+
529
+ for (let i = 0; i < this.#messages.length; i++) {
530
+ const message = this.#messages[i];
531
+ if (message.sticky) {
532
+ totalTokens += this.countTokens(messagesToString(message.content));
533
+ } else {
534
+ break;
535
+ }
536
+ }
537
+ // Count backwards and start deleting when a block takes to much space
538
+ for (let i = this.#messages.length - 1; i >= 0; i--) {
539
+ const message = this.#messages[i];
540
+ if (!message.sticky) {
541
+ // a block starts with a user message
542
+ // ends with an assistant message
543
+ // in reverse
544
+ if (message.role === MESSAGE_ROLES.ASSISTANT) {
545
+ // start record
546
+ block = true;
547
+ } else if (message.role === MESSAGE_ROLES.USER) {
548
+ // end record
549
+ // finish it of
550
+ if (block === true) {
551
+ blockScope.push(i);
552
+ blockTokens += this.countTokens(messagesToString(message.content));
553
+ totalTokens += blockTokens;
554
+ if (totalTokens > this.#contextWindow) {
555
+ messagesToRemove = [...messagesToRemove, ...blockScope];
556
+ };
557
+ blockTokens = 0;
558
+ blockScope = [];
559
+ }
560
+ block = false;
561
+ }
562
+ if (block) {
563
+ blockScope.push(i);
564
+ blockTokens += this.countTokens(messagesToString(message.content));
565
+ }
566
+ }
567
+ }
568
+ messagesToRemove.forEach((index) => {
569
+ this.#messages.splice(index, 1);
570
+ });
571
+ if (length !== this.length) this.emit(EVENTS.truncated, EVENTS.truncated);
572
+ return (length !== this.length);
573
+ }
574
+ /**
575
+ * Gets the length of the messages array.
576
+ * @returns {number} - The length of the messages array.
577
+ */
578
+ get length() {
579
+ return this.#messages.length;
580
+ }
581
+
582
+ /**
583
+ * Gets the system prompt from the messages.
584
+ * @returns {string} - The system prompt.
585
+ * @throws {Error} If no system prompt is available.
586
+ */
587
+ get system_prompt() {
588
+ if (!this.hasSystemprompt) {
589
+ throw new Error('No system prompt available');
590
+ }
591
+ const sys = this.#messages[0];
592
+ return messagesToString(sys.content);
593
+ }
594
+
595
+ /**
596
+ * Checks if a system prompt is available.
597
+ * @returns {boolean} - True if a system prompt is available, false otherwise.
598
+ */
599
+ get hasSystemprompt() {
600
+ if (this.#messages.length === 0) {
601
+ return false;
602
+ }
603
+ if (this.#messages[0].role === MESSAGE_ROLES.SYSTEM) {
604
+ const test = messagesToString(this.#messages[0].content);
605
+ if (test.trim().length > 0) {
606
+ return true;
607
+ }
608
+ }
609
+ return false;
610
+ }
611
+
612
+ /**
613
+ * Adds a text message to the messages array.
614
+ * @param {Roles} role - The role of the message.
615
+ * @param {string} message - The text message.
616
+ * @param {boolean} [sticky] - Whether the message is part of the base prompt.
617
+ * @fires Prompt#message
618
+ * @throws {Error} If the message order is invalid or the message is invalid.
619
+ */
620
+ add(role, message, sticky = false) {
621
+ const content = [];
622
+ const txt = {
623
+ type: 'text',
624
+ text: message
625
+ };
626
+ content.push(txt);
627
+ /**
628
+ * @event Prompt#message
629
+ * @type {Message}
630
+ */
631
+ this.addMultiModal(role, content, sticky);
632
+ }
633
+
634
+ /**
635
+ * Adds a multi-modal message to the messages array.
636
+ * @param {Roles} role - The role of the message.
637
+ * @param {Content[]} messages - The actual messages with multi-modal content.
638
+ * @param {boolean} [sticky] - Whether the message is part of the base prompt.
639
+ * @fires Prompt#add
640
+ * @throws {Error} If the message order is invalid or the messages are invalid.
641
+ */
642
+ addMultiModal(role, messages, sticky = false) {
643
+ this.#basicMessageOrderCheck(role, sticky);
644
+ const content = [];
645
+ messages.forEach((msg) => {
646
+ isValidMessage(role, msg);
647
+ content.push(msg);
648
+ });
649
+ if (content.length === 0) {
650
+ throw new Error('No messages found');
651
+ }
652
+ /**
653
+ * @event Prompt#add
654
+ * @type {Message}
655
+ */
656
+ this.#add({ role, content, sticky });
657
+ }
658
+
659
+ /**
660
+ * Removes all non-sticky messages and returns them
661
+ * Keeps the 'base' (sticky) prompt/messages.
662
+ * @returns {Message[]}
663
+ */
664
+ reset() {
665
+ this.#tokenCount = 0;
666
+ const systemArray = this.#messages.filter(item => item.sticky === true);
667
+ const conversationArray = this.#messages.filter(item => item.sticky === false);
668
+ this.#messages = systemArray;
669
+ this.emit(EVENTS.reset, EVENTS.reset);
670
+ return conversationArray;
671
+ }
672
+
673
+ /**
674
+ * Register a record from the adapter
675
+ * @param {Record} record - Records for billing and optimizing
676
+ * @throws {Error} If the record is invalid.
677
+ */
678
+ addRecord(record) {
679
+ isValidRecord(record);
680
+ this.#tokenCount = record.tokensIn + record.tokensOut
681
+ this.emit(EVENTS.record, record);
682
+ if (this.#tokenCount >= this.#contextWindow) {
683
+ this.emit(EVENTS.warning, `The number of tokens: ${this.#tokenCount} is exceeding the contextWindow: ${this.#contextWindow}`);
684
+ // Need to truncate
685
+ // 1. delete all satisfied function requests
686
+ // 2. run truncate
687
+ }
688
+ }
689
+
690
+ /**
691
+ * Add a list of records (with multiple prompts to maintain a full history of events)
692
+ * @param {Record[]} records - Records for billing and optimizing
693
+ * @throws {Error} If the record is invalid.
694
+ */
695
+ addRecords(records) {
696
+ records.forEach((record) => {
697
+ this.addRecord(record)
698
+ })
699
+ }
700
+ /**
701
+ * Template record
702
+ * @returns {Record}
703
+ */
704
+ templateRecord() {
705
+ const record = {
706
+ id: '',
707
+ isoDate: '',
708
+ duration: 0,
709
+ model: '',
710
+ environment: '',
711
+ tokensIn: 0,
712
+ tokensOut: 0
713
+ }
714
+ return record
715
+ }
716
+ /**
717
+ * Gather metrics as info
718
+ */
719
+ info() {
720
+ const cwd = process.cwd();
721
+ const tools = this.toolsetFunctions().join(', ');
722
+ const context = this.contextLength;
723
+ const tokens = this.countTokens();
724
+ return `Cwd: ${cwd}\nTools: ${tools}\nMax context: ${context}\nUsed Tokens: ${tokens}`
725
+ }
726
+
727
+ }
728
+ export default Prompt;