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/README.md
CHANGED
|
@@ -1,396 +1,458 @@
|
|
|
1
|
-
# Symposium
|
|
2
|
-
|
|
3
|
-
Symposium is a
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
-
|
|
41
|
-
-
|
|
42
|
-
-
|
|
43
|
-
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
])
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
###
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
```javascript
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
1
|
+
# Symposium
|
|
2
|
+
|
|
3
|
+
Symposium is a Node.js framework for building Large Language Model (LLM)-powered agents. It provides a structured, extensible architecture for creating complex AI systems with distinct behaviors, tools, and memory.
|
|
4
|
+
|
|
5
|
+
> **3.0 is a breaking release.** The old `EventEmitter` API is gone, replaced by async generators and streaming input channels. See [MIGRATION.md](./MIGRATION.md) for upgrade instructions.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Agent-Based Architecture**: Create multiple, specialized agents that can be extended with unique behaviors.
|
|
10
|
+
- **Model Agnostic**: Easily integrate with various LLM providers (OpenAI, Anthropic, Groq, DeepSeek, Grok, Ollama). A list of supported models is available in the `Models` folder.
|
|
11
|
+
- **Real streaming**: Model adapters use the underlying providers' streaming APIs and forward token deltas to consumers as they arrive.
|
|
12
|
+
- **Async generator API**: `agent.message(...)` returns an async iterable — consume it with `for await`.
|
|
13
|
+
- **Streaming input**: Push user messages, tool-authorization decisions, and control signals into a running agent via an input channel.
|
|
14
|
+
- **Tool integration**: Extend agents' capabilities with tools that the LLM can call.
|
|
15
|
+
- **Stateful conversations**: Manage conversational state and history through Threads.
|
|
16
|
+
- **Persistent memory**: Pluggable storage adapters allow for long-term memory.
|
|
17
|
+
- **Structured output**: Set `response_schema` on any agent (chat or utility) to constrain the final answer to a JSON schema.
|
|
18
|
+
- **Real-time sessions**: Built-in support for real-time voice conversations.
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
Requires Node.js v18 or higher.
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install symposium
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Configuration
|
|
29
|
+
|
|
30
|
+
Symposium uses environment variables to configure access to various services. You can set these in a `.env` file at the root of your project.
|
|
31
|
+
|
|
32
|
+
- `OPENAI_API_KEY`: Required for OpenAI models and real-time voice sessions.
|
|
33
|
+
- `ANTHROPIC_API_KEY`: Required for Anthropic models.
|
|
34
|
+
- `GROQ_API_KEY`: Required for Groq models.
|
|
35
|
+
- `DEEPSEEK_API_KEY`: Required for DeepSeek models.
|
|
36
|
+
- `TRANSCRIPTION_MODEL`, `EMBEDDING_MODEL`: Model labels routed to STT / embedding providers.
|
|
37
|
+
|
|
38
|
+
## Core Concepts
|
|
39
|
+
|
|
40
|
+
- **`Symposium`**: Static class that acts as the central hub. Responsible for loading models and initializing the storage adapter.
|
|
41
|
+
- **`Agent`**: The heart of the framework. Extend this class to define an agent's prompt, behavior, and tools.
|
|
42
|
+
- **`Thread`**: A single conversation with an agent. Maintains message history and per-conversation state.
|
|
43
|
+
- **`Toolkit`**: Base class for toolkits — groupings of one or more tools that an `Agent` can call.
|
|
44
|
+
- **`Message`**: A typed message inside a `Thread`.
|
|
45
|
+
- **`ContextHandler`** / **`Summarizer`**: Pre-execute hooks for managing long-context strategies.
|
|
46
|
+
- **`createInputChannel`**: Helper that creates an `AsyncIterable` with `send(item)` / `close()` for streaming input into an agent.
|
|
47
|
+
|
|
48
|
+
## Getting Started
|
|
49
|
+
|
|
50
|
+
### 1. Initialize Symposium
|
|
51
|
+
|
|
52
|
+
```javascript
|
|
53
|
+
import { Symposium } from 'symposium';
|
|
54
|
+
|
|
55
|
+
await Symposium.init(); // optional: pass a storage adapter
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### 2. One-shot prompts
|
|
59
|
+
|
|
60
|
+
`Symposium.prompt(system, prompt, options)` is a shortcut that spins up a bare utility agent and resolves directly to the final value.
|
|
61
|
+
|
|
62
|
+
```javascript
|
|
63
|
+
import { Symposium } from 'symposium';
|
|
64
|
+
await Symposium.init();
|
|
65
|
+
|
|
66
|
+
const reply = await Symposium.prompt(
|
|
67
|
+
'Translate from English to French.',
|
|
68
|
+
'Hello, how are you?',
|
|
69
|
+
);
|
|
70
|
+
console.log(reply); // "Bonjour, comment ça va ?"
|
|
71
|
+
|
|
72
|
+
// With structured output:
|
|
73
|
+
const data = await Symposium.prompt(
|
|
74
|
+
'Extract name and emails from the following text',
|
|
75
|
+
email_text,
|
|
76
|
+
{
|
|
77
|
+
response_schema: {
|
|
78
|
+
type: 'array',
|
|
79
|
+
items: {
|
|
80
|
+
type: 'object',
|
|
81
|
+
properties: {
|
|
82
|
+
name: { type: 'string' },
|
|
83
|
+
email: { type: 'string' },
|
|
84
|
+
},
|
|
85
|
+
required: ['name', 'email'],
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
);
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### 3. Create your Agent
|
|
93
|
+
|
|
94
|
+
```javascript
|
|
95
|
+
// MyChatAgent.js
|
|
96
|
+
import { Agent } from 'symposium';
|
|
97
|
+
|
|
98
|
+
export default class MyChatAgent extends Agent {
|
|
99
|
+
name = 'MyChatAgent';
|
|
100
|
+
description = 'A simple chat agent.';
|
|
101
|
+
|
|
102
|
+
async doInitThread(thread) {
|
|
103
|
+
await thread.addMessage('system', 'You are a helpful assistant.');
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### 4. Start a Conversation
|
|
109
|
+
|
|
110
|
+
`agent.message()` returns an async generator. Consume it with `for await`.
|
|
111
|
+
|
|
112
|
+
```javascript
|
|
113
|
+
import { Symposium } from 'symposium';
|
|
114
|
+
import MyChatAgent from './MyChatAgent.js';
|
|
115
|
+
|
|
116
|
+
await Symposium.init();
|
|
117
|
+
|
|
118
|
+
const agent = new MyChatAgent();
|
|
119
|
+
await agent.init();
|
|
120
|
+
|
|
121
|
+
for await (const ev of agent.message('Hello, who are you?')) {
|
|
122
|
+
switch (ev.type) {
|
|
123
|
+
case 'chunk':
|
|
124
|
+
process.stdout.write(ev.content); // streamed text delta
|
|
125
|
+
break;
|
|
126
|
+
|
|
127
|
+
case 'output':
|
|
128
|
+
// Final assembled content block for this assistant turn.
|
|
129
|
+
// ev.content is a typed block ({type:'text'|'image', ...}).
|
|
130
|
+
break;
|
|
131
|
+
|
|
132
|
+
case 'reasoning':
|
|
133
|
+
// Reasoning text from models that emit it (o-series, Claude thinking, etc.).
|
|
134
|
+
break;
|
|
135
|
+
|
|
136
|
+
case 'tool':
|
|
137
|
+
console.log(`\n> Using tool: ${ev.name}(${JSON.stringify(ev.arguments)})`);
|
|
138
|
+
break;
|
|
139
|
+
|
|
140
|
+
case 'tool_response':
|
|
141
|
+
if (ev.success)
|
|
142
|
+
console.log(`> ${ev.name} OK: ${JSON.stringify(ev.response)}`);
|
|
143
|
+
else
|
|
144
|
+
console.log(`> ${ev.name} FAILED: ${ev.error}`);
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
#### Event reference
|
|
151
|
+
|
|
152
|
+
All events yielded from the generator:
|
|
153
|
+
|
|
154
|
+
| Event | Payload | Notes |
|
|
155
|
+
|---|---|---|
|
|
156
|
+
| `start` | `{thread}` | First yield. |
|
|
157
|
+
| `chunk` | `{content}` | Streamed text delta — concatenate to render incrementally. |
|
|
158
|
+
| `output` | `{content}` | Final assembled content block (`text` / `image`) for one assistant message. |
|
|
159
|
+
| `reasoning` | `{content}` | Reasoning text. |
|
|
160
|
+
| `tool` | `{id, name, arguments}` | Emitted before invoking a tool. |
|
|
161
|
+
| `tool_response` | `{name, success, response?, error?}` | Emitted after the tool returns or throws. |
|
|
162
|
+
| `tools_auth` | `{id, tools}` | Yielded when authorization is required — see below. |
|
|
163
|
+
| `retry` | `{attempt, reason}` | Only when an error occurs *after* at least one chunk has already streamed for the current turn. |
|
|
164
|
+
| `result` | `{value}` | Only when `response_schema` is set — parsed structured answer. |
|
|
165
|
+
| `end` | `{thread}` | Always yielded last, even on throw. |
|
|
166
|
+
|
|
167
|
+
Errors throw out of the generator. There is no `error` event.
|
|
168
|
+
|
|
169
|
+
## Advanced Usage
|
|
170
|
+
|
|
171
|
+
### Using Tools
|
|
172
|
+
|
|
173
|
+
Tools allow your agent to interact with the outside world. Extend `Toolkit` and expose one or more tools.
|
|
174
|
+
|
|
175
|
+
```javascript
|
|
176
|
+
// WeatherToolkit.js
|
|
177
|
+
import { Toolkit } from 'symposium';
|
|
178
|
+
|
|
179
|
+
export default class WeatherToolkit extends Toolkit {
|
|
180
|
+
name = 'WeatherToolkit';
|
|
181
|
+
|
|
182
|
+
async getTools() {
|
|
183
|
+
return [{
|
|
184
|
+
name: 'get_weather',
|
|
185
|
+
description: 'Get the current weather for a specific city',
|
|
186
|
+
parameters: {
|
|
187
|
+
type: 'object',
|
|
188
|
+
properties: {
|
|
189
|
+
city: { type: 'string', description: 'The city name' },
|
|
190
|
+
},
|
|
191
|
+
required: ['city'],
|
|
192
|
+
},
|
|
193
|
+
}];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async callTool(thread, name, payload) {
|
|
197
|
+
if (name === 'get_weather')
|
|
198
|
+
return { temperature: '25°C', condition: 'sunny' };
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Add the toolkit to your agent:
|
|
204
|
+
|
|
205
|
+
```javascript
|
|
206
|
+
const agent = new MyChatAgent();
|
|
207
|
+
await agent.addToolkit(new WeatherToolkit());
|
|
208
|
+
await agent.init();
|
|
209
|
+
|
|
210
|
+
for await (const ev of agent.message("What's the weather in Paris?")) {
|
|
211
|
+
// ...
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Tools within a single LLM turn are executed sequentially, in the order the model requested them, so the event stream is fully deterministic.
|
|
216
|
+
|
|
217
|
+
### Tool Authorization
|
|
218
|
+
|
|
219
|
+
To require explicit approval for a tool, override `Toolkit.authorize()`. When it returns `false`, the agent yields a `tools_auth` event and suspends. The consumer resumes the run by sending an `auth` control message through the input channel.
|
|
220
|
+
|
|
221
|
+
```javascript
|
|
222
|
+
import { Toolkit } from 'symposium';
|
|
223
|
+
|
|
224
|
+
class DangerousToolkit extends Toolkit {
|
|
225
|
+
async authorize(thread, name, payload) {
|
|
226
|
+
return false; // always ask
|
|
227
|
+
}
|
|
228
|
+
async authorizeAlways(thread, name, payload) {
|
|
229
|
+
// Persist an "always approve" decision somewhere (DB, file, etc.).
|
|
230
|
+
}
|
|
231
|
+
// ... getTools / callTool
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
```javascript
|
|
236
|
+
import { createInputChannel } from 'symposium';
|
|
237
|
+
|
|
238
|
+
const input = createInputChannel();
|
|
239
|
+
input.send('Please run that risky operation');
|
|
240
|
+
|
|
241
|
+
for await (const ev of agent.message(input)) {
|
|
242
|
+
if (ev.type === 'tools_auth') {
|
|
243
|
+
const decision = await askUser(ev.tools); // 'approve' | 'approve_always' | 'reject'
|
|
244
|
+
input.send({ type: 'auth', id: ev.id, decision });
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
`'approve_always'` calls `tool.authorizeAlways()` on each pending tool so the decision is persisted. If the input channel closes while a `tools_auth` is pending, the decision is treated as `'reject'` and the run is cancelled. If you call `agent.message()` with a plain string (no channel), any auth request auto-rejects, since there is no way to deliver a decision.
|
|
250
|
+
|
|
251
|
+
### Streaming Input
|
|
252
|
+
|
|
253
|
+
`agent.message()` accepts three input shapes:
|
|
254
|
+
|
|
255
|
+
1. a plain `string`,
|
|
256
|
+
2. a `ContentBlock[]` (e.g. text + image),
|
|
257
|
+
3. an `AsyncIterable<string | ContentBlock | ContentBlock[] | ControlMessage>`.
|
|
258
|
+
|
|
259
|
+
The first two behave traditionally — one user turn, one model loop, done. An async iterable enables **streaming input**: keep pushing messages into the agent at any time.
|
|
260
|
+
|
|
261
|
+
```javascript
|
|
262
|
+
import { createInputChannel } from 'symposium';
|
|
263
|
+
|
|
264
|
+
const input = createInputChannel();
|
|
265
|
+
input.send('Plan a trip to Rome');
|
|
266
|
+
|
|
267
|
+
// Concurrently, from elsewhere:
|
|
268
|
+
setTimeout(() => input.send('Actually, make it Florence instead'), 2000);
|
|
269
|
+
|
|
270
|
+
for await (const ev of agent.message(input)) {
|
|
271
|
+
if (ev.type === 'chunk')
|
|
272
|
+
process.stdout.write(ev.content);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// When you're done, close the channel to end the run:
|
|
276
|
+
input.close();
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
Behavior with a channel:
|
|
280
|
+
|
|
281
|
+
- The agent drains incoming items into the initial user message and starts the first model turn once content has arrived (or once a `{type:'submit'}` control message lands).
|
|
282
|
+
- New items pushed during a turn are queued and inserted as a new `user` message at the next inter-turn boundary (after the current tool batch finishes — there is no mid-turn cancellation).
|
|
283
|
+
- The run keeps going across multiple turns until the channel closes, or a `{type:'cancel'}` control message is sent.
|
|
284
|
+
|
|
285
|
+
Control messages accepted on the channel:
|
|
286
|
+
|
|
287
|
+
```js
|
|
288
|
+
{ type: 'auth', id, decision: 'approve' | 'approve_always' | 'reject' }
|
|
289
|
+
{ type: 'submit' } // closes the initial user-message build-up
|
|
290
|
+
{ type: 'cancel' } // gracefully stops the agent loop after the in-flight turn
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### Structured Output
|
|
294
|
+
|
|
295
|
+
`response_schema` is independent of the agent type — set it on either a chat or utility agent to constrain the final answer.
|
|
296
|
+
|
|
297
|
+
**Utility agent** — `await agent.message(...)` resolves directly to the parsed value:
|
|
298
|
+
|
|
299
|
+
```javascript
|
|
300
|
+
// TextExtractorAgent.js
|
|
301
|
+
import { Agent } from 'symposium';
|
|
302
|
+
|
|
303
|
+
export default class TextExtractorAgent extends Agent {
|
|
304
|
+
name = 'TextExtractorAgent';
|
|
305
|
+
type = 'utility';
|
|
306
|
+
response_schema = {
|
|
307
|
+
type: 'object',
|
|
308
|
+
properties: {
|
|
309
|
+
name: { type: 'string' },
|
|
310
|
+
email: { type: 'string' },
|
|
311
|
+
},
|
|
312
|
+
required: ['name', 'email'],
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
async doInitThread(thread) {
|
|
316
|
+
await thread.addMessage('system', 'Extract the name and email from the text.');
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const extractor = new TextExtractorAgent();
|
|
321
|
+
await extractor.init();
|
|
322
|
+
const result = await extractor.message('My name is John Doe, john.doe@example.com');
|
|
323
|
+
console.log(result); // { name: 'John Doe', email: 'john.doe@example.com' }
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
**Chat agent with structured final answer** — events stream normally; a final `{type:'result', value}` event carries the parsed object just before `end`:
|
|
327
|
+
|
|
328
|
+
```javascript
|
|
329
|
+
const agent = new MyChatAgent();
|
|
330
|
+
agent.response_schema = {
|
|
331
|
+
type: 'object',
|
|
332
|
+
properties: { city: { type: 'string' } },
|
|
333
|
+
required: ['city'],
|
|
334
|
+
};
|
|
335
|
+
await agent.init();
|
|
336
|
+
|
|
337
|
+
for await (const ev of agent.message('Look up the weather and reply in JSON')) {
|
|
338
|
+
if (ev.type === 'result')
|
|
339
|
+
console.log(ev.value); // { city: '...' }
|
|
340
|
+
}
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
Internally, structured-output-capable OpenAI models use `response_format: json_schema`; otherwise the agent falls back to a forced function call and parses its arguments.
|
|
344
|
+
|
|
345
|
+
### Real-time Voice and Transcription
|
|
346
|
+
|
|
347
|
+
Symposium has built-in support for audio transcription and real-time voice sessions, currently powered by OpenAI.
|
|
348
|
+
|
|
349
|
+
```javascript
|
|
350
|
+
// Inline audio in a message — automatically transcribed if the model doesn't accept audio:
|
|
351
|
+
for await (const ev of agent.message([
|
|
352
|
+
{
|
|
353
|
+
type: 'audio',
|
|
354
|
+
content: { type: 'url', data: 'http://example.com/audio.mp3' },
|
|
355
|
+
},
|
|
356
|
+
])) {
|
|
357
|
+
// ...
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Standalone transcription:
|
|
361
|
+
const text = await Symposium.transcribe(audio_buffer);
|
|
362
|
+
|
|
363
|
+
// Real-time voice session:
|
|
364
|
+
const { response, thread } = await agent.createRealtimeSession();
|
|
365
|
+
const sessionId = response.id;
|
|
366
|
+
const clientSecret = response.client_secret.value;
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
### Switching Models
|
|
370
|
+
|
|
371
|
+
```javascript
|
|
372
|
+
class MyAgent extends Agent {
|
|
373
|
+
default_model = 'claude-3-5-sonnet';
|
|
374
|
+
// ...
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const thread = await agent.getThread('thread-id');
|
|
378
|
+
await agent.setModel(thread, 'gpt-5');
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
### Persistence
|
|
382
|
+
|
|
383
|
+
Provide a storage adapter implementing `init()`, `get(key)`, `set(key, value)`:
|
|
384
|
+
|
|
385
|
+
```javascript
|
|
386
|
+
import fs from 'fs/promises';
|
|
387
|
+
|
|
388
|
+
class FileStorage {
|
|
389
|
+
async init() {
|
|
390
|
+
await fs.mkdir('./storage', { recursive: true });
|
|
391
|
+
}
|
|
392
|
+
async get(key) {
|
|
393
|
+
try {
|
|
394
|
+
return JSON.parse(await fs.readFile(`./storage/${key}.json`, 'utf-8'));
|
|
395
|
+
} catch {
|
|
396
|
+
return null;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
async set(key, value) {
|
|
400
|
+
await fs.writeFile(`./storage/${key}.json`, JSON.stringify(value, null, 2));
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
await Symposium.init(new FileStorage());
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
### Retries
|
|
408
|
+
|
|
409
|
+
The agent retries each turn up to `max_retries` (default 5) times on transport / model errors. The retry strategy is hybrid:
|
|
410
|
+
|
|
411
|
+
- If no `chunk` has been streamed yet for the current turn, the retry is **silent** (no consumer-visible event).
|
|
412
|
+
- If at least one `chunk` has already been streamed, the agent yields `{type:'retry', attempt, reason}` before retrying so consumers can react (e.g. show a spinner, clear partial output).
|
|
413
|
+
|
|
414
|
+
Errors during *tool* execution are not retried — they're surfaced as `{type:'tool_response', success:false, error}`.
|
|
415
|
+
|
|
416
|
+
## API Reference
|
|
417
|
+
|
|
418
|
+
High-level overview — see source for full details.
|
|
419
|
+
|
|
420
|
+
### `Agent`
|
|
421
|
+
|
|
422
|
+
- `constructor(options)` — Optional `memory_handler` and `logger`.
|
|
423
|
+
- `init()` — Must be called before use.
|
|
424
|
+
- `addToolkit(toolkit)` — Add a `Toolkit` instance.
|
|
425
|
+
- `message(content, thread)` — Send a message. Returns an async generator for chat agents; resolves to the parsed value (Promise) for utility agents.
|
|
426
|
+
- `getThread(id)` — Retrieve a `Thread` instance.
|
|
427
|
+
- `setModel(thread, modelLabel)` — Change the LLM for a thread.
|
|
428
|
+
- `createRealtimeSession(thread_id, options)` — Create a real-time voice session.
|
|
429
|
+
|
|
430
|
+
### `Thread`
|
|
431
|
+
|
|
432
|
+
- `constructor(id, agent)`
|
|
433
|
+
- `addMessage(role, content, name, tags)`
|
|
434
|
+
- `setState(state, save)`
|
|
435
|
+
- `loadState()` / `storeState()`
|
|
436
|
+
|
|
437
|
+
### `Toolkit`
|
|
438
|
+
|
|
439
|
+
A `Toolkit` groups one or more LLM-callable tools. Extend it to publish your own; `MCPServer` is itself a `Toolkit` that exposes whatever tools the remote MCP server lists.
|
|
440
|
+
|
|
441
|
+
- `getTools()` — Abstract. Return an array of tool definitions for the LLM.
|
|
442
|
+
- `callTool(thread, name, payload)` — Abstract. Called when the LLM invokes one of the tools.
|
|
443
|
+
- `authorize(thread, name, payload)` — Optional. Return `false` to require explicit consumer approval (`tools_auth` event).
|
|
444
|
+
- `authorizeAlways(thread, name, payload)` — Optional. Called when the consumer responds with `'approve_always'`.
|
|
445
|
+
|
|
446
|
+
### `createInputChannel()`
|
|
447
|
+
|
|
448
|
+
Returns `{ send(item), close(), [Symbol.asyncIterator]() }`. Push strings, content blocks, or control messages from anywhere; iterate the channel from `agent.message()`.
|
|
449
|
+
|
|
450
|
+
### Other Classes
|
|
451
|
+
|
|
452
|
+
- **`ContextHandler`**: Base for managing long-term context.
|
|
453
|
+
- **`Summarizer`**: Utility agent that compresses old messages once a thread crosses a token threshold.
|
|
454
|
+
- **`Logger`**: Simple per-agent logger.
|
|
455
|
+
|
|
456
|
+
## License
|
|
457
|
+
|
|
458
|
+
ISC
|