@rrr2010/opencode-roundtable 0.2.0 → 0.3.2
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/README.md +244 -198
- package/dist/index.js +139 -100
- package/dist/index.js.map +8 -8
- package/docs/SPEC.md +974 -979
- package/package.json +60 -53
- package/src/tui/tui.tsx +89 -0
package/docs/SPEC.md
CHANGED
|
@@ -1,979 +1,974 @@
|
|
|
1
|
-
# Roundtable Plugin — Technical Spec
|
|
2
|
-
|
|
3
|
-
OpenCode plugin that orchestrates **multi-agent round-robin debates** with
|
|
4
|
-
isolated sessions, shared context between debaters, a built-in observer
|
|
5
|
-
(with override), and extension support.
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
## Table of Contents
|
|
10
|
-
|
|
11
|
-
- [1. Philosophy](#1-philosophy)
|
|
12
|
-
- [2. Architecture](#2-architecture)
|
|
13
|
-
- [3. Tool API](#3-tool-api)
|
|
14
|
-
- [4. Lifecycle](#4-lifecycle)
|
|
15
|
-
- [5. States](#5-states)
|
|
16
|
-
- [6. History & Context](#6-history--context)
|
|
17
|
-
- [7. Edge Case Handling](#7-edge-case-handling)
|
|
18
|
-
- [8. Observer](#8-observer)
|
|
19
|
-
- [9. Extend Mode](#9-extend-mode)
|
|
20
|
-
- [10.
|
|
21
|
-
- [11. Plugin Structure](#11-plugin-structure)
|
|
22
|
-
- [12. Tests & Validation](#12-tests--validation)
|
|
23
|
-
- [13. Glossary](#13-glossary)
|
|
24
|
-
- [14. References](#14-references)
|
|
25
|
-
|
|
26
|
-
---
|
|
27
|
-
|
|
28
|
-
## 1. Philosophy
|
|
29
|
-
|
|
30
|
-
A roundtable simulates a **panel discussion** where experts from different
|
|
31
|
-
areas debate a topic. Each participant keeps their own personality (system
|
|
32
|
-
prompt, tools, temperature, color) but shares the same discussion history.
|
|
33
|
-
|
|
34
|
-
### Principles
|
|
35
|
-
|
|
36
|
-
1. **Clean slate on entry** — the roundtable session starts fresh, without
|
|
37
|
-
baggage from the original conversation.
|
|
38
|
-
2. **Shared context between debaters** — everyone sees the same discussion
|
|
39
|
-
history, including previous tool calls and outputs.
|
|
40
|
-
3. **Isolated personalities** — each agent keeps its own system prompt,
|
|
41
|
-
tools, and configuration. No "personality merging".
|
|
42
|
-
4. **Separate orchestrator** — whoever invokes the roundtable does not
|
|
43
|
-
participate; they only receive the result at the end.
|
|
44
|
-
5. **Default observer** — the plugin has a built-in observer that always
|
|
45
|
-
consolidates the debate at the end. The orchestrator may override it
|
|
46
|
-
with a specific agent.
|
|
47
|
-
6. **Extensible** — roundtables can be continued with additional rounds.
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
│
|
|
57
|
-
│
|
|
58
|
-
│
|
|
59
|
-
│
|
|
60
|
-
│ │
|
|
61
|
-
│
|
|
62
|
-
│
|
|
63
|
-
│
|
|
64
|
-
│ │
|
|
65
|
-
│ │
|
|
66
|
-
│ │ [
|
|
67
|
-
│ │ [agent:
|
|
68
|
-
│ │ [agent:
|
|
69
|
-
│ │
|
|
70
|
-
│ │
|
|
71
|
-
│ │ [
|
|
72
|
-
│ │
|
|
73
|
-
│
|
|
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
|
-
an error
|
|
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
|
-
|
|
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
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
###
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
agent's
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
→
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
-
|
|
503
|
-
|
|
504
|
-
-
|
|
505
|
-
|
|
506
|
-
**
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
-
|
|
526
|
-
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
the
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
###
|
|
606
|
-
|
|
607
|
-
If the orchestrator
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
1.
|
|
616
|
-
2.
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
```
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
###
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
###
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
###
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
- [ ]
|
|
930
|
-
- [ ]
|
|
931
|
-
- [ ]
|
|
932
|
-
- [ ]
|
|
933
|
-
- [ ]
|
|
934
|
-
- [ ]
|
|
935
|
-
- [ ]
|
|
936
|
-
- [ ]
|
|
937
|
-
- [ ]
|
|
938
|
-
- [ ]
|
|
939
|
-
- [ ]
|
|
940
|
-
- [ ]
|
|
941
|
-
- [ ]
|
|
942
|
-
- [ ]
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
|
953
|
-
|
|
954
|
-
| **
|
|
955
|
-
| **
|
|
956
|
-
| **
|
|
957
|
-
| **
|
|
958
|
-
| **
|
|
959
|
-
| **
|
|
960
|
-
| **
|
|
961
|
-
| **
|
|
962
|
-
| **
|
|
963
|
-
| **
|
|
964
|
-
| **
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
- [OpenCode
|
|
973
|
-
- [OpenCode
|
|
974
|
-
-
|
|
975
|
-
- [OpenCode SDK Types (GitHub)](https://github.com/anomalyco/opencode/tree/dev/packages/sdk/js/src)
|
|
976
|
-
- `opencode-sessions` (reference plugin):
|
|
977
|
-
`~/.cache/opencode/packages/opencode-sessions/node_modules/opencode-sessions/dist/index.js`
|
|
978
|
-
- `@opencode-ai/sdk` types:
|
|
979
|
-
`~/.cache/opencode/packages/opencode-sessions/node_modules/@opencode-ai/sdk/dist/gen/types.gen.d.ts`
|
|
1
|
+
# Roundtable Plugin — Technical Spec
|
|
2
|
+
|
|
3
|
+
OpenCode plugin that orchestrates **multi-agent round-robin debates** with
|
|
4
|
+
isolated sessions, shared context between debaters, a built-in observer
|
|
5
|
+
(with override), and extension support.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Table of Contents
|
|
10
|
+
|
|
11
|
+
- [1. Philosophy](#1-philosophy)
|
|
12
|
+
- [2. Architecture](#2-architecture)
|
|
13
|
+
- [3. Tool API](#3-tool-api)
|
|
14
|
+
- [4. Lifecycle](#4-lifecycle)
|
|
15
|
+
- [5. States & Persistence](#5-states--persistence)
|
|
16
|
+
- [6. History & Context](#6-history--context)
|
|
17
|
+
- [7. Edge Case Handling](#7-edge-case-handling)
|
|
18
|
+
- [8. Observer](#8-observer)
|
|
19
|
+
- [9. Extend Mode](#9-extend-mode)
|
|
20
|
+
- [10. Navigation & TUI](#10-navigation--tui)
|
|
21
|
+
- [11. Plugin Structure](#11-plugin-structure)
|
|
22
|
+
- [12. Tests & Validation](#12-tests--validation)
|
|
23
|
+
- [13. Glossary](#13-glossary)
|
|
24
|
+
- [14. References](#14-references)
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## 1. Philosophy
|
|
29
|
+
|
|
30
|
+
A roundtable simulates a **panel discussion** where experts from different
|
|
31
|
+
areas debate a topic. Each participant keeps their own personality (system
|
|
32
|
+
prompt, tools, temperature, color) but shares the same discussion history.
|
|
33
|
+
|
|
34
|
+
### Principles
|
|
35
|
+
|
|
36
|
+
1. **Clean slate on entry** — the roundtable session starts fresh, without
|
|
37
|
+
baggage from the original conversation.
|
|
38
|
+
2. **Shared context between debaters** — everyone sees the same discussion
|
|
39
|
+
history, including previous tool calls and outputs.
|
|
40
|
+
3. **Isolated personalities** — each agent keeps its own system prompt,
|
|
41
|
+
tools, and configuration. No "personality merging".
|
|
42
|
+
4. **Separate orchestrator** — whoever invokes the roundtable does not
|
|
43
|
+
participate; they only receive the result at the end.
|
|
44
|
+
5. **Default observer** — the plugin has a built-in observer that always
|
|
45
|
+
consolidates the debate at the end. The orchestrator may override it
|
|
46
|
+
with a specific agent.
|
|
47
|
+
6. **Extensible** — roundtables can be continued with additional rounds.
|
|
48
|
+
7. **File persistence** — state survives restarts via JSON files on disk.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## 2. Architecture
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
┌──────────────────────────────────────────────────────────────────┐
|
|
56
|
+
│ MAIN SESSION (S1) │
|
|
57
|
+
│ ┌────────────────────────────────────────────────────────────┐ │
|
|
58
|
+
│ │ build/plan: roundtable({agents, prompt, rounds, observer?})│ │
|
|
59
|
+
│ └────────────────────────────────────────────────────────────┘ │
|
|
60
|
+
│ │ │
|
|
61
|
+
│ │ plugin creates │
|
|
62
|
+
│ ▼ │
|
|
63
|
+
│ ┌────────────────────────────────────────────────────────────┐ │
|
|
64
|
+
│ │ ROUNDTABLE SESSION (S2) — parentID: S1 │ │
|
|
65
|
+
│ │ │ │
|
|
66
|
+
│ │ [noReply] ⚙ Parent: #S1 │ │
|
|
67
|
+
│ │ [agent:PM] Round 1 — PM responds │ │
|
|
68
|
+
│ │ [agent:DEV] Round 1 — DEV responds (sees history) │ │
|
|
69
|
+
│ │ [agent:RV] Round 1 — RV responds │ │
|
|
70
|
+
│ │ ... N rounds ... │ │
|
|
71
|
+
│ │ [observer] Consolidated summary │ │
|
|
72
|
+
│ │ [noReply] ━━━ Roundtable Concluded ━━━ (delimiter) │ │
|
|
73
|
+
│ │ │ │
|
|
74
|
+
│ └────────────────────────────────────────────────────────────┘ │
|
|
75
|
+
│ │ │
|
|
76
|
+
│ │ plugin resolves pending promise with summary │
|
|
77
|
+
│ ▼ │
|
|
78
|
+
│ [noReply] ⚙ Roundtable started — #S2 • agents • N round(s) │
|
|
79
|
+
└──────────────────────────────────────────────────────────────────┘
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Session relationship
|
|
83
|
+
|
|
84
|
+
- **S1 (Main):** where the user interacts. The session that calls `roundtable()`.
|
|
85
|
+
- **S2 (Roundtable):** created via `session.create({ parentID: S1 })`. Child
|
|
86
|
+
sessions use SubagentFooter natively and are hidden from the sidebar.
|
|
87
|
+
- The plugin listens to events from BOTH: `session.idle` on S2 for sequencing,
|
|
88
|
+
`session.deleted` on S1 or S2 for cleanup.
|
|
89
|
+
|
|
90
|
+
### Execution model (event-driven)
|
|
91
|
+
|
|
92
|
+
The OpenCode V2 server operates **asynchronously by design**:
|
|
93
|
+
|
|
94
|
+
1. `session.prompt()` returns immediately with an `Admitted` record
|
|
95
|
+
2. The agent execution loop runs in the background
|
|
96
|
+
3. When processing finishes, a `session.idle { sessionID }` event is emitted
|
|
97
|
+
4. The plugin listens to these events to advance the round-robin
|
|
98
|
+
|
|
99
|
+
This makes the roundtable **native to OpenCode's model** — no polling or
|
|
100
|
+
blocking required.
|
|
101
|
+
|
|
102
|
+
### Why `session.create()` and not `session.fork()`?
|
|
103
|
+
|
|
104
|
+
`fork()` copies ALL history from the current session, including tool calls,
|
|
105
|
+
user conversation, etc. This pollutes the roundtable context.
|
|
106
|
+
|
|
107
|
+
`create()` starts from scratch. The plugin builds the debate history manually,
|
|
108
|
+
including only what matters.
|
|
109
|
+
|
|
110
|
+
### File-based persistence (not META blocks)
|
|
111
|
+
|
|
112
|
+
State is stored in JSON files at `~/.config/opencode/roundtable-states/<sessionID>.json`.
|
|
113
|
+
The plugin **no longer** uses `[ROUNDTABLE META]` message blocks in S2.
|
|
114
|
+
|
|
115
|
+
| Function | Purpose |
|
|
116
|
+
|----------|---------|
|
|
117
|
+
| `saveStateFile(state)` | Writes state to disk |
|
|
118
|
+
| `loadStateFile(sessionID)` | Reads state from disk (with validation) |
|
|
119
|
+
| `deleteStateFile(sessionID)` | Removes state file on cleanup |
|
|
120
|
+
| `listStateFiles()` | Lists all session IDs with state files |
|
|
121
|
+
|
|
122
|
+
On startup, the plugin runs `scanOrphanRoundtables()` which loads all state
|
|
123
|
+
files into the in-memory `states` Map.
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## 3. Tool API
|
|
128
|
+
|
|
129
|
+
### Roundtable tool
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
roundtable({
|
|
133
|
+
agents?: string[], // Names in speaking order (min 2). Required for new debates.
|
|
134
|
+
prompt: string, // Topic/challenge. For multi-round, include per-round instructions.
|
|
135
|
+
rounds?: number, // Complete rounds (default: 1, max: 50)
|
|
136
|
+
observer?: string, // Agent for final consolidation (default: built-in)
|
|
137
|
+
sessionID?: string, // ses_xxxx — pass to extend a concluded roundtable
|
|
138
|
+
title?: string, // Custom title (auto-generated if omitted)
|
|
139
|
+
observerPrompt?: string, // Override the observer consolidation prompt entirely
|
|
140
|
+
})
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
**Returns:** `{ sessionID: string, summary: string }` after the debate concludes.
|
|
144
|
+
|
|
145
|
+
**Side effect:** injects system-level prompts into each agent's context during
|
|
146
|
+
the debate (role-setting, topic, turn routing, and lifecycle signals).
|
|
147
|
+
|
|
148
|
+
### Parameters
|
|
149
|
+
|
|
150
|
+
| Parameter | Type | Required | Default | Description |
|
|
151
|
+
|-----------|------|----------|---------|-------------|
|
|
152
|
+
| `agents` | `string[]` | No* | — | Agent names in speaking order (min 2, schema `minItems:2`). Required for new debates; omit when extending |
|
|
153
|
+
| `prompt` | `string` | Yes | — | Topic/challenge. For multi-round, include per-round instructions. All agents see the full agenda |
|
|
154
|
+
| `rounds` | `number` | No | `1` | Number of complete rounds (schema `minimum:1`, `maximum:50`) |
|
|
155
|
+
| `observer` | `string` | No | `built-in` | Agent name for final consolidation. Observer does not debate — it summarizes after all rounds |
|
|
156
|
+
| `sessionID` | `string` | No* | — | Session ID (format: `ses_xxxx`). Pass to extend a concluded roundtable. Omit (and pass `agents`) for a fresh debate |
|
|
157
|
+
| `title` | `string` | No | Auto | Custom title (max 200 chars). Auto-generated as `"(Roundtable) - {first 60 chars of prompt, truncated at word boundary}"` if omitted |
|
|
158
|
+
| `observerPrompt` | `string` | No | — | Overrides the default observer consolidation prompt. Use to control format — e.g., `"Output as JSON"`, `"Save a detailed report to report.md"`, `"Focus only on technical decisions"` |
|
|
159
|
+
|
|
160
|
+
### Available agents tool
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
available_agents()
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
**Returns** a formatted list of configured agent names:
|
|
167
|
+
|
|
168
|
+
```
|
|
169
|
+
Available agents: pm, dev, rv, plan, build
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Active roundtables tool
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
active_roundtables()
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
**Returns** a formatted list of active roundtables with clickable session IDs:
|
|
179
|
+
|
|
180
|
+
```
|
|
181
|
+
Active roundtables:
|
|
182
|
+
- #ses_xxx · pm→dev→rv (R1/2) · debating
|
|
183
|
+
- #ses_yyy · pm→dev (R2/2) · consolidating
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Each entry shows session ID, agents in speaking order, current round, and phase
|
|
187
|
+
status (`debating`, `consolidating`, or `concluded`).
|
|
188
|
+
|
|
189
|
+
### Mode inference
|
|
190
|
+
|
|
191
|
+
The tool infers the operation mode from the presence of `sessionID`:
|
|
192
|
+
- **No `sessionID`** → starts a fresh debate (requires `agents`)
|
|
193
|
+
- **`sessionID` present** → continues a concluded roundtable (uses stored agent config)
|
|
194
|
+
|
|
195
|
+
Passing both `agents` and `sessionID` returns an error.
|
|
196
|
+
|
|
197
|
+
### Return value (tool execute)
|
|
198
|
+
|
|
199
|
+
The tool blocks until the debate concludes (via a pending Promise) and returns
|
|
200
|
+
a string result:
|
|
201
|
+
|
|
202
|
+
```
|
|
203
|
+
━━━ Roundtable Concluded ━━━
|
|
204
|
+
Topic: {prompt}
|
|
205
|
+
Participants: {agents}
|
|
206
|
+
|
|
207
|
+
── PM (Round 1) ──
|
|
208
|
+
{response}
|
|
209
|
+
• toolName → {outputPreview}
|
|
210
|
+
...
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
If an error occurs (invalid agents, session not found, etc.) the tool returns
|
|
214
|
+
an error string prefixed with `"Error:"`. The tool never hangs — error returns
|
|
215
|
+
are immediate.
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## 4. Lifecycle
|
|
220
|
+
|
|
221
|
+
### Full flow (fresh debate)
|
|
222
|
+
|
|
223
|
+
```
|
|
224
|
+
PHASE 1 — INITIALIZATION
|
|
225
|
+
─────────────────────────
|
|
226
|
+
1. Orchestrator agent calls roundtable()
|
|
227
|
+
2. Plugin validates agents against ctx.client.app.agents()
|
|
228
|
+
3. Plugin creates S2 via session.create({ parentID: S1, title }) — title is
|
|
229
|
+
"(Roundtable) - {prompt[:57]}..." or custom title
|
|
230
|
+
4. Plugin stores initial state in in-memory Map + saveStateFile()
|
|
231
|
+
5. Plugin sends first prompt to agents[0] via session.prompt({agent})
|
|
232
|
+
(Prompt returns immediately — LLM runs in background)
|
|
233
|
+
6. Plugin injects noReply in S1: ⚙ Roundtable started — #S2 • agents • N round(s)
|
|
234
|
+
7. Plugin injects noReply in S2: ⚙ Parent: #S1
|
|
235
|
+
8. If navigation === "auto", auto-navigate to S2
|
|
236
|
+
9. Plugin shows toast "Roundtable started in #S2"
|
|
237
|
+
10. Tool execute blocks with pending Promise
|
|
238
|
+
|
|
239
|
+
PHASE 2 — DEBATE (repeats for each round)
|
|
240
|
+
──────────────────────────────────────────
|
|
241
|
+
11. agents[i] finishes → session.idle fires on S2
|
|
242
|
+
12. Plugin reads response with session.messages(S2)
|
|
243
|
+
13. Plugin extracts text + tool call summaries
|
|
244
|
+
14. Plugin appends HistoryEntry to state.history
|
|
245
|
+
15. Plugin calls saveStateFile() after each turn
|
|
246
|
+
16. Plugin checks detectLoop() — if True, finalizes early
|
|
247
|
+
17. Plugin decides next step:
|
|
248
|
+
a) Still have agents in this round? → agents[i+1], sendToAgent()
|
|
249
|
+
b) Round complete + more rounds left? → agents[0], round++, sendToAgent()
|
|
250
|
+
c) All rounds complete? → PHASE 3
|
|
251
|
+
|
|
252
|
+
Session title updates dynamically during debate:
|
|
253
|
+
⚡ "prompt..." · pm→dev→rv (R1/2 · ↑ #S1)
|
|
254
|
+
|
|
255
|
+
PHASE 3 — OBSERVER
|
|
256
|
+
──────────────────
|
|
257
|
+
18. Plugin decides which observer to use:
|
|
258
|
+
a) Explicit observer? → sends full prompt to that agent
|
|
259
|
+
b) Default observer? → sends DEFAULT_OBSERVER_PROMPT to S2
|
|
260
|
+
19. Observer consolidates → session.idle
|
|
261
|
+
20. Plugin extracts summary → appends to history, saves file
|
|
262
|
+
21. phase = "done"
|
|
263
|
+
|
|
264
|
+
PHASE 4 — FINALIZATION
|
|
265
|
+
──────────────────────
|
|
266
|
+
22. Plugin builds consolidated summary from full history
|
|
267
|
+
23. Plugin resolves the pending Promise with the summary
|
|
268
|
+
24. Plugin injects delimiter noReply in S2:
|
|
269
|
+
"━━━ Roundtable Concluded ━━━
|
|
270
|
+
Messages below this line are not part of the original debate.
|
|
271
|
+
The result was consolidated in the main session."
|
|
272
|
+
25. Plugin updates S2 title: ⚡ "prompt..." · pm→dev→rv ✓
|
|
273
|
+
26. Plugin calls saveStateFile() with final state
|
|
274
|
+
27. If navigation === "auto", auto-navigate back to S1
|
|
275
|
+
28. Plugin shows toast "Roundtable concluded"
|
|
276
|
+
29. Plugin clears in-memory state for S2
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### Flow (extend)
|
|
280
|
+
|
|
281
|
+
```
|
|
282
|
+
1. Orchestrator calls roundtable({sessionID, rounds, prompt})
|
|
283
|
+
2. Plugin validates session exists via session.get()
|
|
284
|
+
3. Plugin loads serialized state via loadStateFile(sessionID)
|
|
285
|
+
4. Plugin validates phase was "done" and agents match
|
|
286
|
+
5. Plugin creates NEW state with:
|
|
287
|
+
- accumulated rounds (totalRounds += new_rounds)
|
|
288
|
+
- preserved history + errors
|
|
289
|
+
- extended prompt via buildExtendedPrompt()
|
|
290
|
+
- phase = "active"
|
|
291
|
+
6. Plugin stores new state in Map + saveStateFile()
|
|
292
|
+
7. Plugin updates S2 title with new round info
|
|
293
|
+
8. Plugin injects noReply in S1: ⚙ Roundtable extended — #S2 • +N round(s)
|
|
294
|
+
9. Plugin sends extended prompt to agents[0]
|
|
295
|
+
10. Continues normal PHASE 2 flow
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
## 5. States & Persistence
|
|
301
|
+
|
|
302
|
+
### Roundtable state machine
|
|
303
|
+
|
|
304
|
+
```
|
|
305
|
+
┌────────────┐
|
|
306
|
+
│ ACTIVE │ (debate in progress)
|
|
307
|
+
└─────┬──────┘
|
|
308
|
+
│ all rounds complete
|
|
309
|
+
▼
|
|
310
|
+
┌────────────┐
|
|
311
|
+
│ OBSERVING │ (observer — built-in or explicit — always runs)
|
|
312
|
+
└─────┬──────┘
|
|
313
|
+
│ observer summarizes
|
|
314
|
+
▼
|
|
315
|
+
┌────────────┐
|
|
316
|
+
│ DONE │
|
|
317
|
+
└────────────┘
|
|
318
|
+
|
|
319
|
+
At any state:
|
|
320
|
+
│ session.deleted / fatal error
|
|
321
|
+
▼
|
|
322
|
+
┌────────────┐
|
|
323
|
+
│ ABORTED │
|
|
324
|
+
└────────────┘
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
> **Note**: `OBSERVING` **always** runs. The built-in default observer
|
|
328
|
+
> guarantees every debate produces a consolidated summary at the end.
|
|
329
|
+
|
|
330
|
+
### In-memory state type
|
|
331
|
+
|
|
332
|
+
```typescript
|
|
333
|
+
interface RoundtableState {
|
|
334
|
+
// Identification
|
|
335
|
+
sessionID: string // S2
|
|
336
|
+
parentSessionID: string // S1
|
|
337
|
+
|
|
338
|
+
// Config
|
|
339
|
+
agents: string[]
|
|
340
|
+
totalRounds: number
|
|
341
|
+
observer: "built-in" | string // "built-in" = plugin default observer
|
|
342
|
+
prompt: string
|
|
343
|
+
|
|
344
|
+
// Progress
|
|
345
|
+
currentRound: number // 0-indexed
|
|
346
|
+
currentAgentIndex: number // 0-indexed
|
|
347
|
+
phase: "active" | "observing" | "done" | "aborted"
|
|
348
|
+
currentGeneration: number // incremented each turn, used for timeout staleness
|
|
349
|
+
|
|
350
|
+
// Data
|
|
351
|
+
history: HistoryEntry[]
|
|
352
|
+
errors: string[]
|
|
353
|
+
createdAt: number
|
|
354
|
+
lastProcessedMsgId?: string // prevents duplicate processing
|
|
355
|
+
userInterjections: string[] // user messages typed in S2 during debate
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
interface HistoryEntry {
|
|
359
|
+
agent: string
|
|
360
|
+
round: number
|
|
361
|
+
response: string // plain text of the response
|
|
362
|
+
toolCalls: ToolCallSummary[] // tool names + output previews
|
|
363
|
+
hasError: boolean
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
interface ToolCallSummary {
|
|
367
|
+
toolName: string
|
|
368
|
+
outputPreview: string // first N chars of output (configurable, default 500)
|
|
369
|
+
}
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
### File-based persistence
|
|
373
|
+
|
|
374
|
+
State is persisted as JSON files at:
|
|
375
|
+
|
|
376
|
+
```
|
|
377
|
+
~/.config/opencode/roundtable-states/<sessionID>.json
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
The directory is determined by `$XDG_CONFIG_HOME` (if set) or
|
|
381
|
+
`~/.config/opencode/roundtable-states/`.
|
|
382
|
+
|
|
383
|
+
Key behaviors:
|
|
384
|
+
- `saveStateFile()` is called after state mutations (turn complete, phase
|
|
385
|
+
change, error, etc.)
|
|
386
|
+
- `loadStateFile()` validates required fields before returning
|
|
387
|
+
- `deleteStateFile()` is called when a session is deleted
|
|
388
|
+
- `listStateFiles()` lists all `.json` files in the directory
|
|
389
|
+
- On plugin startup, `scanOrphanRoundtables()` loads all state files into
|
|
390
|
+
the in-memory `states` Map for recovery and active listing
|
|
391
|
+
|
|
392
|
+
### In-memory maps
|
|
393
|
+
|
|
394
|
+
```typescript
|
|
395
|
+
const states = new Map<string, RoundtableState>()
|
|
396
|
+
const timeoutHandles = new Map<string, ReturnType<typeof setTimeout>>()
|
|
397
|
+
const pendingResults = new Map<string, { resolve: (output: string) => void }>()
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
- `states` — all known roundtables, synced from files on startup
|
|
401
|
+
- `timeoutHandles` — per-agent timer handles for timeout enforcement
|
|
402
|
+
- `pendingResults` — Promise resolvers that unblock the `roundtable()` tool
|
|
403
|
+
|
|
404
|
+
---
|
|
405
|
+
|
|
406
|
+
## 6. History & Context
|
|
407
|
+
|
|
408
|
+
### Content sent to each agent
|
|
409
|
+
|
|
410
|
+
Each time the plugin passes the turn (via `buildAgentPrompt`), it sends a
|
|
411
|
+
minimal message with:
|
|
412
|
+
|
|
413
|
+
```
|
|
414
|
+
[System] You are participating in a roundtable debate. Agents (A → B → C) will discuss the topic below in N round(s).
|
|
415
|
+
[Topic] {prompt}
|
|
416
|
+
|
|
417
|
+
Round N/M | Current agent: Z
|
|
418
|
+
|
|
419
|
+
[System] This is the last prompt — finalize your thoughts (last round only)
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
Key points:
|
|
423
|
+
- No `[ROUNDTABLE META]` block is injected (state is file-based)
|
|
424
|
+
- Agents are NOT instructed to ignore META blocks
|
|
425
|
+
- The initial system message is only included on the first turn
|
|
426
|
+
- The "finalize your thoughts" hint is added on the last turn
|
|
427
|
+
|
|
428
|
+
### What the plugin includes in history
|
|
429
|
+
|
|
430
|
+
The accumulated history contains **everything relevant for consolidation**:
|
|
431
|
+
|
|
432
|
+
- `TextPart.text` — agent's textual response
|
|
433
|
+
- `ToolCallSummary` — tool name + **output preview** (configurable, default 500 characters).
|
|
434
|
+
If DEV ran `ls src/`, the next agent sees the discovered directory structure.
|
|
435
|
+
- Errors and failures are tracked per entry
|
|
436
|
+
|
|
437
|
+
### What the plugin does NOT include in history
|
|
438
|
+
|
|
439
|
+
- `ReasoningPart.text` — agent's internal reasoning (stays in S2 session)
|
|
440
|
+
- `StepStartPart` / `StepFinishPart` — execution metadata
|
|
441
|
+
|
|
442
|
+
### History visibility to agents
|
|
443
|
+
|
|
444
|
+
History is accumulated server-side in the plugin state. It is NOT currently
|
|
445
|
+
injected into subsequent agent prompts — each agent only sees its current
|
|
446
|
+
turn's context. The full history is used by the observer for consolidation
|
|
447
|
+
and appears in the final summary returned to S1.
|
|
448
|
+
|
|
449
|
+
### User interjections
|
|
450
|
+
|
|
451
|
+
When a user types a message in S2 during the debate, the plugin tracks those
|
|
452
|
+
messages in `state.userInterjections[]`. These messages do not break the
|
|
453
|
+
state machine — the round-robin continues after the user's message.
|
|
454
|
+
|
|
455
|
+
---
|
|
456
|
+
|
|
457
|
+
## 7. Edge Case Handling
|
|
458
|
+
|
|
459
|
+
### 7.1 Agent failure (provider error)
|
|
460
|
+
|
|
461
|
+
```
|
|
462
|
+
session.error on S2 with agent = currentAgent
|
|
463
|
+
→ Logs error in state.errors[]
|
|
464
|
+
→ Advances to next agent (or next round, or aborts)
|
|
465
|
+
→ Toast: "agent failed on Round N. Skipping to next."
|
|
466
|
+
→ If all agents fail → aborts with error
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
### 7.2 User closes S2 (session.deleted on S2)
|
|
470
|
+
|
|
471
|
+
```
|
|
472
|
+
session.deleted on S2 (deletedSessionID === state.sessionID)
|
|
473
|
+
→ Deletes state file
|
|
474
|
+
→ phase = "aborted"
|
|
475
|
+
→ Builds partial consolidated summary
|
|
476
|
+
→ Resolves pending Promise with:
|
|
477
|
+
"[Roundtable interrupted — session closed.
|
|
478
|
+
Partial history up to interruption:]
|
|
479
|
+
{partial summary}"
|
|
480
|
+
→ Toast: "Roundtable interrupted"
|
|
481
|
+
→ Clears in-memory state
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
### 7.3 User closes S1 (session.deleted on S1)
|
|
485
|
+
|
|
486
|
+
```
|
|
487
|
+
session.deleted on S1 (deletedSessionID === state.parentSessionID)
|
|
488
|
+
→ Deletes state file
|
|
489
|
+
→ phase = "aborted"
|
|
490
|
+
→ Aborts S2 via session.abort({path:{id:S2}})
|
|
491
|
+
→ Clears in-memory state
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
### 7.4 User types a message in S2 during the debate
|
|
495
|
+
|
|
496
|
+
S2 is a visible and interactive session. If the user switches to S2
|
|
497
|
+
and types something:
|
|
498
|
+
|
|
499
|
+
- The message enters S2's context
|
|
500
|
+
- If the current agent is still processing, the user's message will be
|
|
501
|
+
processed after the agent finishes
|
|
502
|
+
- The plugin continues the round-robin — the user's message becomes part
|
|
503
|
+
of the context
|
|
504
|
+
- The plugin tracks interjections in `state.userInterjections[]`
|
|
505
|
+
|
|
506
|
+
**Behavior**: natural and desired. The user can intervene in the debate.
|
|
507
|
+
|
|
508
|
+
### 7.5 Debate loop (agents repeating arguments)
|
|
509
|
+
|
|
510
|
+
After each turn, the plugin checks similarity between the latest response
|
|
511
|
+
and the previous one using **Jaccard similarity of bigrams (character pairs)**:
|
|
512
|
+
|
|
513
|
+
```
|
|
514
|
+
bigrams = set of all consecutive character pairs in cleaned text
|
|
515
|
+
similarity = |bigrams(current) ∩ bigrams(previous)| / |bigrams(current) ∪ bigrams(previous)|
|
|
516
|
+
|
|
517
|
+
if similarity > threshold (default 0.85):
|
|
518
|
+
→ "Loop detected — agents reached an impasse"
|
|
519
|
+
→ phase = "done", finalizes early with partial result
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
**Why Jaccard bigrams?**
|
|
523
|
+
- Purely computational (no LLM calls or embeddings needed)
|
|
524
|
+
- Zero external dependencies
|
|
525
|
+
- Reasonable for detecting textual argument repetition
|
|
526
|
+
- Threshold is configurable via `loopSimilarityThreshold` in config
|
|
527
|
+
|
|
528
|
+
### 7.6 Agent timeout
|
|
529
|
+
|
|
530
|
+
- Each agent turn has a configurable timeout (default **5 minutes**)
|
|
531
|
+
- Implemented via `setTimeout` + generation counter (stale check)
|
|
532
|
+
- If it expires: `session.abort({path:{id:S2}})` → skip to next agent
|
|
533
|
+
- Timeout handle is tracked in `timeoutHandles` map
|
|
534
|
+
|
|
535
|
+
### 7.7 Multiple simultaneous roundtables
|
|
536
|
+
|
|
537
|
+
Each roundtable has its own state in `Map<sessionID, RoundtableState>`.
|
|
538
|
+
They are independent. The plugin allows multiple roundtables running in
|
|
539
|
+
parallel.
|
|
540
|
+
|
|
541
|
+
### 7.8 State recovery after restart
|
|
542
|
+
|
|
543
|
+
If OpenCode restarts mid-roundtable:
|
|
544
|
+
- In-memory state is lost
|
|
545
|
+
- State JSON files persist on disk at `~/.config/opencode/roundtable-states/`
|
|
546
|
+
- S2 session remains in the database
|
|
547
|
+
|
|
548
|
+
**Recovery**: on initialization, the plugin runs `scanOrphanRoundtables()`:
|
|
549
|
+
```
|
|
550
|
+
1. List all .json files in roundtable-states directory
|
|
551
|
+
2. For each file, loadStateFile() and add to in-memory states Map
|
|
552
|
+
3. Log: "scanOrphanRoundtables: N state(s) loaded, M error(s)"
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
Recovered states with `phase: "active"` become visible via `active_roundtables()`
|
|
556
|
+
but the debate does not auto-resume — the user must decide whether to extend
|
|
557
|
+
or close the session.
|
|
558
|
+
|
|
559
|
+
### 7.9 Compaction during a roundtable
|
|
560
|
+
|
|
561
|
+
The plugin registers an `experimental.session.compacting` hook (currently a
|
|
562
|
+
no-op). Future phase 4 will re-inject critical state during compaction to
|
|
563
|
+
prevent data loss.
|
|
564
|
+
|
|
565
|
+
### 7.10 Human message in S2 after conclusion
|
|
566
|
+
|
|
567
|
+
After `phase = "done"` (roundtable concluded):
|
|
568
|
+
- The plugin has already injected the `━━━ Roundtable Concluded ━━━`
|
|
569
|
+
delimiter in S2
|
|
570
|
+
- The plugin's `processNextTurn` checks `state.phase === "done" | "aborted"`
|
|
571
|
+
and returns early — it no longer processes events for this session
|
|
572
|
+
- If the user types in S2, OpenCode responds normally with the session's
|
|
573
|
+
default agent
|
|
574
|
+
- The response **does not** alter the result already consolidated in S1
|
|
575
|
+
|
|
576
|
+
```
|
|
577
|
+
[noReply] ━━━ Roundtable Concluded ━━━
|
|
578
|
+
Messages below this line are not part of the original debate.
|
|
579
|
+
The result was consolidated in the main session.
|
|
580
|
+
── You ──
|
|
581
|
+
(conversation can continue here, but the plugin no longer
|
|
582
|
+
manages this session.)
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
**Behavior**: natural and transparent. S2 becomes a regular session after
|
|
586
|
+
the roundtable concludes.
|
|
587
|
+
|
|
588
|
+
### 7.11 Error during observer phase
|
|
589
|
+
|
|
590
|
+
If the observer fails (provider error in `OBSERVING` phase):
|
|
591
|
+
- `handleAgentError` sets `phase = "aborted"`
|
|
592
|
+
- Calls `finalizeRoundtable()` which builds partial summary from existing
|
|
593
|
+
history and resolves the pending Promise
|
|
594
|
+
|
|
595
|
+
---
|
|
596
|
+
|
|
597
|
+
## 8. Observer
|
|
598
|
+
|
|
599
|
+
### Function
|
|
600
|
+
|
|
601
|
+
The observer **always consolidates** the debate at the end. It does not
|
|
602
|
+
participate in rounds — it enters after all rounds are complete to produce
|
|
603
|
+
an executive summary.
|
|
604
|
+
|
|
605
|
+
### Default observer (built-in)
|
|
606
|
+
|
|
607
|
+
If the orchestrator does not specify `observer`, the plugin uses a built-in
|
|
608
|
+
observer with the following prompt:
|
|
609
|
+
|
|
610
|
+
```
|
|
611
|
+
DEFAULT_OBSERVER_PROMPT = `
|
|
612
|
+
You are an impartial roundtable observer.
|
|
613
|
+
Consolidate the debate below into:
|
|
614
|
+
|
|
615
|
+
1. **Executive summary** (2-3 sentences)
|
|
616
|
+
2. **Key points** raised by each participant
|
|
617
|
+
3. **Decisions or convergences** reached
|
|
618
|
+
4. **Remaining open questions**
|
|
619
|
+
5. **Suggested next steps**
|
|
620
|
+
|
|
621
|
+
Debate:
|
|
622
|
+
{full_history}
|
|
623
|
+
`
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
This prompt is sent to the S2 session itself (no specific agent), using the
|
|
627
|
+
session's default model/provider.
|
|
628
|
+
|
|
629
|
+
### Explicit observer (override)
|
|
630
|
+
|
|
631
|
+
If the orchestrator passes `observer: "rv"`, the plugin:
|
|
632
|
+
1. Builds a similar prompt, but with `Your role: rv. Provide an executive summary...`
|
|
633
|
+
2. Sends via `session.prompt({ agent: "rv" })` on the S2 session
|
|
634
|
+
3. The `rv` agent responds with its own tools and personality
|
|
635
|
+
|
|
636
|
+
### Flow
|
|
637
|
+
|
|
638
|
+
```
|
|
639
|
+
1. All rounds complete → transition to OBSERVING
|
|
640
|
+
2. Plugin decides:
|
|
641
|
+
a) Explicit observer? → sends to that agent
|
|
642
|
+
b) Default observer? → sends DEFAULT_OBSERVER_PROMPT to S2
|
|
643
|
+
3. Observer responds → session.idle
|
|
644
|
+
4. Plugin extracts text, appends to history, saves state file
|
|
645
|
+
5. phase = "done", finalizeRoundtable() called
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
### Observer prompt is configurable
|
|
649
|
+
|
|
650
|
+
**Per-call override (runtime):** pass `observerPrompt` in the `roundtable()` call.
|
|
651
|
+
This completely replaces the observer prompt for that debate:
|
|
652
|
+
|
|
653
|
+
```
|
|
654
|
+
roundtable({
|
|
655
|
+
agents: ["pm", "dev", "rv"],
|
|
656
|
+
prompt: "...",
|
|
657
|
+
observerPrompt: "Output only a single emoji that captures the discussion",
|
|
658
|
+
})
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
The `observerPrompt` is persisted in the state file and carried over on extend
|
|
662
|
+
(unless a new `observerPrompt` is provided in the extend call).
|
|
663
|
+
|
|
664
|
+
**Static override (config):** the `defaultObserverPrompt` field in
|
|
665
|
+
`roundtable.json` replaces the built-in template for all debates that don't
|
|
666
|
+
provide a per-call `observerPrompt`.
|
|
667
|
+
|
|
668
|
+
---
|
|
669
|
+
|
|
670
|
+
## 9. Extend Mode
|
|
671
|
+
|
|
672
|
+
### Usage
|
|
673
|
+
|
|
674
|
+
```typescript
|
|
675
|
+
roundtable({
|
|
676
|
+
sessionID: "ses_abc123", // Original S2 ID
|
|
677
|
+
rounds: 2,
|
|
678
|
+
prompt: "Dive deeper into operational costs",
|
|
679
|
+
})
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
### Prompt semantics in extend mode
|
|
683
|
+
|
|
684
|
+
The `prompt` parameter in `extend` mode can be:
|
|
685
|
+
|
|
686
|
+
| Intent | Example | Effect |
|
|
687
|
+
|--------|---------|--------|
|
|
688
|
+
| **Continuation** | "Debate more about X" | New prompt is sent as a complement to the original topic. `final_prompt = "Original topic: {original.prompt}\n\nContinuation: {prompt}"` |
|
|
689
|
+
| **New topic** | "Now plan Y" | New prompt replaces the topic, but previous history is preserved as context. `final_prompt = "Previous discussion history preserved. Original topic was: ...\n\nNew challenge: {prompt}"` |
|
|
690
|
+
|
|
691
|
+
The plugin infers intent heuristically: if the prompt starts with
|
|
692
|
+
"Debate more", "Continue", "Dive deeper", "Expand on", "Elaborate",
|
|
693
|
+
"Further discuss", or "Keep debating" → continuation. Otherwise → new topic.
|
|
694
|
+
|
|
695
|
+
### Flow
|
|
696
|
+
|
|
697
|
+
```
|
|
698
|
+
1. Plugin validates session exists via session.get(sessionID)
|
|
699
|
+
2. Plugin loads state from file via loadStateFile(sessionID)
|
|
700
|
+
3. Plugin validates:
|
|
701
|
+
a) phase === "done" (cannot extend active/observing)
|
|
702
|
+
b) agents match (if passed)
|
|
703
|
+
c) all original agents still exist on server
|
|
704
|
+
4. Creates NEW state with:
|
|
705
|
+
- same sessionID, parentSessionID, agents, observer
|
|
706
|
+
- totalRounds += new_rounds (accumulative)
|
|
707
|
+
- history + errors from original
|
|
708
|
+
- extended_prompt from buildExtendedPrompt()
|
|
709
|
+
- phase = "active"
|
|
710
|
+
5. Stores new state in Map + saveStateFile()
|
|
711
|
+
6. Updates S2 title with new round info
|
|
712
|
+
7. Injects noReply in S1: ⚙ Roundtable extended — #S2 • +N round(s)
|
|
713
|
+
8. Sends extended_prompt to agents[0]
|
|
714
|
+
9. Continues normal PHASE 2 flow
|
|
715
|
+
```
|
|
716
|
+
|
|
717
|
+
### Constraints
|
|
718
|
+
|
|
719
|
+
- Only works if S2 still exists (was not deleted)
|
|
720
|
+
- Only works if original phase was "done"
|
|
721
|
+
- Agents and observer must be the same as the original roundtable (agents
|
|
722
|
+
are optional in the request, but if provided they must match exactly)
|
|
723
|
+
- Previous discussion history is always preserved
|
|
724
|
+
- If the original roundtable used an explicit observer, the extend also uses
|
|
725
|
+
it (observer type is stored in state file)
|
|
726
|
+
|
|
727
|
+
### Stuck session recovery
|
|
728
|
+
|
|
729
|
+
If an extend is interrupted mid-way (TUI freeze, process kill, etc.), the
|
|
730
|
+
state file has `phase: "active"` but no agent is running. The next extend
|
|
731
|
+
call returns an error indicating the session is in an invalid state.
|
|
732
|
+
The user must manually close the S2 and start fresh.
|
|
733
|
+
|
|
734
|
+
---
|
|
735
|
+
|
|
736
|
+
## 10. Navigation & TUI
|
|
737
|
+
|
|
738
|
+
### Navigation modes
|
|
739
|
+
|
|
740
|
+
Configurable via `navigation` in `roundtable.json`:
|
|
741
|
+
|
|
742
|
+
| Mode | Value | Behavior |
|
|
743
|
+
|------|-------|----------|
|
|
744
|
+
| **Link** | `"link"` (default) | No auto-navigation. Relies on native `#ses_xxx` link rendering in the TUI for user to navigate manually |
|
|
745
|
+
| **Auto** | `"auto"` | Auto-navigates S1 → S2 on create, and S2 → S1 on conclude. Uses `navigateToSession()` helper which calls `tui.publish({ type: "tui.session.select" })` |
|
|
746
|
+
| **Disabled** | `"none"` | No automatic navigation. No link rendering relied upon |
|
|
747
|
+
|
|
748
|
+
`navigateToSession()` tries `ctx.client.tui.selectSession()` first, falling
|
|
749
|
+
back to `tui.publish({ type: "tui.session.select" })`.
|
|
750
|
+
|
|
751
|
+
### Session titles
|
|
752
|
+
|
|
753
|
+
| State | Format |
|
|
754
|
+
|-------|--------|
|
|
755
|
+
| Active (auto-generated) | `(Roundtable) - {prompt[:57]}...` |
|
|
756
|
+
| During debate | `⚡ "{prompt[:37]}..." · agent1→agent2 (R1/2 · ↑ #parentID)` |
|
|
757
|
+
| Concluded | `⚡ "{prompt[:37]}..." · agent1→agent2 ✓` |
|
|
758
|
+
|
|
759
|
+
### Compact S1 markers
|
|
760
|
+
|
|
761
|
+
Instead of verbose serialization, S1 gets minimal noReply markers:
|
|
762
|
+
|
|
763
|
+
| Event | Marker |
|
|
764
|
+
|-------|--------|
|
|
765
|
+
| Roundtable started | `⚙ Roundtable started — #ses_xxx • agents • N round(s)` |
|
|
766
|
+
| Roundtable extended | `⚙ Roundtable extended — #ses_xxx • +N round(s)` |
|
|
767
|
+
|
|
768
|
+
### S2 markers
|
|
769
|
+
|
|
770
|
+
| Purpose | Marker |
|
|
771
|
+
|---------|--------|
|
|
772
|
+
| Parent reference | `⚙ Parent: #ses_yyy` — compact marker for TUI navigation |
|
|
773
|
+
|
|
774
|
+
### TUI Plugin
|
|
775
|
+
|
|
776
|
+
Registered in `tui.json`, exported at `./tui`, the TUI plugin provides:
|
|
777
|
+
|
|
778
|
+
| Feature | Implementation |
|
|
779
|
+
|---------|---------------|
|
|
780
|
+
| **Badge `[RT]`** | `api.slots.register({ sidebar_title })` — shows `[RT]` badge on sessions whose title starts with `⚡` |
|
|
781
|
+
| **← Back link** | `api.slots.register({ sidebar_content })` — shows a clickable `← Back` link on child sessions that navigates to the parent |
|
|
782
|
+
| **`/roundtables` command** | `api.command.register()` — slash command opens a dialog listing all active roundtable sessions. Clicking a session navigates to it |
|
|
783
|
+
| **Toast notifications** | `api.client.tui.showToast()` — on start, completion, errors, and interruptions |
|
|
784
|
+
|
|
785
|
+
### TUI appearance example
|
|
786
|
+
|
|
787
|
+
```
|
|
788
|
+
┌─────────────────────────────────────────────────┐
|
|
789
|
+
│ Main Session (S1) [sidebar: no RT badge] │
|
|
790
|
+
├─────────────────────────────────────────────────┤
|
|
791
|
+
│ You: roundtable({agents:[pm,dev], rounds:2}) │
|
|
792
|
+
│ │
|
|
793
|
+
│ Build: ⚙ Roundtable started — #abc123 │
|
|
794
|
+
│ • pm → dev • 2 round(s) │
|
|
795
|
+
│ │
|
|
796
|
+
│ [tool returns consolidated summary │
|
|
797
|
+
│ ━━━ Roundtable Concluded ━━━ │
|
|
798
|
+
│ Topic: What architecture? │
|
|
799
|
+
│ PM: microservices... │
|
|
800
|
+
│ DEV: monolith... │
|
|
801
|
+
│ Conclusion: modular monolith] │
|
|
802
|
+
└─────────────────────────────────────────────────┘
|
|
803
|
+
|
|
804
|
+
┌─────────────────────────────────────────────────┐
|
|
805
|
+
│ Roundtable Session #abc123 [sidebar: [RT]] │
|
|
806
|
+
│ ← Back (clickable link) │
|
|
807
|
+
├─────────────────────────────────────────────────┤
|
|
808
|
+
│ [noReply] ⚙ Parent: #S1 │
|
|
809
|
+
│ ── PM ── │
|
|
810
|
+
│ The microservices approach allows... │
|
|
811
|
+
│ ── DEV ── │
|
|
812
|
+
│ Disagree, the complexity is not justified... │
|
|
813
|
+
│ ── PM (R2) ── │
|
|
814
|
+
│ OK, but what if we split only the... │
|
|
815
|
+
│ ── DEV (R2) ── │
|
|
816
|
+
│ That's basically a modular monolith... │
|
|
817
|
+
│ [noReply] ━━━ Roundtable Concluded ━━━ │
|
|
818
|
+
└─────────────────────────────────────────────────┘
|
|
819
|
+
```
|
|
820
|
+
|
|
821
|
+
### Recommended color config
|
|
822
|
+
|
|
823
|
+
```json
|
|
824
|
+
{
|
|
825
|
+
"agent": {
|
|
826
|
+
"pm": { "color": "#3498db" },
|
|
827
|
+
"dev": { "color": "#2ecc71" },
|
|
828
|
+
"rv": { "color": "#e74c3c" },
|
|
829
|
+
"plan": { "color": "#f39c12" },
|
|
830
|
+
"build": { "color": "#9b59b6" }
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
```
|
|
834
|
+
|
|
835
|
+
---
|
|
836
|
+
|
|
837
|
+
## 11. Plugin Structure
|
|
838
|
+
|
|
839
|
+
### Files
|
|
840
|
+
|
|
841
|
+
```
|
|
842
|
+
opencode-roundtable/
|
|
843
|
+
├── package.json # name, version, main: dist/index.js, export ./tui
|
|
844
|
+
├── tsconfig.json
|
|
845
|
+
├── index.ts # Plugin entry point + tool definitions + event handler
|
|
846
|
+
├── src/
|
|
847
|
+
│ ├── types.ts # Interfaces: RoundtableState, HistoryEntry, ToolCallSummary, etc.
|
|
848
|
+
│ ├── config.ts # loadConfig, validateConfig, defaults (roundtable.json)
|
|
849
|
+
│ ├── state.ts # In-memory maps + file persistence (saveStateFile, loadStateFile, etc.)
|
|
850
|
+
│ ├── prompts.ts # buildAgentPrompt, buildObserverPrompt
|
|
851
|
+
│ ├── handlers.ts # All orchestration logic (start, extend, process turns, finalize, errors)
|
|
852
|
+
│ ├── utils.ts # detectLoop, extractResponse, buildToolSummaries, navigateToSession, etc.
|
|
853
|
+
│ └── tui/
|
|
854
|
+
│ └── tui.tsx # TUI plugin (badge, sidebar link, /roundtables command)
|
|
855
|
+
├── docs/
|
|
856
|
+
│ ├── SPEC.md
|
|
857
|
+
│ ├── IMPLEMENTATION.md
|
|
858
|
+
│ └── roundtable.schema.json
|
|
859
|
+
└── README.md
|
|
860
|
+
```
|
|
861
|
+
|
|
862
|
+
### Dependencies
|
|
863
|
+
|
|
864
|
+
None external. Only `@opencode-ai/plugin` (peer dependency of OpenCode).
|
|
865
|
+
|
|
866
|
+
### Hooks used
|
|
867
|
+
|
|
868
|
+
| Hook | Purpose |
|
|
869
|
+
|------|---------|
|
|
870
|
+
| `event` | Listens to `session.idle`, `session.error`, `session.deleted` |
|
|
871
|
+
| `experimental.session.compacting` | Placeholder for state preservation during compaction |
|
|
872
|
+
| `tool` | Defines the `roundtable`, `available_agents`, and `active_roundtables` tools |
|
|
873
|
+
|
|
874
|
+
### SDK APIs used
|
|
875
|
+
|
|
876
|
+
| API | Usage |
|
|
877
|
+
|-----|-------|
|
|
878
|
+
| `ctx.client.session.create()` | Create S2 |
|
|
879
|
+
| `ctx.client.session.prompt()` | Send messages to agents |
|
|
880
|
+
| `ctx.client.session.messages()` | Read agent responses |
|
|
881
|
+
| `ctx.client.session.abort()` | Abort timed-out agent |
|
|
882
|
+
| `ctx.client.session.update()` | Update session title |
|
|
883
|
+
| `ctx.client.session.get()` | Validate session exists (extend mode) |
|
|
884
|
+
| `ctx.client.tui.showToast()` | Notify user |
|
|
885
|
+
| `ctx.client.tui.publish()` | Auto-navigate (fallback) |
|
|
886
|
+
| `ctx.client.app.agents()` | Discover available agents |
|
|
887
|
+
| `ctx.client.app.log()` | Debug logging |
|
|
888
|
+
|
|
889
|
+
---
|
|
890
|
+
|
|
891
|
+
## 12. Tests & Validation
|
|
892
|
+
|
|
893
|
+
### Test scenarios
|
|
894
|
+
|
|
895
|
+
1. **Basic roundtable**: 2 agents, 1 round, default observer
|
|
896
|
+
2. **Explicit observer**: 3 agents, 2 rounds, observer="rv" consolidates
|
|
897
|
+
3. **Default observer**: same scenario, no observer param → uses built-in
|
|
898
|
+
4. **Agent failure**: provider error → skip → continue
|
|
899
|
+
5. **Timeout**: agent takes too long → abort + skip
|
|
900
|
+
6. **Loop detection**: agents repeat arguments → Jaccard > 0.85 → end debate early
|
|
901
|
+
7. **Session deleted**: close S2 → partial result in S1
|
|
902
|
+
8. **Parent deleted**: close S1 → S2 aborted
|
|
903
|
+
9. **Extend (continuation)**: conclude → extend with "Debate more about X"
|
|
904
|
+
10. **Extend (new topic)**: conclude → extend with "Now plan Y"
|
|
905
|
+
11. **Multiple**: 2 roundtables in parallel
|
|
906
|
+
12. **Compaction**: compact S2 during debate (no-op currently)
|
|
907
|
+
13. **User interjection**: user writes in S2 during debate → continues
|
|
908
|
+
14. **Post-conclusion**: user writes in S2 after done → S2 becomes normal session
|
|
909
|
+
15. **Startup recovery**: restart during ACTIVE → scanOrphanRoundtables detects states
|
|
910
|
+
16. **Invalid agent**: agents with nonexistent name → error with available list
|
|
911
|
+
17. **Available agents tool**: orchestrator calls `available_agents()` → gets list
|
|
912
|
+
18. **Active roundtables tool**: lists all active roundtables with status
|
|
913
|
+
19. **Navigation config**: auto-navigate on create/conclude with `navigation: "auto"`
|
|
914
|
+
|
|
915
|
+
### Acceptance criteria
|
|
916
|
+
|
|
917
|
+
- [ ] `roundtable` tool appears in the agent's tool list
|
|
918
|
+
- [ ] `available_agents` tool appears in the agent's tool list
|
|
919
|
+
- [ ] `active_roundtables` tool lists active roundtables
|
|
920
|
+
- [ ] S2 is created as a child of S1
|
|
921
|
+
- [ ] Agents speak in the specified order
|
|
922
|
+
- [ ] Each agent keeps its own personality
|
|
923
|
+
- [ ] History + tool outputs tracked in state per turn
|
|
924
|
+
- [ ] Default observer (built-in) consolidates the debate at the end
|
|
925
|
+
- [ ] Explicit observer (named agent) also works
|
|
926
|
+
- [ ] Final result is returned to the calling tool
|
|
927
|
+
- [ ] Toast notifications appear on start, error, and conclusion
|
|
928
|
+
- [ ] Agent errors are skipped with notification
|
|
929
|
+
- [ ] S2 session title updates dynamically during debate
|
|
930
|
+
- [ ] Session title shows `✓` on conclusion
|
|
931
|
+
- [ ] Extend mode resumes from state file (continuation and new topic)
|
|
932
|
+
- [ ] Session deletion is handled gracefully (partial result or abort)
|
|
933
|
+
- [ ] Loop detection works (Jaccard bigrams, configurable threshold)
|
|
934
|
+
- [ ] Invalid agent names return an error with the available list
|
|
935
|
+
- [ ] `━━━ Roundtable Concluded ━━━` delimiter appears in S2 at the end
|
|
936
|
+
- [ ] Post-conclusion messages in S2 do not affect the S1 result
|
|
937
|
+
- [ ] State files persist in `~/.config/opencode/roundtable-states/`
|
|
938
|
+
- [ ] Startup recovery loads state files into memory
|
|
939
|
+
- [ ] Navigation config respects `link`/`auto`/`none` modes
|
|
940
|
+
- [ ] TUI plugin shows `[RT]` badge on roundtable sessions
|
|
941
|
+
- [ ] TUI plugin shows `← Back` link on child sessions
|
|
942
|
+
- [ ] `/roundtables` slash command opens session list dialog
|
|
943
|
+
|
|
944
|
+
---
|
|
945
|
+
|
|
946
|
+
## 13. Glossary
|
|
947
|
+
|
|
948
|
+
| Term | Definition |
|
|
949
|
+
|------|------------|
|
|
950
|
+
| **S1** | Main session, where the user interacts |
|
|
951
|
+
| **S2** | Child session, where the roundtable runs |
|
|
952
|
+
| **Round** | One round = all agents speak once |
|
|
953
|
+
| **Turn** | A specific agent's turn to speak |
|
|
954
|
+
| **Observer** | Agent that does not debate, only summarizes |
|
|
955
|
+
| **Orchestrator** | Agent that called `roundtable()` |
|
|
956
|
+
| **History** | Accumulated discussion entries (text + tool summaries) |
|
|
957
|
+
| **Phase** | Current roundtable state (`active`, `observing`, `done`, `aborted`) |
|
|
958
|
+
| **Extend** | Continue a concluded roundtable with more rounds |
|
|
959
|
+
| **noReply** | Injected message that does not trigger an AI response |
|
|
960
|
+
| **ToolCallSummary** | Record of a tool used by an agent: name + output preview (500 chars) |
|
|
961
|
+
| **Jaccard (bigrams)** | Text similarity algorithm used in loop detection: `|A∩B|/|A∪B|` over character pairs |
|
|
962
|
+
| **Default observer** | Built-in plugin mechanism that consolidates the debate into an executive summary without relying on an external agent |
|
|
963
|
+
| **State file** | JSON file at `~/.config/opencode/roundtable-states/<sessionID>.json` containing full `RoundtableState` |
|
|
964
|
+
| **scanOrphanRoundtables** | Startup process that loads all state files into the in-memory Map |
|
|
965
|
+
|
|
966
|
+
---
|
|
967
|
+
|
|
968
|
+
## 14. References
|
|
969
|
+
|
|
970
|
+
- [OpenCode Plugin SDK](https://opencode.ai/docs/sdk/)
|
|
971
|
+
- [OpenCode Plugin API](https://opencode.ai/docs/plugins/)
|
|
972
|
+
- [OpenCode Agent Config](https://opencode.ai/docs/agents/)
|
|
973
|
+
- [OpenCode SDK Types (GitHub)](https://github.com/anomalyco/opencode/tree/dev/packages/sdk/js/src)
|
|
974
|
+
- `@opencode-ai/sdk` types
|