@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
@@ -0,0 +1,415 @@
1
+ /**
2
+ * [api documentation](https://docs.x.ai/docs/overview)
3
+ */
4
+ import { GLOBAL } from '../../fafs.js'
5
+ import { request as doRequest } from '@j-o-r/apiserver';
6
+ import { sleep } from '@j-o-r/sh';
7
+ /**
8
+ * @typedef {import('../../Prompt.js').default} Prompt
9
+ * @typedef {import('../../ToolSet.js').default} ToolSet
10
+ */
11
+ /**
12
+ * @typedef {"grok-4"|"grok-3"|"grok-3-mini"|"grok-3-fast"|"grok-3-mini-fast"} grokModels
13
+ * @typedef {"low"|"high"} grokReason
14
+ */
15
+ /**
16
+ * @typedef {Object} SearchParameters
17
+ * @property {"on"|"auto"} mode
18
+ * @property {string} [from_date] ISO-8601 date (YYYY-MM-DD)
19
+ * @property {string} [to_date] ISO-8601 date (YYYY-MM-DD)
20
+ * @property {(RssSource|WebSource|NewsSource|XSource)[]} [sources]
21
+ *
22
+ * @typedef {Object} RssSource
23
+ * @property {"rss"} type
24
+ * @property {string[]} links
25
+ *
26
+ * @typedef {Object} WebSource
27
+ * @property {"web"} type
28
+ * @property {string} [country] ISO-3166-1 alpha-2
29
+ * @property {boolean} [safe_search=true]
30
+ * @property {string[]} [allowed_websites] // cannot coexist with excluded_websites
31
+ * @property {string[]} [excluded_websites]
32
+ *
33
+ * @typedef {Object} NewsSource
34
+ * @property {"news"} type
35
+ * @property {string} [country]
36
+ * @property {boolean} [safe_search=true]
37
+ * @property {string[]} [allowed_websites]
38
+ * @property {string[]} [excluded_websites]
39
+ *
40
+ * @typedef {Object} XSource
41
+ * @property {"x"} type
42
+ */
43
+ /**
44
+ * @typedef {object} XRequest
45
+ * @property {XOptions} body
46
+ * @property {Headers} headers
47
+ * @property {string} url -
48
+ */
49
+ /**
50
+ * @typedef {Object} XFunctionCall
51
+ * @property {string} name - The name of the function to call.
52
+ * @property {string} arguments - The arguments for the function call in JSON string format.
53
+ * @typedef {Object} XToolCall
54
+ * @property {string} id - The unique identifier for the tool call.
55
+ * @property {string} type - The type of the tool call, e.g., "function".
56
+ * @property {XFunctionCall} function - The function call details.
57
+ */
58
+ /**
59
+ * @typedef {Object} XFunctionResponse
60
+ * @property {string} role - must be 'tool'
61
+ * @property {string} content - The arguments for the function call in JSON string format.
62
+ * @property {string} tool_call_id - The unique identifier for the tool call.
63
+ */
64
+ /**
65
+ * @typedef {Object} XMessageResponse
66
+ * @property {number} index - The index of the message.
67
+ * @property {Object} message - The message details.
68
+ * @property {string} message.role - The role of the sender (e.g., "assistant").
69
+ * @property {string} message.content - The content of the message.
70
+ * @property {string} [message.reasoning_content] - The reasoning behind the response.
71
+ * @property {?string} message.refusal - Any refusal content, if applicable.
72
+ * @property {string} finish_reason - The reason for finishing (e.g., "stop").
73
+ * @property {XToolCall[]} tools_calls - The reason for finishing (e.g., "stop").
74
+ */
75
+
76
+ /**
77
+ * https://docs.x.ai/api/endpoints#chat-completions
78
+ * @typedef {object} XOptions
79
+ * @property {grokModels} [model] - What model to use
80
+ * @property {Array<*>} [messages] - the actual prompt
81
+ * @property {XToolCall[]} [tools], callable functions
82
+ * @property {string} [tool_choice] - Default 'none' when no function, 'auto' when present, or required
83
+ * @property {Object} [response_format] - Response format
84
+ * @property {string} [response_format.type] - The role of the sender (e.g., "assistant").
85
+ * @property {number} [temperature] - What sampling temperature to use, between 0 and 2.
86
+ * @property {number} [max_completion_tokens] - The maximum number of tokens allowed for the generated answer.
87
+ * @property {number} [presence_penalty] - Number between -2.0 and 2.0.
88
+ * @property {number} [frequency_penalty] - Number between -2.0 and 2.0.
89
+ * @property {number} [top_p] - Number between -2.0 and 2.0.
90
+ * @property {number} [n] - How many chat completion choices to generate for each input message. default 1
91
+ * @property {boolean} [stream] - Chunk/stream request default null
92
+ * @property {boolean} [parallel_tool_calls] - Multiple calls at once
93
+ * @property {grokReason} [reasoning_effort] - Who i am
94
+ * @property {string} [user] - Who i am
95
+ * @property {number} [seed] - see options
96
+ * @property {SearchParameters} [search_parameters] - live search
97
+ * @property {string[]} [stop] - Up to 4 sequences where the API will stop generating further tokens.
98
+ */
99
+ const API_URL = 'https://api.x.ai/v1/chat/completions';
100
+ /** @type {XOptions} */
101
+ const defaultSettings = {
102
+ model: 'grok-4',
103
+ messages: [
104
+ {
105
+ role: 'system',
106
+ content: 'Be precise and concise'
107
+ }
108
+ ],
109
+ // temperature: 0.2,
110
+ // response_format: { type: 'text' },
111
+ parallel_tool_calls: true,
112
+ // reasoning_effort: null,
113
+ // max_completion_tokens: 2000,
114
+ // presence_penalty: 0,
115
+ // frequency_penalty: 0,
116
+ // top_p: 1,
117
+ // stop: []
118
+ };
119
+
120
+
121
+ /**
122
+ * Get the default headers
123
+ * @returns {object}
124
+ */
125
+ const getHeaders = () => {
126
+ if (!process.env['XAIKEY']) {
127
+ throw new Error('Missing XAIKEY! export XAIKEY=<XAIKEY>')
128
+ }
129
+ const KEY = process.env['XAIKEY'];
130
+ return {
131
+ 'Content-Type': 'application/json',
132
+ 'Authorization': `Bearer ${KEY}`
133
+ }
134
+ }
135
+ /**
136
+ * Get the text from content
137
+ * @param {import('../../Prompt.js').Content[]} content
138
+ * @returns {string}
139
+ */
140
+ function getTextFromContent(content) {
141
+ let res = '';
142
+ let i = 0;
143
+ const len = content.length;
144
+ for (; i < len; i++) {
145
+ if (content[i].type === 'text') {
146
+ // @ts-ignore
147
+ res = `${res}\n${content[i].text} `
148
+ }
149
+ }
150
+ return res.trim();
151
+ }
152
+ /**
153
+ * Convert messages to XAI
154
+ * @param {Prompt} prompt
155
+ * @returns {Array<*>}
156
+ */
157
+ const generateHistory = (prompt) => {
158
+ const messages = prompt.messages
159
+ const result = [];
160
+ let i = 0;
161
+ const len = messages.length;
162
+ for (; i < len; i++) {
163
+ const msg = messages[i];
164
+ const role = msg.role;
165
+ if (['system', 'assistant', 'user'].includes(role)) {
166
+ // Get the text
167
+ const text = getTextFromContent(msg.content);
168
+ const newMesg = {
169
+ role,
170
+ content: text,
171
+ refusal: null
172
+ };
173
+ // Add initial toolcalls
174
+ const toolCalls = msg.content
175
+ .filter(item => item.type === 'function_request')
176
+ .map(req => ({
177
+ // @ts-ignore
178
+ id: req.function_request.id,
179
+ type: "function",
180
+ function: {
181
+ // @ts-ignore
182
+ arguments: req.function_request.parameters,
183
+ // @ts-ignore
184
+ name: req.function_request.name,
185
+ }
186
+ }));
187
+ if (toolCalls.length > 0) {
188
+ newMesg.tool_calls = toolCalls;
189
+ }
190
+ result.push(newMesg);
191
+ } else if (role === 'tool') {
192
+ const content = msg.content;
193
+ let it = 0;
194
+ const lent = content.length;
195
+ for (; it < lent; it++) {
196
+ /** @type {import('../..//Prompt.js').FunctionResponse} */
197
+ // @ts-ignore
198
+ const item = content[it];
199
+ if (item.type === 'function_response') {
200
+ // Add tool function response
201
+ result.push({
202
+ role: 'tool',
203
+ name: item.function_response.name,
204
+ call_id: item.function_response.call_id,
205
+ content: item.function_response.response
206
+ });
207
+ }
208
+ }
209
+ }
210
+ }
211
+ return result;
212
+ }
213
+
214
+ /**
215
+ * Convert a toolset to something openai understands
216
+ * @param {ToolSet} toolset
217
+ * @returns {XToolCall[]}
218
+ */
219
+ const generateXAIToolCalls = (toolset) => {
220
+ const list = toolset.list();
221
+ /** @type {XToolCall[]} */
222
+ const result = [];
223
+ list.forEach((item) => {
224
+ result.push({
225
+ type: 'function', function: {
226
+ name: item.name,
227
+ // @ts-ignore
228
+ description: item.description,
229
+ parameters: item.parameters
230
+ }
231
+ })
232
+ });
233
+ return result;
234
+ }
235
+
236
+ /**
237
+ * Create an anthropic request
238
+ * @param {Prompt} prompt
239
+ * @param {ToolSet|void} [tools]
240
+ * @param {XOptions} [opts] overwrite default request settings
241
+ * @param {object} [hdrs] - optional headers to pass
242
+ * @returns {XRequest}
243
+ * @throws {Error}
244
+ */
245
+ const generateRequest = (prompt, tools, opts = {}, hdrs = {}) => {
246
+ /** @type {XOptions} */
247
+ const body = { ...defaultSettings, ...opts };
248
+ const headers = { ...getHeaders(), ...hdrs };
249
+ // Sanity check
250
+ // basePromptCheck(prompt);
251
+ // body.messages = generateXAIMessages(prompt) || [];
252
+ body.messages = generateHistory(prompt);
253
+ if (body.messages.length == 0) {
254
+ throw new Error('No messages found');
255
+ }
256
+ body.tools = (tools) ? generateXAIToolCalls(tools) : [];
257
+ body.tool_choice = (tools) ? tools.toolChoice : '';
258
+ if (body.tools.length === 0) {
259
+ delete body.tools;
260
+ delete body.tool_choice;
261
+ }
262
+ return { body, headers, url: API_URL };
263
+ };
264
+
265
+ // RESPONSE PARSER
266
+
267
+ /**
268
+ * @typedef {Object} XResponse
269
+ * @property {string} id - The unique identifier for the response.
270
+ * @property {string} object - The type of object returned, typically "chat.completion".
271
+ * @property {number} created - The timestamp of when the response was created.
272
+ * @property {string} model - The model used to generate the response.
273
+ * @property {Array<XChoice>} choices - An array of choice objects containing the response details.
274
+ * @property {XUsage} usage - An object containing token usage information.
275
+ * @property {string} system_fingerprint - The system fingerprint for the response.
276
+ * @property {Object} http_headers - HTTP headers associated with the response.
277
+ */
278
+ /**
279
+ * @typedef {Object} XChoice
280
+ * @property {number} index - The index of the choice in the response.
281
+ * @property {XMessageResponse} message - The message object containing the role and content.
282
+ * @property {null} logprobs - Log probabilities, typically null.
283
+ * @property {string} finish_reason - The reason why the response finished.
284
+ */
285
+ /**
286
+ * @typedef {Object} XUsage
287
+ * @property {number} prompt_tokens - The number of tokens in the prompt.
288
+ * @property {number} completion_tokens - The number of tokens in the completion.
289
+ * @property {number} total_tokens - The total number of tokens used.
290
+ */
291
+
292
+ const parseMessages = (response, prompt) => {
293
+ /** @type {import('../../Prompt.js').FunctionRequest[]} */
294
+ const function_requests = [];
295
+ const messages = response.choices;
296
+ if (response.citations && response.citations.length > 0) {
297
+ const cits = response.citations.map((item, index) => `${index + 1}. ${item}`).join('\n');
298
+ prompt.add('log', cits);
299
+ }
300
+ let i = 0;
301
+ const len = messages.length;
302
+ for (; i < len; i++) {
303
+ const msg = messages[i];
304
+ if (msg.message.reasoning_content) {
305
+ // Add reasoning content
306
+ // Handy for debugging prompts
307
+ prompt.add('reasoning', msg.message.reasoning_content);
308
+ }
309
+ if (msg.refusal) {
310
+ // Wrong question in relation to policies
311
+ prompt.add('assistant', msg.refusal);
312
+ // console.log(msg);
313
+ continue;
314
+ }
315
+ // Add a text message
316
+ if (
317
+ msg.message.role === 'assistant' &&
318
+ typeof msg.message.content === 'string') {
319
+ prompt.add('assistant', msg.message.content);
320
+ }
321
+ // Get function request
322
+ if (msg.message.tool_calls) {
323
+ for (const toolCall of msg.message.tool_calls) {
324
+ function_requests.push(
325
+ {
326
+ type: 'function_request',
327
+ function_request: {
328
+ name: toolCall.function.name,
329
+ id: toolCall.id,
330
+ call_id: toolCall.id,
331
+ parameters: toolCall.function.arguments
332
+ }
333
+ }
334
+ );
335
+ }
336
+ }
337
+ }
338
+ if (function_requests.length > 0) {
339
+ prompt.addMultiModal('assistant', function_requests);
340
+ }
341
+ }
342
+
343
+ /**
344
+ * Process an openai response
345
+ * @param {number} duration - the time is MS before and after the request
346
+ * @param {import('lib/request.js').FetchResponse} res
347
+ * @param {Prompt} prompt
348
+ * @param {ToolSet} [toolset]
349
+ * @returns {Promise<void>}
350
+ * @throws {Error}
351
+ */
352
+ const parseResponse = async (duration, prompt, res, toolset) => {
353
+ // @todo - Put some effort in the parsing, this is quick and dirty
354
+ if (res.status !== 200) {
355
+ new Error(`${res.status}: res.response`)
356
+ }
357
+ const record = prompt.templateRecord();
358
+ record.model = res.response.model;
359
+ record.id = res.response.id;
360
+ record.environment = 'openai';
361
+ record.isoDate = new Date(res.response.created * 1000).toISOString();
362
+ record.tokensIn = res.response.usage.prompt_tokens;
363
+ record.tokensOut = res.response.usage.completion_tokens;
364
+ record.duration = duration;
365
+ prompt.addRecord(record);
366
+ parseMessages(res.response, prompt);
367
+ if (toolset) {
368
+ // Inspect for function_request and execute if so
369
+ await toolset.execute(prompt);
370
+ }
371
+ }
372
+
373
+ /**
374
+ * Do a request
375
+ * @param {Prompt} prompt
376
+ * @param {ToolSet|null} toolset
377
+ * @param {XOptions} [options]:
378
+ * @param {number} [counter] - leave empty this counts the number of requests when doing recursive request calls
379
+ * @return {Promise<import('../../Session.js').Message>}
380
+ */
381
+ async function request(prompt, toolset = null, options, counter = 0) {
382
+ // Max number of recurusive calls
383
+ const counterMax = GLOBAL.max_recursive_requests;
384
+ const start = new Date().getTime();
385
+ // Generate the request
386
+ const { url, headers, body } = generateRequest(prompt, toolset, options);
387
+ prompt.emit(prompt.EVENTS.http_request, {url, counter, body});
388
+ const res = await doRequest(url, 'POST', headers, body);
389
+ prompt.emit(prompt.EVENTS.http_response, res);
390
+ if (res.status !== 200) {
391
+ throw new Error(`${res.status}: ${JSON.stringify(res.response, null, ' ')}`)
392
+ }
393
+ const duration = new Date().getTime() - start;
394
+ await parseResponse(duration, prompt, res, toolset);
395
+ counter = 1 + counter;
396
+ if (counter >= counterMax) {
397
+ throw new Error('Max number of recursive calls reached');
398
+ }
399
+ const lastMessage = prompt.getLastMessage();
400
+ if (!lastMessage) throw new Error('No message found');
401
+ const role = lastMessage.role;
402
+ if (role === 'tool') {
403
+ // need to do another roundtrip to include the function_responses
404
+ // Go easy on the speed, do not hit limits to soon
405
+ await sleep('30s');
406
+ return request(prompt, toolset, options, counter)
407
+ } else {
408
+ return lastMessage
409
+ }
410
+ }
411
+ export {
412
+ generateRequest,
413
+ parseResponse,
414
+ request
415
+ }
@@ -0,0 +1,197 @@
1
+ /*
2
+ Websocket client for AI sessions
3
+ */
4
+ import { WebSocketClient } from "@j-o-r/apiserver";
5
+
6
+ /**
7
+ * @typedef {import('./API/openai.com/reponses/text.js').request} OARequest
8
+ * @typedef {import('./API/x.ai/text.js').request} XRequest
9
+ * @typedef {import('./API/anthropic.com/text.js').request} ANTHRequest
10
+ *
11
+ * @typedef {import('./API/x.ai/text.js').XOptions} XOptions
12
+ * @typedef {import('./API/openai.com/reponses/text.js').OAOptions} OAOptions
13
+ * @typedef {import('./API/anthropic.com/text.js').ANTHOptions} ANTHOptions
14
+ *
15
+ * @typedef {import('./Prompt.js').default} Prompt
16
+ * @typedef {import('./ToolSet.js').default} ToolSet
17
+ */
18
+ /**
19
+ * @typedef {Object} WSOptions
20
+ * @property {Prompt} prompt - The prompt session
21
+ * @property {ToolSet} [toolset] - The toolset
22
+ * @property {string} description - Custom introduction message.
23
+ * @property {string} name - Logical name: e.g. search, code, os, support.
24
+ * @property {string} [url='ws://127.0.0.1:8000/ws?params=1234']
25
+ * @property {number} [intervalMs=250] - How often to pull one message from the queue.
26
+ */
27
+ /**
28
+ * Websocket Client
29
+ * Wrapper combining a session and a websocket
30
+ */
31
+ class AgentClient {
32
+ /** @type {Prompt} */
33
+ #prompt;
34
+ #description = '';
35
+ #name;
36
+ /** @type {WebSocketClient} */
37
+ #ws;
38
+ #url = 'ws://127.0.0.1:8000/ws';
39
+
40
+ // Message queue and processing flag to ensure one-at-a-time handling
41
+ /** @type {Array<any>} */
42
+ #queue = [];
43
+ #processing = false;
44
+ // Bump this to invalidate in-flight work (hard reset)
45
+ #epoch = 0;
46
+
47
+ // Interval-based scheduler (no tight while-loop)
48
+ #intervalId = null;
49
+ #intervalMs = 2000;
50
+
51
+ /**
52
+ * Creates an instance of CLILoader.
53
+ * @param {WSOptions} options - The options to configure the CLILoader.
54
+ */
55
+ constructor(options) {
56
+ if (options.url) this.#url = options.url;
57
+ this.#prompt = options.prompt;
58
+ this.#description = options.description;
59
+ this.#name = options.name;
60
+ if (typeof options.intervalMs === 'number') this.#intervalMs = Math.max(1, options.intervalMs);
61
+ this.#registerPromptEvents();
62
+ this._start();
63
+ }
64
+ /**
65
+ * Informative.
66
+ * See what is happening
67
+ */
68
+ #registerPromptEvents () {
69
+ const events = Object.keys(this.#prompt.EVENTS);
70
+ events.forEach((evt) => {
71
+ this.#prompt.on(evt, (_msg) => {
72
+ if (evt === 'tool_error') {
73
+ console.log(JSON.stringify(_msg, null, ' '));
74
+ }
75
+ console.log(evt);
76
+ });
77
+ });
78
+ }
79
+ _start() {
80
+ this.#ws = new WebSocketClient(this.#url); // Match server port/path
81
+ this.#ws.onopen = () => {
82
+ // Send my introduction to the server
83
+ this.#ws.send(JSON.stringify({
84
+ action: 'agent_introduction',
85
+ name: this.#name,
86
+ content: this.#description,
87
+ id: new Date().getTime()
88
+ }));
89
+ console.log('INIT:>>')
90
+ console.log(this.#name);
91
+ console.log(this.#description);
92
+ };
93
+ this.#ws.onmessage = (m) => {
94
+ // Enqueue messages; processing happens sequentially
95
+ this.incoming(m);
96
+ };
97
+ this.#ws.onclose = () => {
98
+ console.error('CLOSE: Lost Connection, retry in 5 seconds');
99
+ // Stop ticking while disconnected
100
+ this.#stopInterval();
101
+ setTimeout(() => {
102
+ this._start();
103
+ }, 5000)
104
+ };
105
+ this.#ws.onerror = (e) => {
106
+
107
+ }
108
+ }
109
+ /**
110
+ * Enqueue an incoming websocket message and trigger the processor.
111
+ */
112
+ incoming(e) {
113
+ const message = JSON.parse(e.data);
114
+ // Handle resets immediately: do not enqueue, just clear the queue and reset the prompt
115
+ if (message?.action === "reset") {
116
+ console.log("<RESET:incoming>");
117
+ // Invalidate in-flight work
118
+ this.#epoch++;
119
+ // Empty the queue immediately
120
+ this.#queue = [];
121
+ // Stop the scheduler; any in-flight handler will finish, but no further items will run
122
+ this.#stopInterval();
123
+ // Reset the prompt/session state
124
+ this.#prompt.reset();
125
+ return; // Do not enqueue this message
126
+ }
127
+ this.#queue.push(message);
128
+ // Start interval-based processing (no while-loop)
129
+ this.#startInterval();
130
+ }
131
+
132
+ /**
133
+ * Start the interval that pulls one message at each tick.
134
+ */
135
+ #startInterval() {
136
+ if (this.#intervalId) return;
137
+ this.#intervalId = setInterval(() => {
138
+ // We intentionally do not await here; #processOne guards reentrancy
139
+ void this.#processOne();
140
+ }, this.#intervalMs);
141
+ }
142
+
143
+ /** Stop the interval if it is running. */
144
+ #stopInterval() {
145
+ if (this.#intervalId) {
146
+ clearInterval(this.#intervalId);
147
+ this.#intervalId = null;
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Internal: process at most ONE message. Called on an interval tick.
153
+ */
154
+ async #processOne() {
155
+ if (this.#processing) return;
156
+ const message = this.#queue.shift();
157
+ if (!message) {
158
+ // Nothing to do; stop ticking until a new message arrives
159
+ this.#stopInterval();
160
+ return;
161
+ }
162
+ this.#processing = true;
163
+ try {
164
+ const epochAtStart = this.#epoch;
165
+ await this.#handleMessage(message, epochAtStart);
166
+ } finally {
167
+ this.#processing = false;
168
+ // If the queue drained, we can stop; else the next tick will pick the next one
169
+ if (this.#queue.length === 0) this.#stopInterval();
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Internal: handle a single message
175
+ */
176
+ async #handleMessage(message, epochAtStart) {
177
+ if (message.action === 'agent_query') {
178
+ const id = message.id;
179
+ try {
180
+ console.log('IN:>>\n' + message.content);
181
+ const content = await this.#prompt.call(message.content);
182
+ console.log('OUT:>>\n' + content);
183
+ this.#ws.send(JSON.stringify({ action: 'agent_response', content, id }));
184
+ } catch (err) {
185
+ this.#ws.send(JSON.stringify({ action: 'agent_error', content: String(err?.message || err), id }));
186
+ }
187
+ } else if (message.action === 'reset') {
188
+ console.log('<RESET>');
189
+ // Empty the que;
190
+ this.#queue = [];
191
+ this.#epoch++;
192
+ this.#prompt.reset();
193
+ }
194
+ }
195
+ }
196
+
197
+ export default AgentClient;