symposium 2.4.2 → 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/MIGRATION.md ADDED
@@ -0,0 +1,369 @@
1
+ # Migrating from Symposium 2.x to 3.0
2
+
3
+ Symposium 3.0 is a major release with breaking API changes. The core idea: replace the `EventEmitter`-based agent API with **async generators**, and let consumers push messages into a running agent via a **streaming input channel**.
4
+
5
+ This guide walks through the most common patterns side by side.
6
+
7
+ ## TL;DR
8
+
9
+ | 2.x | 3.0 |
10
+ |---|---|
11
+ | `agent.message()` returns an `EventEmitter` | `agent.message()` returns an `AsyncGenerator` for chat agents, `Promise<value>` for utility agents |
12
+ | `emitter.on('output', ...)` | `for await (const ev of agent.message(...))` |
13
+ | `emitter.on('error', ...)` | `try { for await ... } catch (err) { ... }` |
14
+ | `agent.confirmFunctions(thread, fns, completion, decision)` | `input.send({ type: 'auth', id, decision })` on the input channel |
15
+ | `agent.utility = { type, function, parameters }` | `agent.response_schema = <json-schema>` (works on chat agents too) |
16
+ | Fake "chunks" synthesized after the model finished | Real `chunk` events streamed as tokens arrive |
17
+ | No way to push messages mid-run | Pass an `AsyncIterable` (see `createInputChannel()`) |
18
+
19
+ ## 1. Simple chat
20
+
21
+ **Before (2.x):**
22
+
23
+ ```javascript
24
+ const emitter = await agent.message('Hello');
25
+
26
+ emitter.on('output', msg => {
27
+ if (msg.type === 'text')
28
+ process.stdout.write(msg.content);
29
+ });
30
+ emitter.on('error', err => console.error(err));
31
+ emitter.on('end', () => console.log('done'));
32
+ ```
33
+
34
+ **After (3.0):**
35
+
36
+ ```javascript
37
+ try {
38
+ for await (const ev of agent.message('Hello')) {
39
+ if (ev.type === 'chunk')
40
+ process.stdout.write(ev.content);
41
+ }
42
+ console.log('done');
43
+ } catch (err) {
44
+ console.error(err);
45
+ }
46
+ ```
47
+
48
+ Key changes:
49
+
50
+ - No `await` needed before iteration — `agent.message()` returns the generator synchronously.
51
+ - **Use `chunk` for incremental streaming.** It carries text deltas as they arrive from the provider (true streaming, finally). The old `output` event still exists, but it now carries the fully assembled content block for the assistant message — you'd use it if you only care about the final result.
52
+ - Errors throw out of the generator. The `error` event is gone — wrap the loop in `try/catch`.
53
+ - The `end` event still exists but mainly for side-channel cleanup; the loop simply finishes.
54
+
55
+ ## 2. Tools
56
+
57
+ **Before (2.x):**
58
+
59
+ ```javascript
60
+ const emitter = await agent.message('What is the weather in Paris?');
61
+
62
+ emitter.on('tool', tool => {
63
+ console.log(`> calling ${tool.name}`);
64
+ });
65
+ emitter.on('tool_response', resp => {
66
+ if (resp.success)
67
+ console.log(`> ${resp.name} OK`);
68
+ else
69
+ console.log(`> ${resp.name} FAILED: ${resp.error}`);
70
+ });
71
+ ```
72
+
73
+ **After (3.0):**
74
+
75
+ ```javascript
76
+ for await (const ev of agent.message('What is the weather in Paris?')) {
77
+ switch (ev.type) {
78
+ case 'tool':
79
+ console.log(`> calling ${ev.name}`);
80
+ break;
81
+ case 'tool_response':
82
+ if (ev.success)
83
+ console.log(`> ${ev.name} OK`);
84
+ else
85
+ console.log(`> ${ev.name} FAILED: ${ev.error}`);
86
+ break;
87
+ }
88
+ }
89
+ ```
90
+
91
+ Same event shapes, different delivery mechanism. Note that tools within a single LLM turn now execute **sequentially** in the order the model requested them, so `tool` / `tool_response` events arrive in deterministic order.
92
+
93
+ ## 3. Tool authorization
94
+
95
+ This is the biggest behavioral change. The old `confirmFunctions` callback is gone.
96
+
97
+ **Before (2.x):**
98
+
99
+ ```javascript
100
+ const emitter = await agent.message('Run that risky thing');
101
+
102
+ emitter.on('tools_auth', async ({ thread, functions, completion }) => {
103
+ const decision = await askUser(functions); // 'approve' | 'approve_always' | 'reject'
104
+ await agent.confirmFunctions(thread, functions, completion, decision);
105
+ });
106
+ ```
107
+
108
+ **After (3.0):**
109
+
110
+ ```javascript
111
+ import { createInputChannel } from 'symposium';
112
+
113
+ const input = createInputChannel();
114
+ input.send('Run that risky thing');
115
+
116
+ for await (const ev of agent.message(input)) {
117
+ if (ev.type === 'tools_auth') {
118
+ const decision = await askUser(ev.tools); // 'approve' | 'approve_always' | 'reject'
119
+ input.send({ type: 'auth', id: ev.id, decision });
120
+ }
121
+ }
122
+ ```
123
+
124
+ Key changes:
125
+
126
+ - You now need an **input channel** to deliver the auth decision (there's no callback to call).
127
+ - The `tools_auth` event carries an `id` (UUID) — echo it back in the `auth` control message so the agent knows which pending batch you're answering.
128
+ - If you call `agent.message()` with a plain string and a tool requires auth, the agent **auto-rejects** the call (no channel = no way to respond).
129
+ - If the channel closes while a `tools_auth` is pending, it's treated as a reject and the run cancels.
130
+
131
+ ## 4. Structured output
132
+
133
+ The old `utility = { type, function, parameters }` shape was removed. Use `response_schema` (a raw JSON schema) instead. `response_schema` is independent of the agent type — it works on chat agents too.
134
+
135
+ **Before (2.x):**
136
+
137
+ ```javascript
138
+ class ExtractorAgent extends Agent {
139
+ type = 'utility';
140
+ utility = {
141
+ type: 'json',
142
+ function: {
143
+ name: 'extract',
144
+ parameters: {
145
+ type: 'object',
146
+ properties: {
147
+ name: { type: 'string' },
148
+ email: { type: 'string' },
149
+ },
150
+ required: ['name', 'email'],
151
+ },
152
+ },
153
+ };
154
+ }
155
+
156
+ const result = await extractor.message('My name is John, john@example.com');
157
+ // result was already the parsed value — that part hasn't changed.
158
+ ```
159
+
160
+ **After (3.0):**
161
+
162
+ ```javascript
163
+ class ExtractorAgent extends Agent {
164
+ type = 'utility';
165
+ response_schema = {
166
+ type: 'object',
167
+ properties: {
168
+ name: { type: 'string' },
169
+ email: { type: 'string' },
170
+ },
171
+ required: ['name', 'email'],
172
+ };
173
+ }
174
+
175
+ const result = await extractor.message('My name is John, john@example.com');
176
+ // { name: 'John', email: 'john@example.com' }
177
+ ```
178
+
179
+ For a **chat agent with a structured final answer**, the parsed value arrives as a `result` event right before `end`:
180
+
181
+ ```javascript
182
+ agent.response_schema = { type: 'object', properties: { city: { type: 'string' } }, required: ['city'] };
183
+
184
+ for await (const ev of agent.message('Look up the weather and answer in JSON')) {
185
+ if (ev.type === 'result')
186
+ console.log(ev.value); // { city: '...' }
187
+ }
188
+ ```
189
+
190
+ ## 5. Streaming user input
191
+
192
+ New in 3.0 — there was no 2.x equivalent.
193
+
194
+ ```javascript
195
+ import { createInputChannel } from 'symposium';
196
+
197
+ const input = createInputChannel();
198
+ input.send('Plan a trip to Rome');
199
+
200
+ const events = agent.message(input);
201
+
202
+ // Concurrently, from anywhere else:
203
+ setTimeout(() => input.send('Actually, make it Florence instead'), 2000);
204
+
205
+ for await (const ev of events) {
206
+ if (ev.type === 'chunk')
207
+ process.stdout.write(ev.content);
208
+ }
209
+
210
+ // End the run when you're done sending:
211
+ input.close();
212
+ ```
213
+
214
+ Behavior:
215
+
216
+ - The agent starts the first model turn once content has arrived (or once a `{type:'submit'}` control message lands).
217
+ - New items pushed mid-turn are queued and inserted as a `user` message at the next inter-turn boundary (after the current tool batch finishes — there is no mid-turn cancellation of the model call).
218
+ - The run continues across multiple turns until you `input.close()` or send `{type:'cancel'}`.
219
+
220
+ ## 6. Retries
221
+
222
+ Agents always retried on transport errors. In 3.0 the retry is now visible to the consumer **when it matters**:
223
+
224
+ - If no `chunk` has been streamed yet for the current turn → silent retry (most cases: connection error before any tokens).
225
+ - If at least one `chunk` has streamed → yield `{type:'retry', attempt, reason}` so the consumer can clear partial output or show a spinner.
226
+
227
+ ```javascript
228
+ for await (const ev of agent.message('Hello')) {
229
+ if (ev.type === 'retry')
230
+ console.warn(`retrying (${ev.attempt}): ${ev.reason}`);
231
+ }
232
+ ```
233
+
234
+ ## 7. Lifecycle hooks
235
+
236
+ If you've subclassed `Agent` and overridden `beforeExecute` / `afterExecute` / `afterHandle`, the `emitter` parameter was removed:
237
+
238
+ ```javascript
239
+ // Before:
240
+ async afterHandle(thread, completion, emitter) { ... }
241
+
242
+ // After:
243
+ async afterHandle(thread, completion, value) { ... }
244
+ // `value` is the parsed result when `response_schema` is set; undefined otherwise.
245
+ ```
246
+
247
+ If your hook used to emit events on the emitter, that's no longer possible directly. Hooks now run inside the generator pipeline — if you need to surface information, return it from the hook or stash it on the thread state and let the consumer read it from the events.
248
+
249
+ ## 8. `BufferedEventEmitter`
250
+
251
+ Deleted. The class only existed to paper over the listener-attach race created by `agent.message()` returning an emitter. With async generators, the issue doesn't exist.
252
+
253
+ ## 9. Things that did NOT change
254
+
255
+ - Agent / Thread / Toolkit / Message / Context class shapes.
256
+ - Model registration (drop a file in `Models/`, `Symposium.init()` picks it up).
257
+ - Storage adapter interface (`init` / `get` / `set`).
258
+ - `Symposium.prompt()` — still a one-shot value-returning helper.
259
+ - `Symposium.transcribe()` / `Symposium.embed()`.
260
+ - Real-time session API (`agent.createRealtimeSession()`).
261
+ - Italian-language fallback prompt in `Model.promptFromTools()` and the realtime session preamble.
262
+
263
+ ## 10. Notes & gotchas
264
+
265
+ - **Backpressure.** Async generators are pull-driven: a slow consumer pauses the model upstream. This is generally an improvement over fire-and-forget emitter events, but it's a behavioral change to be aware of.
266
+ - **Multi-consumer.** Async generators are single-consumer. If you need to fan out an agent run to two listeners, tee it manually.
267
+ - **`message()` is not `async`.** It returns the generator synchronously for chat agents (no `await`). Utility agents return a Promise — same as before in spirit, but it now resolves to the value directly without a separate event loop.
268
+
269
+ ---
270
+
271
+ # Additional breaking changes in 3.0
272
+
273
+ 3.0 ships a vocabulary cleanup to align with industry terminology (OpenAI/Anthropic/LangChain all use **tool** and **toolkit**, not **function**). It also adds first-class **MCP** support — see the `addMCPServer()` section in `README.md` / `CLAUDE.md`. The renames below are purely cosmetic but pervasive; runtime semantics are unchanged.
274
+
275
+ ## A. `Tool` class → `Toolkit`
276
+
277
+ The base class for "a thing that publishes one or more LLM-callable tools" is now `Toolkit`. The word **tool** is reserved for the individual callable unit (which is what the LLM actually sees).
278
+
279
+ | 2.x | 3.x |
280
+ |-----------------------------------------------------|--------------------------------------------------------|
281
+ | `Tool.js` / `GetContextTool.js` | `Toolkit.js` / `GetContextToolkit.js` |
282
+ | `import { Tool } from 'symposium'` | `import { Toolkit } from 'symposium'` |
283
+ | `class Weather extends Tool` | `class Weather extends Toolkit` |
284
+ | `agent.addTool(t)` | `agent.addToolkit(t)` |
285
+ | `agent.tools` — `Map<toolkitName, Toolkit>` | `agent.toolkits` — `Map<toolkitName, Toolkit>` |
286
+ | *(internal: `agent.functions` / `agent.toolIndex`)* | `agent.tools` — `Map<toolName, {toolkit, definition}>` |
287
+
288
+ The two maps swapped roles on purpose: `agent.tools` is now the flat lookup of LLM-callables (matching how every provider's API talks about "tools"), and `agent.toolkits` is the registry of `Toolkit` instances. Lookup entries are now `{toolkit, definition}` (was `{tool, function}`).
289
+
290
+ ## B. `function` → `tool` everywhere
291
+
292
+ The framework no longer uses the word **function** for LLM-callable units. Method names, parameter names, message content types, option keys, and event payload keys all changed.
293
+
294
+ ### Method renames
295
+
296
+ | 2.x | 3.x |
297
+ |--------------------------------------------------|--------------------------------------------|
298
+ | `Toolkit.getFunctions()` | `Toolkit.getTools()` |
299
+ | `Toolkit.callFunction(thread, name, payload)` | `Toolkit.callTool(thread, name, payload)` |
300
+ | `Agent.getFunctions()` | `Agent.getTools()` |
301
+ | `Agent.callFunction()` / `Agent.callFunctions()` | `Agent.callTool()` / `Agent.callTools()` |
302
+ | `Agent.parseFunctions()` | `Agent.parseTools()` |
303
+ | `Model.promptFromFunctions()` | `Model.promptFromTools()` |
304
+ | `Symposium.extractFunctionsFromResponse()` | `Symposium.extractToolCallsFromResponse()` |
305
+
306
+ ### Provider signature
307
+
308
+ ```javascript
309
+ // before
310
+ const parsed = this.parseOptions(options, functions);
311
+
312
+ // after
313
+ const parsed = this.parseOptions(options, tools);
314
+ ```
315
+
316
+ `parseOptions()` now returns `{options, tools}` (was `{options, functions}`).
317
+
318
+ ### Options
319
+
320
+ | 2.x | 3.0 |
321
+ |----------------------------------|------------------------------|
322
+ | `options.functions: [...]` | `options.tools: [...]` |
323
+ | `options.force_function: 'name'` | `options.force_tool: 'name'` |
324
+
325
+ ### Internal Message content block types
326
+
327
+ If you build `Message` objects by hand or inspect a thread's history, the content-block tags changed:
328
+
329
+ | 2.x | 3.0 |
330
+ |--------------------------------------------------------------|--------------------------------------------------------------|
331
+ | `{type: 'function', content: [{id, name, arguments}, ...]}` | `{type: 'tool_call', content: [{id, name, arguments}, ...]}` |
332
+ | `{type: 'function_response', content: {name, id, response}}` | `{type: 'tool_result', content: {name, id, response}}` |
333
+
334
+ Provider wire formats (e.g. OpenAI's `{type: 'function', function: {...}}` tool-definition shape and `tool_calls[].type = 'function'`) are unchanged — those are the providers' contract, not Symposium's.
335
+
336
+ ### Event payload key
337
+
338
+ ```javascript
339
+ // before
340
+ if (ev.type === 'tools_auth') {
341
+ const decision = await askUser(ev.functions);
342
+ input.send({ type: 'auth', id: ev.id, decision });
343
+ }
344
+
345
+ // after
346
+ if (ev.type === 'tools_auth') {
347
+ const decision = await askUser(ev.tools);
348
+ input.send({ type: 'auth', id: ev.id, decision });
349
+ }
350
+ ```
351
+
352
+ ## C. Mechanical migration
353
+
354
+ For most consumers, a project-wide find-and-replace covers it:
355
+
356
+ ```
357
+ Tool → Toolkit (class references, imports — careful with the literal string "tool")
358
+ extends Tool → extends Toolkit
359
+ GetContextTool → GetContextToolkit
360
+ addTool → addToolkit
361
+ getFunctions → getTools
362
+ callFunction → callTool
363
+ parseFunctions → parseTools
364
+ promptFromFunctions → promptFromTools
365
+ force_function → force_tool
366
+ type: 'function' → type: 'tool_call' (only inside Symposium Message content)
367
+ type: 'function_response' → type: 'tool_result' (only inside Symposium Message content)
368
+ ev.functions → ev.tools (only on tools_auth events)
369
+ ```
package/Model.js CHANGED
@@ -7,7 +7,14 @@ export default class Model {
7
7
  return new Map();
8
8
  }
9
9
 
10
- async generate(model, thread, functions = [], options = {}) {
10
+ async *generate(model, thread, tools = [], options = {}) {
11
+ // Subclasses implement as async generator: yield deltas during streaming,
12
+ // return assembled Message[] when complete.
13
+ // Delta shape:
14
+ // {type: 'text_delta', content: string}
15
+ // {type: 'reasoning_delta', content: string}
16
+ // {type: 'tool_call', content: {id?, name, arguments}}
17
+ // {type: 'image', content: <image-block-content>, meta}
11
18
  return null;
12
19
  }
13
20
 
@@ -15,55 +22,55 @@ export default class Model {
15
22
  throw new Error('countTokens not implemented in this model');
16
23
  }
17
24
 
18
- parseOptions(options = {}, functions = []) {
25
+ parseOptions(options = {}, tools = []) {
19
26
  options = {
20
- functions: null,
21
- force_function: null,
27
+ tools: null,
28
+ force_tool: null,
22
29
  ...options,
23
30
  };
24
31
 
25
- if (options.functions !== null)
26
- functions = options.functions;
32
+ if (options.tools !== null)
33
+ tools = options.tools;
27
34
 
28
- if (options.force_function && !functions.find(f => f.name === options.force_function))
29
- throw new Error('Function ' + options.force_function + ' not found.');
35
+ if (options.force_tool && !tools.find(t => t.name === options.force_tool))
36
+ throw new Error('Tool ' + options.force_tool + ' not found.');
30
37
 
31
- return {options, functions};
38
+ return {options, tools};
32
39
  }
33
40
 
34
- promptFromFunctions(options, functions) {
35
- if (options.force_function)
36
- functions = functions.filter(f => f.name !== options.force_function);
41
+ promptFromTools(options, tools) {
42
+ if (options.force_tool)
43
+ tools = tools.filter(t => t.name !== options.force_tool);
37
44
 
38
- if (!functions.length)
45
+ if (!tools.length)
39
46
  return '';
40
47
 
41
48
  let message;
42
- if (options.force_function) {
49
+ if (options.force_tool) {
43
50
  message = "Nella prossima risposta, rispondi UNICAMENTE seguendo le seguenti istruzioni:\n";
44
- message += functions[0].description + "\n";
45
- delete functions[0].description;
46
- message += "Rispondi con un messaggio che inizia con le parole:\nCALL " + options.force_function + "\nE poi a capo un oggetto JSON che segue queste direttive OpenAPI:\n";
51
+ message += tools[0].description + "\n";
52
+ delete tools[0].description;
53
+ message += "Rispondi con un messaggio che inizia con le parole:\nCALL " + options.force_tool + "\nE poi a capo un oggetto JSON che segue queste direttive OpenAPI:\n";
47
54
  } else {
48
- message = "Hai a disposizione alcune funzioni che puoi chiamare per ottenere risposte o compiere azioni. Ricorda che devi attendere la risposta dalla funzione per sapere se ha avuto successo. Per chiamare una funzione scrivi un messaggio che inizia con CALL nome_funzione e a capo inserisci il JSON con gli argomenti; delimitando il tutto da 3 caratteri ``` - ad esempio:\n" +
55
+ message = "Hai a disposizione alcuni strumenti che puoi chiamare per ottenere risposte o compiere azioni. Ricorda che devi attendere la risposta dello strumento per sapere se ha avuto successo. Per chiamare uno strumento scrivi un messaggio che inizia con CALL nome_strumento e a capo inserisci il JSON con gli argomenti; delimitando il tutto da 3 caratteri ``` - ad esempio:\n" +
49
56
  "```\n" +
50
57
  "CALL create_user\n" +
51
58
  '{"name":"test"}' + "\n" +
52
59
  "```\n\n" +
53
- "Lista delle funzioni che hai a disposizione:\n";
60
+ "Lista degli strumenti che hai a disposizione:\n";
54
61
 
55
- for (let f of functions)
56
- message += '- ' + f.name + "\n " + f.description + "\n";
62
+ for (let t of tools)
63
+ message += '- ' + t.name + "\n " + t.description + "\n";
57
64
  }
58
65
 
59
66
  message += "\nOpenAPI specs:\n\n";
60
- for (let f of functions) {
61
- if (!f.parameters)
67
+ for (let t of tools) {
68
+ if (!t.parameters)
62
69
  continue;
63
- message += '=== ' + f.name + " ===\n" + JSON.stringify(f.parameters.properties) + "\n\n";
70
+ message += '=== ' + t.name + " ===\n" + JSON.stringify(t.parameters.properties) + "\n\n";
64
71
  }
65
72
 
66
- if (options.force_function)
73
+ if (options.force_tool)
67
74
  message += "\nNella risposta non deve esserci NIENTE ALTRO se non queste due cose, non saranno prese in considerazione dal sistema altro genere di risposte.";
68
75
 
69
76
  return message;
@@ -47,19 +47,19 @@ export default class AnthropicModel extends Model {
47
47
  return this.anthropic;
48
48
  }
49
49
 
50
- async generate(model, thread, functions = [], options = {}) {
50
+ async *generate(model, thread, tools = [], options = {}) {
51
51
  try {
52
- const parsed = this.parseOptions(options, functions);
52
+ const parsed = this.parseOptions(options, tools);
53
53
  options = parsed.options;
54
- functions = parsed.functions;
54
+ tools = parsed.tools;
55
55
 
56
56
  let [system, messages] = await this.convertMessages(thread);
57
57
 
58
- if (functions.length && !model.tools) {
59
- // Se il modello non supporta nativamente le funzioni, aggiungo il prompt al messaggio di sistema
60
- const functions_prompt = this.promptFromFunctions(options, functions);
61
- system += "\n\n" + functions_prompt;
62
- functions = [];
58
+ if (tools.length && !model.tools) {
59
+ // Se il modello non supporta nativamente gli strumenti, aggiungo il prompt al messaggio di sistema
60
+ const tools_prompt = this.promptFromTools(options, tools);
61
+ system += "\n\n" + tools_prompt;
62
+ tools = [];
63
63
  }
64
64
 
65
65
  const completion_payload = {
@@ -72,25 +72,71 @@ export default class AnthropicModel extends Model {
72
72
  },
73
73
  betas: ["interleaved-thinking-2025-05-14"],
74
74
  messages,
75
- tools: functions.map(f => ({
76
- name: f.name,
77
- description: f.description,
78
- input_schema: f.parameters,
79
- required: f.required || undefined,
75
+ tools: tools.map(t => ({
76
+ name: t.name,
77
+ description: t.description,
78
+ input_schema: t.parameters,
79
+ required: t.required || undefined,
80
80
  })),
81
81
  };
82
82
 
83
- if (options.force_function) {
83
+ if (options.force_tool) {
84
84
  completion_payload.tool_choice = {
85
85
  type: 'tool',
86
- name: options.force_function,
86
+ name: options.force_tool,
87
87
  };
88
88
 
89
89
  delete completion_payload.thinking;
90
90
  delete completion_payload.betas;
91
91
  }
92
92
 
93
- const message = await this.getAnthropic().beta.messages.create(completion_payload);
93
+ const stream = this.getAnthropic().beta.messages.stream(completion_payload);
94
+
95
+ const toolBuffers = new Map();
96
+
97
+ for await (const event of stream) {
98
+ switch (event.type) {
99
+ case 'content_block_start':
100
+ if (event.content_block?.type === 'tool_use') {
101
+ toolBuffers.set(event.index, {
102
+ id: event.content_block.id,
103
+ name: event.content_block.name,
104
+ arguments: '',
105
+ });
106
+ }
107
+ break;
108
+
109
+ case 'content_block_delta':
110
+ if (event.delta?.type === 'text_delta' && event.delta.text)
111
+ yield {type: 'text_delta', content: event.delta.text};
112
+ else if (event.delta?.type === 'thinking_delta' && event.delta.thinking)
113
+ yield {type: 'reasoning_delta', content: event.delta.thinking};
114
+ else if (event.delta?.type === 'input_json_delta') {
115
+ const buf = toolBuffers.get(event.index);
116
+ if (buf)
117
+ buf.arguments += event.delta.partial_json || '';
118
+ }
119
+ break;
120
+
121
+ case 'content_block_stop': {
122
+ const buf = toolBuffers.get(event.index);
123
+ if (buf) {
124
+ yield {
125
+ type: 'tool_call',
126
+ content: {
127
+ id: buf.id,
128
+ name: buf.name,
129
+ arguments: buf.arguments ? JSON.parse(buf.arguments) : {},
130
+ },
131
+ };
132
+ toolBuffers.delete(event.index);
133
+ }
134
+ break;
135
+ }
136
+ }
137
+ }
138
+
139
+ const message = await stream.finalMessage();
94
140
 
95
141
  const message_content = [];
96
142
  if (message.content) {
@@ -102,7 +148,7 @@ export default class AnthropicModel extends Model {
102
148
 
103
149
  case 'tool_use':
104
150
  message_content.push({
105
- type: 'function',
151
+ type: 'tool_call',
106
152
  content: [
107
153
  {
108
154
  id: m.id,
@@ -135,7 +181,7 @@ export default class AnthropicModel extends Model {
135
181
  console.warn('Rate limite exceeded for Anthropic API, waiting 60 seconds...');
136
182
  await new Promise(resolve => setTimeout(resolve, 60000));
137
183
  if ((options.counter || 0) < 3)
138
- return this.generate(model, thread, functions, {...options, counter: (options.counter || 0) + 1});
184
+ return yield* this.generate(model, thread, tools, {...options, counter: (options.counter || 0) + 1});
139
185
  else
140
186
  throw new Error('Rate limit exceeded for Anthropic API, aborting.');
141
187
  }
@@ -160,7 +206,7 @@ export default class AnthropicModel extends Model {
160
206
  });
161
207
  break;
162
208
 
163
- case 'function':
209
+ case 'tool_call':
164
210
  content.push({
165
211
  type: 'tool_use',
166
212
  name: c.content[0].name,
@@ -169,7 +215,7 @@ export default class AnthropicModel extends Model {
169
215
  });
170
216
  break;
171
217
 
172
- case 'function_response':
218
+ case 'tool_result':
173
219
  content.push({
174
220
  type: 'tool_result',
175
221
  content: JSON.stringify(c.content.response),
@@ -38,9 +38,9 @@ export default class GrokModel extends OpenAIModel {
38
38
  return this.openai;
39
39
  }
40
40
 
41
- async generate(model, thread, functions = [], options = {}) {
41
+ async generate(model, thread, tools = [], options = {}) {
42
42
  if (options.image_generation) {
43
- functions.push({
43
+ tools.push({
44
44
  name: 'generate_image',
45
45
  description: 'Generate an image based on a detailed prompt that you provide',
46
46
  parameters: {
@@ -56,21 +56,21 @@ export default class GrokModel extends OpenAIModel {
56
56
  });
57
57
  }
58
58
 
59
- const response = await super.generate(model, thread, functions, options);
59
+ const response = await super.generate(model, thread, tools, options);
60
60
 
61
61
  // Check for image generation response
62
62
  if (options.image_generation) {
63
- const function_call = response[0].content.find(c => c.type === 'function');
64
- if (function_call) {
65
- const generation_call = function_call.content.find(f => f.name === 'generate_image');
63
+ const tool_call_block = response[0].content.find(c => c.type === 'tool_call');
64
+ if (tool_call_block) {
65
+ const generation_call = tool_call_block.content.find(t => t.name === 'generate_image');
66
66
  if (generation_call) {
67
67
  const response = await this.getOpenAi().images.generate({
68
68
  model: 'grok-imagine-image',
69
69
  prompt: generation_call.arguments.prompt,
70
70
  });
71
71
 
72
- function_call.type = 'image';
73
- function_call.content = {
72
+ tool_call_block.type = 'image';
73
+ tool_call_block.content = {
74
74
  type: 'url',
75
75
  mime: 'image/jpeg',
76
76
  data: response.data[0].url,