symposium 2.4.3 → 3.0.1

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/Agent.js CHANGED
@@ -1,26 +1,76 @@
1
1
  import {v7 as uuid} from 'uuid';
2
2
 
3
- import BufferedEventEmitter from "./BufferedEventEmitter.js";
4
-
5
3
  import Symposium from "./Symposium.js";
6
4
  import Thread from "./Thread.js";
7
- import Tool from "./Tool.js";
5
+ import Toolkit from "./Toolkit.js";
8
6
  import Context from "./Context.js";
9
7
  import Text from "./Contexts/Text.js";
10
- import GetContextTool from "./GetContextTool.js";
8
+ import GetContextToolkit from "./GetContextToolkit.js";
9
+ import MCPServer from "./MCPServer.js";
10
+ import MCPResource from "./Contexts/MCPResource.js";
11
+
12
+ const CONTROL_TYPES = new Set(['submit', 'cancel', 'auth']);
13
+
14
+ function isControlMessage(item) {
15
+ if (!item || typeof item !== 'object' || Array.isArray(item))
16
+ return false;
17
+ if (typeof item.type !== 'string')
18
+ return false;
19
+ return CONTROL_TYPES.has(item.type);
20
+ }
21
+
22
+ function isAsyncIterableInput(content) {
23
+ return content !== null
24
+ && typeof content === 'object'
25
+ && !Array.isArray(content)
26
+ && typeof content[Symbol.asyncIterator] === 'function';
27
+ }
28
+
29
+ function normalizeStreamBuffer(items) {
30
+ const blocks = [];
31
+ for (const item of items) {
32
+ if (typeof item === 'string')
33
+ blocks.push({type: 'text', content: item});
34
+ else if (Array.isArray(item))
35
+ blocks.push(...item);
36
+ else if (item && typeof item === 'object')
37
+ blocks.push(item);
38
+ }
39
+ return blocks;
40
+ }
41
+
42
+ function makeNotifier() {
43
+ let resolve;
44
+ let promise = new Promise(r => { resolve = r; });
45
+ return {
46
+ wait() { return promise; },
47
+ signal() {
48
+ const r = resolve;
49
+ promise = new Promise(res => { resolve = res; });
50
+ r();
51
+ },
52
+ };
53
+ }
54
+
55
+ async function isPromiseReady(p) {
56
+ return Promise.race([
57
+ p.then(() => true, () => true),
58
+ new Promise(r => setImmediate(() => r(false))),
59
+ ]);
60
+ }
11
61
 
12
62
  export default class Agent {
13
63
  name = 'Agent';
14
64
  description = null;
15
65
  options = {};
16
66
  threads;
17
- functions = null;
18
- tools = new Map();
67
+ tools = null;
68
+ toolkits = new Map();
19
69
  context = [];
20
70
  default_model = 'gpt-4o';
21
71
  max_retries = 5;
22
72
  type = 'chat'; // chat, utility
23
- utility = null;
73
+ response_schema = null; // raw JSON schema; when set, final assistant message is parsed against it
24
74
  initialized = false;
25
75
  enable_image_generation = false;
26
76
 
@@ -31,6 +81,7 @@ export default class Agent {
31
81
  };
32
82
 
33
83
  this.threads = new Map();
84
+ this._streamingInputs = new Map();
34
85
  }
35
86
 
36
87
  async init() {
@@ -40,11 +91,11 @@ export default class Agent {
40
91
  if (this.options.memory_handler)
41
92
  this.options.memory_handler.setAgent(this);
42
93
 
43
- if (this.type === 'utility') {
44
- if (!this.utility || !this.utility.type)
45
- throw new Error('Utility function not defined');
46
- if (!['text', 'function', 'json'].includes(this.utility.type))
47
- throw new Error('Bad utility definition');
94
+ if (this.response_schema !== null) {
95
+ if (typeof this.response_schema !== 'object' || Array.isArray(this.response_schema))
96
+ throw new Error('response_schema must be a JSON schema object');
97
+ if (typeof this.response_schema.type !== 'string')
98
+ throw new Error('response_schema must declare a top-level "type"');
48
99
  }
49
100
 
50
101
  this.initialized = true;
@@ -66,14 +117,14 @@ export default class Agent {
66
117
  return {};
67
118
  }
68
119
 
69
- async addTool(tool) {
70
- if (!(tool instanceof Tool) || !tool.name)
71
- throw new Error('Tool must be an instance of Tool class');
72
- if (this.tools.has(tool.name))
73
- throw new Error('Tool with name ' + tool.name + ' already exists in agent');
120
+ async addToolkit(toolkit) {
121
+ if (!(toolkit instanceof Toolkit) || !toolkit.name)
122
+ throw new Error('Toolkit must be an instance of Toolkit class');
123
+ if (this.toolkits.has(toolkit.name))
124
+ throw new Error('Toolkit with name ' + toolkit.name + ' already exists in agent');
74
125
 
75
- await tool.init(this);
76
- this.tools.set(tool.name, tool);
126
+ await toolkit.init(this);
127
+ this.toolkits.set(toolkit.name, toolkit);
77
128
  }
78
129
 
79
130
  async addContext(context, options = {}) {
@@ -95,6 +146,23 @@ export default class Agent {
95
146
  this.context.push({title, context, options});
96
147
  }
97
148
 
149
+ async addMCPServer(config) {
150
+ const server = new MCPServer(config);
151
+ await this.addToolkit(server);
152
+
153
+ if (config.resources) {
154
+ const resources = await server.listResources();
155
+ for (const res of resources) {
156
+ await this.addContext(new MCPResource(server, res), {
157
+ type: 'on_request',
158
+ description: res.description || '',
159
+ });
160
+ }
161
+ }
162
+
163
+ return server;
164
+ }
165
+
98
166
  async initThread(thread) {
99
167
  await this.doInitThread(thread);
100
168
 
@@ -121,8 +189,8 @@ export default class Agent {
121
189
  let context_string = context_texts.join('\n\n');
122
190
  if (is_there_on_request) {
123
191
  context_string = '<important>Some of the context files are available to you immediately here, while longer texts may be available only on request; you are provided with a title and a description of these files. If you think it may be useful for your current task, you can request the text via the get_context tool - IMPORTANT: use the title of the file verbatim as it is provided</important>' + context_string;
124
- if (!this.tools.has('get_context'))
125
- await this.addTool(new GetContextTool(this));
192
+ if (!this.toolkits.has('get_context'))
193
+ await this.addToolkit(new GetContextToolkit(this));
126
194
  }
127
195
  context_string = `<context_info>
128
196
  ${context_string}
@@ -165,7 +233,22 @@ ${context_string}
165
233
  return thread;
166
234
  }
167
235
 
168
- async message(content, thread = null) {
236
+ message(content, thread = null) {
237
+ if (this.type === 'utility')
238
+ return this._messageAsValue(content, thread);
239
+ return this._messageAsStream(content, thread);
240
+ }
241
+
242
+ async _messageAsValue(content, thread) {
243
+ let value;
244
+ for await (const ev of this._messageAsStream(content, thread)) {
245
+ if (ev.type === 'result')
246
+ value = ev.value;
247
+ }
248
+ return value;
249
+ }
250
+
251
+ async *_messageAsStream(content, thread = null) {
169
252
  if (!this.initialized)
170
253
  throw new Error('Agent not initialized');
171
254
 
@@ -174,24 +257,221 @@ ${context_string}
174
257
  if (typeof thread !== 'object')
175
258
  thread = await this.getThread(thread);
176
259
 
260
+ if (!isAsyncIterableInput(content)) {
261
+ const model = Symposium.getModel(thread.state.model);
262
+ if (!model.audio && typeof content !== 'string') {
263
+ for (let c of content) {
264
+ if (c.type === 'audio' && !c.content?.transcription) {
265
+ const words = await this.getPromptWordsForTranscription(thread);
266
+ const prompt = words.length ? 'Possibili parole usate: ' + words.join(', ') : null;
267
+ c.content.transcription = await Symposium.transcribe(c.content, prompt);
268
+ }
269
+ }
270
+ }
271
+
272
+ await this.log('user_message', content);
273
+ thread.addMessage('user', content);
274
+
275
+ yield* this.trigger(thread);
276
+ return;
277
+ }
278
+
279
+ const iterator = content[Symbol.asyncIterator]();
280
+ const notifier = makeNotifier();
281
+ const pendingMessages = [];
282
+ const pendingAuthResponses = new Map();
283
+ const controlFlags = {cancelled: false, readerFinished: false};
284
+ const inputState = {streaming: true, pendingMessages, pendingAuthResponses, controlFlags, notifier};
285
+ this._streamingInputs.set(thread.unique, inputState);
286
+
287
+ let readerPromise = Promise.resolve();
288
+ let readerStarted = false;
289
+
290
+ try {
291
+ const {buffer, iteratorClosed, leftoverNext} = await this._drainInitialInput(iterator, controlFlags);
292
+
293
+ if (controlFlags.cancelled) {
294
+ yield {type: 'start', thread};
295
+ yield {type: 'end', thread};
296
+ return;
297
+ }
298
+
299
+ if (buffer.length === 0 && iteratorClosed) {
300
+ yield {type: 'start', thread};
301
+ yield {type: 'end', thread};
302
+ return;
303
+ }
304
+
305
+ const initialContent = normalizeStreamBuffer(buffer);
306
+ await this._transcribeAudioIfNeeded(thread, initialContent);
307
+
308
+ await this.log('user_message', initialContent);
309
+ thread.addMessage('user', initialContent);
310
+
311
+ if (!iteratorClosed) {
312
+ readerStarted = true;
313
+ readerPromise = this._runBackgroundReader(iterator, leftoverNext, inputState);
314
+ } else {
315
+ controlFlags.readerFinished = true;
316
+ }
317
+
318
+ yield* this.trigger(thread);
319
+ } finally {
320
+ this._streamingInputs.delete(thread.unique);
321
+ if (readerStarted) {
322
+ try {
323
+ if (typeof iterator.return === 'function')
324
+ await iterator.return();
325
+ } catch (e) {}
326
+ try {
327
+ await readerPromise;
328
+ } catch (e) {}
329
+ }
330
+ }
331
+ }
332
+
333
+ async _transcribeAudioIfNeeded(thread, blocks) {
177
334
  const model = Symposium.getModel(thread.state.model);
178
- if (!model.audio && typeof content !== 'string') {
179
- for (let c of content) {
180
- if (c.type === 'audio' && !c.content?.transcription) {
181
- const words = await this.getPromptWordsForTranscription(thread);
182
- const prompt = words.length ? 'Possibili parole usate: ' + words.join(', ') : null;
183
- c.content.transcription = await Symposium.transcribe(c.content, prompt);
335
+ if (model.audio)
336
+ return;
337
+ for (let c of blocks) {
338
+ if (c?.type === 'audio' && !c.content?.transcription) {
339
+ const words = await this.getPromptWordsForTranscription(thread);
340
+ const prompt = words.length ? 'Possibili parole usate: ' + words.join(', ') : null;
341
+ c.content.transcription = await Symposium.transcribe(c.content, prompt);
342
+ }
343
+ }
344
+ }
345
+
346
+ async _drainInitialInput(iterator, controlFlags) {
347
+ const buffer = [];
348
+ let iteratorClosed = false;
349
+ let nextPromise = iterator.next();
350
+
351
+ while (true) {
352
+ if (buffer.length > 0) {
353
+ const ready = await isPromiseReady(nextPromise);
354
+ if (!ready)
355
+ break;
356
+ }
357
+
358
+ let result;
359
+ try {
360
+ result = await nextPromise;
361
+ } catch (e) {
362
+ iteratorClosed = true;
363
+ nextPromise = null;
364
+ throw e;
365
+ }
366
+
367
+ if (result.done) {
368
+ iteratorClosed = true;
369
+ nextPromise = null;
370
+ break;
371
+ }
372
+
373
+ const item = result.value;
374
+ if (isControlMessage(item)) {
375
+ if (item.type === 'submit') {
376
+ nextPromise = iterator.next();
377
+ break;
378
+ }
379
+ if (item.type === 'cancel') {
380
+ controlFlags.cancelled = true;
381
+ iteratorClosed = true;
382
+ nextPromise = null;
383
+ break;
184
384
  }
385
+ nextPromise = iterator.next();
386
+ continue;
185
387
  }
388
+
389
+ buffer.push(item);
390
+ nextPromise = iterator.next();
186
391
  }
187
392
 
393
+ return {buffer, iteratorClosed, leftoverNext: nextPromise};
394
+ }
395
+
396
+ async _runBackgroundReader(iterator, leftoverNext, inputState) {
397
+ const {pendingMessages, controlFlags, notifier} = inputState;
398
+ try {
399
+ let pending = leftoverNext;
400
+ while (true) {
401
+ const result = await (pending || iterator.next());
402
+ pending = null;
403
+ if (result.done)
404
+ return;
405
+ const item = result.value;
406
+ if (isControlMessage(item)) {
407
+ if (item.type === 'cancel') {
408
+ controlFlags.cancelled = true;
409
+ notifier.signal();
410
+ return;
411
+ }
412
+ if (item.type === 'auth' && item.id) {
413
+ inputState.pendingAuthResponses.set(item.id, item.decision);
414
+ notifier.signal();
415
+ continue;
416
+ }
417
+ continue;
418
+ }
419
+ pendingMessages.push(item);
420
+ notifier.signal();
421
+ }
422
+ } finally {
423
+ controlFlags.readerFinished = true;
424
+ notifier.signal();
425
+ }
426
+ }
427
+
428
+ async _drainPendingMessages(thread) {
429
+ const state = this._streamingInputs.get(thread.unique);
430
+ if (!state || !state.pendingMessages.length)
431
+ return;
432
+ const items = state.pendingMessages.splice(0);
433
+ const content = normalizeStreamBuffer(items);
434
+ await this._transcribeAudioIfNeeded(thread, content);
188
435
  await this.log('user_message', content);
189
436
  thread.addMessage('user', content);
437
+ }
190
438
 
191
- return this.trigger(thread);
439
+ async _awaitNextStreamingInput(thread) {
440
+ const state = this._streamingInputs.get(thread.unique);
441
+ if (!state)
442
+ return false;
443
+ while (true) {
444
+ if (state.controlFlags.cancelled)
445
+ return false;
446
+ if (state.pendingMessages.length)
447
+ return true;
448
+ if (state.controlFlags.readerFinished)
449
+ return false;
450
+ await state.notifier.wait();
451
+ }
192
452
  }
193
453
 
194
- async trigger(thread = null) {
454
+ async _awaitAuthDecision(thread, id) {
455
+ const state = this._streamingInputs.get(thread.unique);
456
+ if (!state)
457
+ return 'reject';
458
+ while (true) {
459
+ if (state.pendingAuthResponses.has(id)) {
460
+ const decision = state.pendingAuthResponses.get(id);
461
+ state.pendingAuthResponses.delete(id);
462
+ return decision;
463
+ }
464
+ if (state.controlFlags.cancelled)
465
+ return 'reject';
466
+ if (state.controlFlags.readerFinished) {
467
+ state.controlFlags.cancelled = true;
468
+ return 'reject';
469
+ }
470
+ await state.notifier.wait();
471
+ }
472
+ }
473
+
474
+ async *trigger(thread = null) {
195
475
  if (!this.initialized)
196
476
  throw new Error('Agent not initialized');
197
477
 
@@ -200,119 +480,130 @@ ${context_string}
200
480
  if (typeof thread !== 'object')
201
481
  thread = await this.getThread(thread);
202
482
 
203
- const emitter = new BufferedEventEmitter();
204
- emitter.emit('start', thread);
205
-
206
- return this.execute(thread, emitter);
483
+ yield {type: 'start', thread};
484
+ try {
485
+ yield* this.execute(thread);
486
+ } finally {
487
+ yield {type: 'end', thread};
488
+ }
207
489
  }
208
490
 
209
- async beforeExecute(thread, emitter) {
491
+ async beforeExecute(thread) {
210
492
  if (this.options.memory_handler)
211
493
  thread = await this.options.memory_handler.handle(thread);
212
494
  return thread;
213
495
  }
214
496
 
215
- async execute(thread, emitter, counter = 0) {
216
- const execution = new Promise(async (resolve, reject) => {
217
- try {
218
- if (counter === 0)
219
- thread = await this.beforeExecute(thread, emitter);
220
-
221
- const model = Symposium.getModel(thread.state.model);
222
-
223
- const completion_options = {};
224
- if (this.type === 'utility') {
225
- if (['function', 'json'].includes(this.utility.type)) {
226
- if (!this.utility.function || !this.utility.function.name || !this.utility.function.parameters)
227
- throw new Error('Bad function definition');
228
-
229
- let response_format = null;
230
- if (this.utility.type === 'json' && model.structured_output)
231
- response_format = this.convertFunctionToResponseFormat(this.utility.function.parameters);
232
-
233
- if (response_format && response_format.count <= 100) { // OpenAI does not support structured output if there are more than 100 parameters
234
- completion_options.response_format = {
235
- type: 'json_schema',
236
- name: this.utility.function.name,
237
- schema: response_format.obj,
238
- strict: true,
239
- };
240
- } else {
241
- completion_options.functions = [
242
- this.utility.function,
243
- ];
244
- completion_options.force_function = this.utility.function.name;
245
- }
246
- }
247
- }
497
+ async *execute(thread) {
498
+ thread = await this.beforeExecute(thread);
499
+
500
+ const model = Symposium.getModel(thread.state.model);
501
+
502
+ const completion_options = {};
503
+ if (this.response_schema) {
504
+ const schema = this.response_schema;
505
+ const converted = (schema.type === 'object' && model.structured_output)
506
+ ? this.convertFunctionToResponseFormat(JSON.parse(JSON.stringify(schema)))
507
+ : null;
508
+
509
+ if (converted && converted.count <= 100) { // OpenAI does not support structured output if there are more than 100 parameters
510
+ completion_options.response_format = {
511
+ type: 'json_schema',
512
+ name: 'response',
513
+ schema: converted.obj,
514
+ strict: true,
515
+ };
516
+ } else {
517
+ completion_options.tools = [{
518
+ name: 'response',
519
+ parameters: schema,
520
+ }];
521
+ completion_options.force_tool = 'response';
522
+ }
523
+ }
248
524
 
525
+ const streamingState = this._streamingInputs.get(thread.unique);
526
+ const streaming = !!streamingState;
527
+
528
+ let counter = 0;
529
+ let output_yielded = false;
530
+ while (true) {
531
+ if (streaming) {
532
+ await this._drainPendingMessages(thread);
533
+ if (streamingState.controlFlags.cancelled)
534
+ return;
535
+ }
536
+
537
+ try {
538
+ // Inline drain of generateCompletion so we can observe `chunk`
539
+ // events and flip output_yielded for the hybrid retry strategy.
540
+ const it = this.generateCompletion(thread, completion_options);
541
+ let step = await it.next();
249
542
  let completion;
250
- try {
251
- completion = await this.generateCompletion(thread, completion_options);
252
- } catch (e) {
253
- console.error(e.message);
254
- switch (this.type) {
255
- case 'chat':
256
- emitter.emit('error', e.message);
257
- return resolve(e);
258
-
259
- case 'utility':
260
- throw e;
261
-
262
- default:
263
- throw new Error('Bad agent type');
264
- }
543
+ while (!step.done) {
544
+ const ev = step.value;
545
+ if (ev?.type === 'chunk')
546
+ output_yielded = true;
547
+ yield ev;
548
+ step = await it.next();
265
549
  }
550
+ completion = step.value;
266
551
 
267
- try {
268
- thread = await this.afterExecute(thread, completion, emitter);
552
+ thread = await this.afterExecute(thread, completion);
269
553
 
270
- for (let message of completion) {
271
- if (message.role === 'assistant' && message.content.some(c => c.type === 'reasoning')) {
272
- const reasoning = message.content.find(c => c.type === 'reasoning').content;
273
- if (reasoning)
274
- emitter.emit('reasoning', reasoning);
275
- }
554
+ for (let message of completion) {
555
+ if (message.role === 'assistant' && message.content.some(c => c.type === 'reasoning')) {
556
+ const reasoning = message.content.find(c => c.type === 'reasoning').content;
557
+ if (reasoning)
558
+ yield {type: 'reasoning', content: reasoning};
276
559
  }
560
+ }
277
561
 
278
- const response = await this.handleCompletion(thread, completion, emitter);
279
-
280
- switch (this.type) {
281
- case 'utility':
282
- if (response.type !== 'response')
283
- throw new Error('Utility agent did not return a response');
562
+ const verdict = yield* this.handleCompletion(thread, completion);
284
563
 
285
- return resolve(response.value);
564
+ if (verdict?.type === 'response') {
565
+ yield {type: 'result', value: verdict.value};
566
+ return;
567
+ }
286
568
 
287
- case 'chat':
288
- if (response?.type === 'continue')
289
- return this.execute(thread, emitter);
569
+ switch (this.type) {
570
+ case 'utility':
571
+ throw new Error('Utility agent did not return a response');
290
572
 
291
- return resolve(null);
573
+ case 'chat':
574
+ if (verdict?.type === 'continue') {
575
+ counter = 0;
576
+ output_yielded = false;
577
+ continue;
578
+ }
579
+ if (streaming) {
580
+ const more = await this._awaitNextStreamingInput(thread);
581
+ if (!more)
582
+ return;
583
+ counter = 0;
584
+ output_yielded = false;
585
+ continue;
586
+ }
587
+ return;
292
588
 
293
- default:
294
- throw new Error('Bad agent type');
295
- }
296
- } catch (e) {
297
- console.error(e);
298
-
299
- if (counter < this.max_retries) {
300
- await this.execute(thread, emitter, counter + 1)
301
- .then(response => resolve(response))
302
- .catch(err => reject(err));
303
- } else {
304
- throw e;
305
- }
589
+ default:
590
+ throw new Error('Bad agent type');
306
591
  }
307
592
  } catch (e) {
308
- reject(e);
593
+ if (counter < this.max_retries) {
594
+ counter++;
595
+ const reason = e?.message || String(e);
596
+ if (output_yielded)
597
+ yield {type: 'retry', attempt: counter, reason};
598
+ // Preserve the legacy 1-second backoff for transport-level 5xx.
599
+ if (e?.response?.status >= 500)
600
+ await new Promise(resolve => setTimeout(resolve, 1000));
601
+ output_yielded = false;
602
+ continue;
603
+ }
604
+ throw e;
309
605
  }
310
- }).then(value => {
311
- emitter.emit('end', thread);
312
- return value;
313
- });
314
-
315
- return this.type === 'chat' ? emitter : execution;
606
+ }
316
607
  }
317
608
 
318
609
  convertFunctionToResponseFormat(obj) {
@@ -348,41 +639,43 @@ ${context_string}
348
639
  };
349
640
  }
350
641
 
351
- async afterExecute(thread, completion, emitter) {
642
+ async afterExecute(thread, completion) {
352
643
  return thread;
353
644
  }
354
645
 
355
- async generateCompletion(thread, options = {}, retry_counter = 1) {
646
+ async *generateCompletion(thread, options = {}) {
647
+ const model = Symposium.getModel(thread.state.model);
648
+ const it = model.class.generate(model, thread, await this.getTools(), {
649
+ ...options,
650
+ image_generation: this.enable_image_generation,
651
+ });
652
+
653
+ let messages;
356
654
  try {
357
- const model = Symposium.getModel(thread.state.model);
358
- const messages = await model.class.generate(model, thread, await this.getFunctions(), {
359
- ...options,
360
- image_generation: this.enable_image_generation,
361
- });
362
- return model.tools ? messages : messages.map(m => this.parseFunctions(m));
655
+ let step = await it.next();
656
+ while (!step.done) {
657
+ const delta = step.value;
658
+ if (delta && delta.type === 'text_delta')
659
+ yield {type: 'chunk', content: delta.content};
660
+ step = await it.next();
661
+ }
662
+ messages = step.value;
363
663
  } catch (error) {
364
- if (error.response) {
365
- console.error(error.response.status);
366
- console.error(error.response.data);
367
-
368
- if (error.response.status >= 500 && retry_counter <= this.max_retries) {
369
- await new Promise(resolve => {
370
- setTimeout(resolve, 1000);
371
- });
372
-
373
- return this.generateCompletion(thread, options, retry_counter + 1);
374
- }
375
-
376
- throw new Error(error.response.status + ': ' + JSON.stringify(error.response.data));
377
- } else if (error.message) {
378
- throw new Error(error.message);
379
- } else {
380
- throw new Error('Errore interno');
664
+ // Normalize error shape; the outer execute loop owns retry policy.
665
+ if (error?.response) {
666
+ const normalized = new Error(error.response.status + ': ' + JSON.stringify(error.response.data));
667
+ normalized.response = error.response;
668
+ throw normalized;
381
669
  }
670
+ if (error?.message)
671
+ throw new Error(error.message);
672
+ throw error;
382
673
  }
674
+
675
+ return model.tools ? messages : messages.map(m => this.parseTools(m));
383
676
  }
384
677
 
385
- parseFunctions(message) {
678
+ parseTools(message) {
386
679
  const newContent = [];
387
680
  for (let m of message.content) {
388
681
  if (m.type === 'text' && m.content.match(/```\nCALL [A-Za-z0-9_]+\n[\s\S]*```/)) {
@@ -394,7 +687,7 @@ ${context_string}
394
687
 
395
688
  const match = text.match(/^CALL ([A-Za-z0-9_]+)\n([\s\S]*)$/);
396
689
  if (match)
397
- newContent.push({type: 'function', content: [{name: match[1], arguments: JSON.parse(match[2] || '{}')}]});
690
+ newContent.push({type: 'tool_call', content: [{name: match[1], arguments: JSON.parse(match[2] || '{}')}]});
398
691
  else
399
692
  newContent.push({type: 'text', content: text});
400
693
  }
@@ -407,10 +700,10 @@ ${context_string}
407
700
  return message;
408
701
  }
409
702
 
410
- async handleCompletion(thread, completion, emitter) {
703
+ async *handleCompletion(thread, completion) {
411
704
  const model = Symposium.getModel(thread.state.model);
412
705
 
413
- const functions = [];
706
+ const tool_calls = [];
414
707
  for (let message of completion) {
415
708
  thread.addDirectMessage(message);
416
709
  await this.log('ai_message', message.content);
@@ -418,33 +711,31 @@ ${context_string}
418
711
  for (let m of message.content) {
419
712
  switch (m.type) {
420
713
  case 'text':
421
- if (this.type === 'utility') {
422
- if (this.utility.type === 'text')
423
- return {type: 'response', value: this.afterHandle(thread, completion, m.content)};
424
- if (this.utility.type === 'json' && model.structured_output)
425
- return {type: 'response', value: this.afterHandle(thread, completion, JSON.parse(m.content))};
426
- }
714
+ if (this.response_schema && model.structured_output)
715
+ return {type: 'response', value: await this.afterHandle(thread, completion, JSON.parse(m.content))};
716
+ if (this.type === 'utility' && !this.response_schema)
717
+ return {type: 'response', value: await this.afterHandle(thread, completion, m.content)};
427
718
 
428
- emitter.emit('output', m);
719
+ yield {type: 'output', content: m};
429
720
  break;
430
721
 
431
722
  case 'image':
432
- emitter.emit('output', m);
723
+ yield {type: 'output', content: m};
433
724
  break;
434
725
 
435
- case 'function':
436
- for (let f of m.content)
437
- functions.push(f);
726
+ case 'tool_call':
727
+ for (let t of m.content)
728
+ tool_calls.push(t);
438
729
  break;
439
730
  }
440
731
  }
441
732
  }
442
733
 
443
- if (functions.length) {
444
- if (this.utility && ['function', 'json'].includes(this.utility.type))
445
- return {type: 'response', value: this.afterHandle(thread, completion, functions[0].arguments)};
734
+ if (tool_calls.length) {
735
+ if (this.response_schema)
736
+ return {type: 'response', value: await this.afterHandle(thread, completion, tool_calls[0].arguments)};
446
737
 
447
- return this.callFunctions(thread, emitter, completion, functions);
738
+ return yield* this.callTools(thread, completion, tool_calls);
448
739
  } else {
449
740
  await thread.storeState();
450
741
  await this.afterHandle(thread, completion);
@@ -452,49 +743,54 @@ ${context_string}
452
743
  }
453
744
  }
454
745
 
455
- async confirmFunctions({thread, functions, completion, emitter}, always = false) {
456
- const response = await this.callFunctions(thread, emitter, completion, functions, true, always);
457
- if (response?.type === 'continue')
458
- return this.execute(thread, emitter);
459
- }
460
-
461
- async callFunctions(thread, emitter, completion, functions_to_call, authorize = false, authorize_always = false) {
462
- const functions = await this.getFunctions(false);
746
+ async *callTools(thread, completion, tools_to_call) {
747
+ const tools = await this.getTools(false);
463
748
 
464
749
  let is_authorized = true;
465
- for (let f of functions_to_call) {
466
- if (!functions.has(f.name))
467
- throw new Error('Unrecognized function ' + f.name);
750
+ for (let t of tools_to_call) {
751
+ if (!tools.has(t.name))
752
+ throw new Error('Unrecognized tool ' + t.name);
468
753
 
469
- if (authorize && authorize_always)
470
- await functions.get(f.name).tool.authorizeAlways(thread, f.name, f.arguments);
471
-
472
- if (!authorize && !(await functions.get(f.name).tool.authorize(thread, f.name, f.arguments))) {
754
+ if (!(await tools.get(t.name).toolkit.authorize(thread, t.name, t.arguments))) {
473
755
  is_authorized = false;
474
756
  break;
475
757
  }
476
758
  }
477
759
 
478
760
  if (!is_authorized) {
479
- emitter.emit('tools_auth', {thread, functions: functions_to_call, completion, emitter});
480
- return {type: 'void'};
761
+ const id = uuid();
762
+ yield {type: 'tools_auth', id, tools: tools_to_call};
763
+
764
+ const decision = await this._awaitAuthDecision(thread, id);
765
+
766
+ if (decision === 'reject')
767
+ return {type: 'void'};
768
+
769
+ if (decision === 'approve_always') {
770
+ for (let t of tools_to_call)
771
+ await tools.get(t.name).toolkit.authorizeAlways(thread, t.name, t.arguments);
772
+ } else if (decision !== 'approve') {
773
+ throw new Error('Bad authorization decision: ' + decision);
774
+ }
481
775
  }
482
776
 
483
- const responses = await Promise.all(functions_to_call.map(async f => this.callFunction(thread, functions, f, emitter)));
777
+ const responses = [];
778
+ for (let t of tools_to_call)
779
+ responses.push(yield* this.callTool(thread, tools, t));
484
780
 
485
781
  for (let response of responses) {
486
782
  thread.addMessage('tool', [
487
783
  {
488
- type: 'function_response',
784
+ type: 'tool_result',
489
785
  content: {
490
- name: response.function.name,
491
- id: response.function.id || undefined,
786
+ name: response.tool_call.name,
787
+ id: response.tool_call.id || undefined,
492
788
  response: response.response,
493
789
  },
494
790
  },
495
- ], response.function.name);
791
+ ], response.tool_call.name);
496
792
 
497
- await this.log('function_response', response);
793
+ await this.log('tool_result', response);
498
794
  }
499
795
 
500
796
  thread.flushPlannedMessages();
@@ -503,53 +799,51 @@ ${context_string}
503
799
  return {type: 'continue'};
504
800
  }
505
801
 
506
- async getFunctions(parsed = true) {
507
- if (this.functions === null) {
508
- this.functions = new Map();
509
- for (let tool of this.tools.values()) {
510
- let functions = await tool.getFunctions();
511
- for (let func of functions) {
512
- if (this.functions.has(func.name))
513
- throw new Error('Duplicate function ' + func.name + ' in agent');
514
-
515
- this.functions.set(func.name, {
516
- tool,
517
- function: func,
802
+ async getTools(parsed = true) {
803
+ if (this.tools === null) {
804
+ this.tools = new Map();
805
+ for (let toolkit of this.toolkits.values()) {
806
+ let toolDefs = await toolkit.getTools();
807
+ for (let toolDef of toolDefs) {
808
+ if (this.tools.has(toolDef.name))
809
+ throw new Error('Duplicate tool ' + toolDef.name + ' in agent');
810
+
811
+ this.tools.set(toolDef.name, {
812
+ toolkit,
813
+ definition: toolDef,
518
814
  });
519
815
  }
520
816
  }
521
817
  }
522
818
 
523
819
  if (parsed)
524
- return Array.from(this.functions.values()).map(f => f.function)
820
+ return Array.from(this.tools.values()).map(e => e.definition)
525
821
  else
526
- return this.functions;
822
+ return this.tools;
527
823
  }
528
824
 
529
- async callFunction(thread, functions, function_call, emitter = null) {
530
- const function_definition = functions.get(function_call.name);
825
+ async *callTool(thread, tools, tool_call) {
826
+ const entry = tools.get(tool_call.name);
531
827
 
532
- await this.log('function_call', function_call);
533
- emitter.emit('tool', function_call);
828
+ await this.log('tool_call', tool_call);
829
+ yield {type: 'tool', id: tool_call.id, name: tool_call.name, arguments: tool_call.arguments};
534
830
 
535
831
  try {
536
- const response = await function_definition.tool.callFunction(thread, function_call.name, function_call.arguments);
537
- if (emitter)
538
- emitter.emit('tool_response', {name: function_definition.tool.name, success: true, response});
832
+ const response = await entry.toolkit.callTool(thread, tool_call.name, tool_call.arguments);
833
+ yield {type: 'tool_response', name: entry.toolkit.name, success: true, response};
539
834
 
540
835
  return {
541
836
  type: 'response',
542
837
  response,
543
- function: function_call,
838
+ tool_call,
544
839
  };
545
840
  } catch (error) {
546
- if (emitter)
547
- emitter.emit('tool_response', {name: function_definition.tool.name, success: false, error: error.message || error});
841
+ yield {type: 'tool_response', name: entry.toolkit.name, success: false, error: error.message || error};
548
842
 
549
843
  return {
550
844
  type: 'response',
551
845
  response: {error},
552
- function: function_call,
846
+ tool_call,
553
847
  };
554
848
  }
555
849
  }
@@ -558,10 +852,6 @@ ${context_string}
558
852
  return value;
559
853
  }
560
854
 
561
- getEmitter() {
562
- return new BufferedEventEmitter();
563
- }
564
-
565
855
  async setModel(thread, label) {
566
856
  const model_to_switch = Symposium.getModel(label);
567
857
  if (model_to_switch && model_to_switch.type === 'llm')
@@ -602,7 +892,7 @@ ${context_string}
602
892
  if (conversation.length && options.include_thread)
603
893
  instructions += '\n\n# Ecco la tua conversazione fino ad ora: #\n' + conversation.join('\n');
604
894
 
605
- const tools = (await this.getFunctions()).map(t => ({
895
+ const tools = (await this.getTools()).map(t => ({
606
896
  type: 'function',
607
897
  ...t,
608
898
  }));