symposium 2.4.3 → 3.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.
- package/Agent.js +509 -219
- package/CLAUDE.md +101 -0
- package/Contexts/MCPResource.js +19 -0
- package/{GetContextTool.js → GetContextToolkit.js} +5 -5
- package/InputChannel.js +42 -0
- package/MCPServer.js +160 -0
- package/MIGRATION.md +369 -0
- package/Model.js +32 -25
- package/Models/AnthropicModel.js +66 -20
- package/Models/GrokModel.js +8 -8
- package/Models/GroqModel.js +61 -35
- package/Models/LegacyOpenAIModel.js +61 -35
- package/Models/OllamaModel.js +57 -31
- package/Models/OpenAIModel.js +65 -20
- package/README.md +458 -396
- package/Summarizer.js +5 -5
- package/Symposium.js +12 -12
- package/{Tool.js → Toolkit.js} +4 -4
- package/index.js +10 -2
- package/package.json +7 -3
- package/test/agent.test.js +698 -0
- package/test/helpers/mockSdk.js +52 -0
- package/test/mcp.test.js +216 -0
- package/test/models/anthropic.test.js +135 -0
- package/test/models/groq.test.js +71 -0
- package/test/models/legacyOpenai.test.js +87 -0
- package/test/models/ollama.test.js +90 -0
- package/test/models/openai.test.js +168 -0
- package/BufferedEventEmitter.js +0 -28
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
|
|
5
|
+
import Toolkit from "./Toolkit.js";
|
|
8
6
|
import Context from "./Context.js";
|
|
9
7
|
import Text from "./Contexts/Text.js";
|
|
10
|
-
import
|
|
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
|
-
|
|
18
|
-
|
|
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
|
-
|
|
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.
|
|
44
|
-
if (
|
|
45
|
-
throw new Error('
|
|
46
|
-
if (
|
|
47
|
-
throw new Error('
|
|
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
|
|
70
|
-
if (!(
|
|
71
|
-
throw new Error('
|
|
72
|
-
if (this.
|
|
73
|
-
throw new Error('
|
|
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
|
|
76
|
-
this.
|
|
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.
|
|
125
|
-
await 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
|
-
|
|
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 (
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
|
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
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
-
|
|
268
|
-
thread = await this.afterExecute(thread, completion, emitter);
|
|
552
|
+
thread = await this.afterExecute(thread, completion);
|
|
269
553
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
|
|
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
|
-
|
|
564
|
+
if (verdict?.type === 'response') {
|
|
565
|
+
yield {type: 'result', value: verdict.value};
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
286
568
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
569
|
+
switch (this.type) {
|
|
570
|
+
case 'utility':
|
|
571
|
+
throw new Error('Utility agent did not return a response');
|
|
290
572
|
|
|
291
|
-
|
|
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
|
-
|
|
294
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
|
642
|
+
async afterExecute(thread, completion) {
|
|
352
643
|
return thread;
|
|
353
644
|
}
|
|
354
645
|
|
|
355
|
-
async generateCompletion(thread, options = {}
|
|
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
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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
|
-
|
|
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: '
|
|
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
|
|
703
|
+
async *handleCompletion(thread, completion) {
|
|
411
704
|
const model = Symposium.getModel(thread.state.model);
|
|
412
705
|
|
|
413
|
-
const
|
|
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.
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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
|
-
|
|
719
|
+
yield {type: 'output', content: m};
|
|
429
720
|
break;
|
|
430
721
|
|
|
431
722
|
case 'image':
|
|
432
|
-
|
|
723
|
+
yield {type: 'output', content: m};
|
|
433
724
|
break;
|
|
434
725
|
|
|
435
|
-
case '
|
|
436
|
-
for (let
|
|
437
|
-
|
|
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 (
|
|
444
|
-
if (this.
|
|
445
|
-
return {type: 'response', value: this.afterHandle(thread, completion,
|
|
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.
|
|
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
|
|
456
|
-
const
|
|
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
|
|
466
|
-
if (!
|
|
467
|
-
throw new Error('Unrecognized
|
|
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
|
|
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
|
-
|
|
480
|
-
|
|
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 =
|
|
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: '
|
|
784
|
+
type: 'tool_result',
|
|
489
785
|
content: {
|
|
490
|
-
name: response.
|
|
491
|
-
id: response.
|
|
786
|
+
name: response.tool_call.name,
|
|
787
|
+
id: response.tool_call.id || undefined,
|
|
492
788
|
response: response.response,
|
|
493
789
|
},
|
|
494
790
|
},
|
|
495
|
-
], response.
|
|
791
|
+
], response.tool_call.name);
|
|
496
792
|
|
|
497
|
-
await this.log('
|
|
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
|
|
507
|
-
if (this.
|
|
508
|
-
this.
|
|
509
|
-
for (let
|
|
510
|
-
let
|
|
511
|
-
for (let
|
|
512
|
-
if (this.
|
|
513
|
-
throw new Error('Duplicate
|
|
514
|
-
|
|
515
|
-
this.
|
|
516
|
-
|
|
517
|
-
|
|
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.
|
|
820
|
+
return Array.from(this.tools.values()).map(e => e.definition)
|
|
525
821
|
else
|
|
526
|
-
return this.
|
|
822
|
+
return this.tools;
|
|
527
823
|
}
|
|
528
824
|
|
|
529
|
-
async
|
|
530
|
-
const
|
|
825
|
+
async *callTool(thread, tools, tool_call) {
|
|
826
|
+
const entry = tools.get(tool_call.name);
|
|
531
827
|
|
|
532
|
-
await this.log('
|
|
533
|
-
|
|
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
|
|
537
|
-
|
|
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
|
-
|
|
838
|
+
tool_call,
|
|
544
839
|
};
|
|
545
840
|
} catch (error) {
|
|
546
|
-
|
|
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
|
-
|
|
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.
|
|
895
|
+
const tools = (await this.getTools()).map(t => ({
|
|
606
896
|
type: 'function',
|
|
607
897
|
...t,
|
|
608
898
|
}));
|