@rrr2010/opencode-roundtable 0.2.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/README.md +198 -0
- package/dist/index.js +1032 -0
- package/dist/index.js.map +15 -0
- package/docs/SPEC.md +979 -0
- package/docs/roundtable.schema.json +42 -0
- package/package.json +53 -0
package/docs/SPEC.md
ADDED
|
@@ -0,0 +1,979 @@
|
|
|
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. Visual Interface](#10-visual-interface)
|
|
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
|
+
## 2. Architecture
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
┌──────────────────────────────────────────────────────────────────┐
|
|
55
|
+
│ MAIN SESSION (S1) │
|
|
56
|
+
│ ┌────────────────────────────────────────────────────────────┐ │
|
|
57
|
+
│ │ build/plan: roundtable({agents, prompt, rounds, observer?})│ │
|
|
58
|
+
│ └────────────────────────────────────────────────────────────┘ │
|
|
59
|
+
│ │ │
|
|
60
|
+
│ │ plugin creates │
|
|
61
|
+
│ ▼ │
|
|
62
|
+
│ ┌────────────────────────────────────────────────────────────┐ │
|
|
63
|
+
│ │ ROUNDTABLE SESSION (S2) — parentID: S1 │ │
|
|
64
|
+
│ │ │ │
|
|
65
|
+
│ │ [noReply] Debate rules / serialized state │ │
|
|
66
|
+
│ │ [agent:PM] Round 1 — PM responds │ │
|
|
67
|
+
│ │ [agent:DEV] Round 1 — DEV responds (sees history) │ │
|
|
68
|
+
│ │ [agent:RV] Round 1 — RV responds │ │
|
|
69
|
+
│ │ ... N rounds ... │ │
|
|
70
|
+
│ │ [observer] Consolidated summary │ │
|
|
71
|
+
│ │ [noReply] ━━━ Roundtable Concluded ━━━ (delimiter) │ │
|
|
72
|
+
│ │ │ │
|
|
73
|
+
│ └────────────────────────────────────────────────────────────┘ │
|
|
74
|
+
│ │ │
|
|
75
|
+
│ │ plugin injects summary │
|
|
76
|
+
│ ▼ │
|
|
77
|
+
│ [noReply] Consolidated result │
|
|
78
|
+
└──────────────────────────────────────────────────────────────────┘
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Session relationship
|
|
82
|
+
|
|
83
|
+
- **S1 (Main):** where the user interacts. The session that calls `roundtable()`.
|
|
84
|
+
- **S2 (Roundtable):** created via `session.create({ parentID: S1 })`. Appears
|
|
85
|
+
as a child session in navigation (`<Leader>+Right/Left`).
|
|
86
|
+
- The plugin listens to events from BOTH: `session.idle` on S2 for sequencing,
|
|
87
|
+
`session.deleted` on S1 or S2 for cleanup.
|
|
88
|
+
|
|
89
|
+
### Execution model (event-driven)
|
|
90
|
+
|
|
91
|
+
The OpenCode V2 server already operates **asynchronously by design**:
|
|
92
|
+
|
|
93
|
+
1. `session.prompt()` returns immediately with an `Admitted` record
|
|
94
|
+
2. The agent execution loop runs in the background
|
|
95
|
+
3. When processing finishes, a `session.idle { sessionID }` event is emitted
|
|
96
|
+
4. The plugin listens to these events to advance the round-robin
|
|
97
|
+
|
|
98
|
+
This makes the roundtable **native to OpenCode's model** — no polling or
|
|
99
|
+
blocking required.
|
|
100
|
+
|
|
101
|
+
### Why `session.create()` and not `session.fork()`?
|
|
102
|
+
|
|
103
|
+
`fork()` copies ALL history from the current session, including tool calls,
|
|
104
|
+
user conversation, etc. This pollutes the roundtable context.
|
|
105
|
+
|
|
106
|
+
`create()` starts from scratch. The plugin builds the debate history manually,
|
|
107
|
+
including only what matters.
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## 3. Tool API
|
|
112
|
+
|
|
113
|
+
### Roundtable tool
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
roundtable({
|
|
117
|
+
agents?: string[], // Names in speaking order (min 2). Required for new debates.
|
|
118
|
+
prompt: string, // Topic/challenge. For multi-round, include per-round instructions.
|
|
119
|
+
rounds?: number, // Complete rounds (default: 1, max: 50)
|
|
120
|
+
observer?: string, // Agent for final consolidation (default: built-in)
|
|
121
|
+
sessionID?: string, // ses_xxxx — pass to extend a concluded roundtable
|
|
122
|
+
title?: string, // Custom title (auto-generated if omitted)
|
|
123
|
+
})
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
**Returns:** `{ sessionID: string, summary: string }` after the debate concludes.
|
|
127
|
+
|
|
128
|
+
**Side effect:** injects system-level prompts into each agent's context during the debate (role-setting, topic, turn routing, and lifecycle signals). The `[ROUNDTABLE META]` block is internal routing data — agents should ignore it for analysis purposes.
|
|
129
|
+
|
|
130
|
+
### Parameters
|
|
131
|
+
|
|
132
|
+
| Parameter | Type | Required | Default | Description |
|
|
133
|
+
|-----------|------|----------|---------|-------------|
|
|
134
|
+
| `agents` | `string[]` | No* | — | Agent names in speaking order (min 2, schema `minItems:2`). Required for new debates; omit when extending |
|
|
135
|
+
| `prompt` | `string` | Yes | — | Topic/challenge. For multi-round, include per-round instructions. All agents see the full agenda |
|
|
136
|
+
| `rounds` | `number` | No | `1` | Number of complete rounds (schema `minimum:1`, `maximum:50`) |
|
|
137
|
+
| `observer` | `string` | No | `built-in` | Agent name for final consolidation. Observer does not debate — it summarizes after all rounds |
|
|
138
|
+
| `sessionID` | `string` | No* | — | Session ID (format: `ses_xxxx`). Pass to extend a concluded roundtable. Omit (and pass `agents`) for a fresh debate |
|
|
139
|
+
| `title` | `string` | No | Auto | Custom title (max 200 chars). Auto-generated as `"(Roundtable) - {first 80 chars of prompt, truncated at word boundary}"` if omitted |
|
|
140
|
+
|
|
141
|
+
### Available agents tool
|
|
142
|
+
|
|
143
|
+
A complementary tool lets the orchestrator discover which agents exist:
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
available_agents()
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**Returns** a formatted list of configured agent names:
|
|
150
|
+
|
|
151
|
+
```
|
|
152
|
+
Available agents: pm, dev, rv, plan, build
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
This helps the orchestrator (e.g., `build`) know which names to pass in
|
|
156
|
+
`agents` when calling `roundtable()`.
|
|
157
|
+
|
|
158
|
+
The tool infers the operation mode from the presence of `sessionID`:
|
|
159
|
+
- **No `sessionID`** → starts a fresh debate (requires `agents`)
|
|
160
|
+
- **`sessionID` present** → continues a concluded roundtable (uses stored agent config)
|
|
161
|
+
|
|
162
|
+
### Usage examples
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
// Basic: 1 round, built-in observer (default)
|
|
166
|
+
roundtable({
|
|
167
|
+
agents: ["pm", "dev", "rv"],
|
|
168
|
+
prompt: "What architecture should we use for the payments module?",
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
// Explicit observer and 2 rounds
|
|
172
|
+
roundtable({
|
|
173
|
+
agents: ["pm", "dev"],
|
|
174
|
+
prompt: "Should we migrate to microservices?",
|
|
175
|
+
rounds: 2,
|
|
176
|
+
observer: "rv", // uses agent "rv" as observer
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
// Built-in observer with custom title
|
|
180
|
+
roundtable({
|
|
181
|
+
agents: ["pm", "dev"],
|
|
182
|
+
prompt: "Which ORM should we use?",
|
|
183
|
+
title: "Debate: ORM vs Raw SQL",
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
// Extending a previous roundtable (no agents — uses original config)
|
|
187
|
+
roundtable({
|
|
188
|
+
sessionID: "abc123",
|
|
189
|
+
rounds: 2,
|
|
190
|
+
prompt: "Dive deeper into operational costs",
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
// Discovering agents before calling roundtable
|
|
194
|
+
// Orchestrator can call available_agents() first, then:
|
|
195
|
+
roundtable({
|
|
196
|
+
agents: ["pm", "dev"], // now it knows these names exist
|
|
197
|
+
prompt: "...",
|
|
198
|
+
})
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Return value (tool execute)
|
|
202
|
+
|
|
203
|
+
The tool blocks until the debate concludes and returns an object with:
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
{
|
|
207
|
+
sessionID: string, // S2 session ID — use for extend mode
|
|
208
|
+
summary: string // Consolidated executive summary from the observer
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
If an error occurs (invalid agents, session not found, etc.) the tool returns
|
|
213
|
+
an error string prefixed with `"Error:"`. The tool never hangs — error returns
|
|
214
|
+
are immediate.
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## 4. Lifecycle
|
|
219
|
+
|
|
220
|
+
### Full flow (fresh debate)
|
|
221
|
+
|
|
222
|
+
```
|
|
223
|
+
PHASE 1 — INITIALIZATION
|
|
224
|
+
─────────────────────────
|
|
225
|
+
1. Orchestrator agent (e.g., build) calls roundtable()
|
|
226
|
+
2. Plugin validates agents against ctx.client.app.agents()
|
|
227
|
+
3. Plugin creates S2 via session.create({ parentID: S1, title })
|
|
228
|
+
4. Plugin serializes initial state as noReply in S2
|
|
229
|
+
5. Plugin sends initial prompt to agents[0] via session.prompt({agent})
|
|
230
|
+
(Prompt returns immediately — LLM runs in background)
|
|
231
|
+
6. roundtable() returns confirmation
|
|
232
|
+
7. session.idle fires on S1 (no active S1 state → ignored)
|
|
233
|
+
|
|
234
|
+
PHASE 2 — DEBATE (repeats for each round)
|
|
235
|
+
──────────────────────────────────────────
|
|
236
|
+
8. agents[i] finishes → session.idle fires on S2
|
|
237
|
+
9. Plugin reads response with session.messages(S2)
|
|
238
|
+
10. Plugin accumulates into internal history[] (text + tool outputs)
|
|
239
|
+
11. Plugin decides next step:
|
|
240
|
+
a) Still have agents in this round? → agents[i+1]
|
|
241
|
+
b) Round complete + more rounds left? → agents[0], round++
|
|
242
|
+
c) All rounds complete? → PHASE 3 (observer always runs)
|
|
243
|
+
|
|
244
|
+
PHASE 3 — OBSERVER
|
|
245
|
+
──────────────────
|
|
246
|
+
12. Plugin decides which observer to use:
|
|
247
|
+
a) Explicit observer? → sends full history to that agent
|
|
248
|
+
b) Default observer? → sends to S2 with built-in plugin prompt
|
|
249
|
+
13. Observer consolidates → session.idle
|
|
250
|
+
14. Plugin reads summary → PHASE 4
|
|
251
|
+
|
|
252
|
+
PHASE 4 — FINALIZATION
|
|
253
|
+
──────────────────────
|
|
254
|
+
15. Plugin extracts consolidated summary
|
|
255
|
+
16. Plugin injects into S1 via session.prompt({noReply: true})
|
|
256
|
+
17. Plugin re-serializes final state into S2 (phase: done, full history)
|
|
257
|
+
→ replaces the stale initial meta so extend mode finds correct state
|
|
258
|
+
18. Plugin injects delimiter noReply in S2:
|
|
259
|
+
"━━━ Roundtable Concluded ━━━
|
|
260
|
+
Messages below this line are not part of the original debate.
|
|
261
|
+
The result was consolidated in the main session."
|
|
262
|
+
19. Plugin updates S2 title: "Roundtable: A vs B · CONCLUDED"
|
|
263
|
+
20. Plugin shows toast "Roundtable concluded"
|
|
264
|
+
21. Plugin clears in-memory state for S2
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### Flow (extend)
|
|
268
|
+
|
|
269
|
+
```
|
|
270
|
+
1. Orchestrator calls roundtable({sessionID, rounds, prompt})
|
|
271
|
+
2. Plugin fetches serialized state from S2 (from the noReply tag)
|
|
272
|
+
3. Plugin restores history + config of the original roundtable
|
|
273
|
+
4. Adds new rounds to totalRounds (accumulative)
|
|
274
|
+
5. Sends continuation prompt to agents[0]
|
|
275
|
+
6. Returns to PHASE 2 of the normal flow
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
---
|
|
279
|
+
|
|
280
|
+
## 5. States
|
|
281
|
+
|
|
282
|
+
### Roundtable state machine
|
|
283
|
+
|
|
284
|
+
```
|
|
285
|
+
┌────────────┐
|
|
286
|
+
│ PENDING │ (initial state, before S2 creation)
|
|
287
|
+
└─────┬──────┘
|
|
288
|
+
│ session.create() + prompt[0]
|
|
289
|
+
▼
|
|
290
|
+
┌────────────┐
|
|
291
|
+
│ ACTIVE │ (debate in progress)
|
|
292
|
+
└─────┬──────┘
|
|
293
|
+
│ all rounds complete
|
|
294
|
+
▼
|
|
295
|
+
┌────────────┐
|
|
296
|
+
│ OBSERVING │ (observer — built-in or explicit — always runs)
|
|
297
|
+
└─────┬──────┘
|
|
298
|
+
│ observer summarizes
|
|
299
|
+
▼
|
|
300
|
+
┌────────────┐
|
|
301
|
+
│ DONE │
|
|
302
|
+
└────────────┘
|
|
303
|
+
|
|
304
|
+
At any state:
|
|
305
|
+
│ session.deleted
|
|
306
|
+
▼
|
|
307
|
+
┌────────────┐
|
|
308
|
+
│ ABORTED │
|
|
309
|
+
└────────────┘
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
> **Note**: unlike the previous spec, `OBSERVING` **always** runs.
|
|
313
|
+
> The built-in default observer guarantees every debate produces a
|
|
314
|
+
> consolidated summary at the end.
|
|
315
|
+
|
|
316
|
+
### In-memory state type
|
|
317
|
+
|
|
318
|
+
```typescript
|
|
319
|
+
interface RoundtableState {
|
|
320
|
+
// Identification
|
|
321
|
+
sessionID: string // S2
|
|
322
|
+
parentSessionID: string // S1
|
|
323
|
+
|
|
324
|
+
// Config
|
|
325
|
+
agents: string[]
|
|
326
|
+
totalRounds: number
|
|
327
|
+
observer: "built-in" | string // "built-in" = plugin default observer
|
|
328
|
+
prompt: string
|
|
329
|
+
|
|
330
|
+
// Progress
|
|
331
|
+
currentRound: number // 0-indexed
|
|
332
|
+
currentAgentIndex: number // 0-indexed
|
|
333
|
+
phase: "active" | "observing" | "done" | "aborted"
|
|
334
|
+
|
|
335
|
+
// Data
|
|
336
|
+
history: HistoryEntry[]
|
|
337
|
+
errors: string[]
|
|
338
|
+
createdAt: number
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
interface HistoryEntry {
|
|
342
|
+
agent: string
|
|
343
|
+
round: number
|
|
344
|
+
response: string // plain text of the response
|
|
345
|
+
toolCalls: ToolCallSummary[] // tool names + output previews
|
|
346
|
+
hasError: boolean
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
interface ToolCallSummary {
|
|
350
|
+
toolName: string
|
|
351
|
+
outputPreview: string // first 500 chars of output, or "error"
|
|
352
|
+
}
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
### Serialization in S2
|
|
356
|
+
|
|
357
|
+
State is serialized into a `noReply` message at the start of S2 to survive
|
|
358
|
+
restarts and enable the `extend` mode. The tag includes an "IGNORE FOR
|
|
359
|
+
ANALYSIS" note so agents know this is internal routing data, not part of
|
|
360
|
+
the debate topic:
|
|
361
|
+
|
|
362
|
+
```
|
|
363
|
+
[ROUNDTABLE META — INTERNAL ORCHESTRATION DATA — IGNORE FOR ANALYSIS]
|
|
364
|
+
{"sessionID":"S2","agents":["pm","dev"],"totalRounds":2,
|
|
365
|
+
"observer":"built-in","prompt":"...","currentRound":1,
|
|
366
|
+
"currentAgentIndex":0,"phase":"active","history":[...]}
|
|
367
|
+
[/ROUNDTABLE META]
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
The state is re-serialized when the debate concludes (`finalizeRoundtable`),
|
|
371
|
+
overwriting the initial meta with `phase: "done"` and the full history.
|
|
372
|
+
This ensures extend mode always reads the most recent state.
|
|
373
|
+
|
|
374
|
+
The parsing functions (`findSerializedState`, `deserializeState`) search
|
|
375
|
+
for the last occurrence of the `[ROUNDTABLE META` prefix to handle both
|
|
376
|
+
old and new format, and to prefer the newest state when multiple metas
|
|
377
|
+
exist in the same session.
|
|
378
|
+
|
|
379
|
+
---
|
|
380
|
+
|
|
381
|
+
## 6. History & Context
|
|
382
|
+
|
|
383
|
+
### Content sent to each agent
|
|
384
|
+
|
|
385
|
+
Each time the plugin passes the turn (via `buildAgentPrompt`), it sends a
|
|
386
|
+
minimal message with:
|
|
387
|
+
|
|
388
|
+
```
|
|
389
|
+
[System] You are participating in a roundtable debate. Agents (A → B → C) will discuss the topic below in N round(s).
|
|
390
|
+
[System] The [ROUNDTABLE META] block above is internal routing data — ignore it for all analysis. Focus only on the topic given in [Topic] below.
|
|
391
|
+
[Topic] {prompt}
|
|
392
|
+
|
|
393
|
+
Round N/M | Current agent: Z
|
|
394
|
+
|
|
395
|
+
[System] This is the last prompt — finalize your thoughts (last round only)
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
The `[ROUNDTABLE META]` block is injected separately as a `noReply` user
|
|
399
|
+
message. It contains orchestration state (session ID, agents, rounds,
|
|
400
|
+
phase, history, errors, timestamps) and is marked as internal — agents
|
|
401
|
+
are instructed to ignore it for analysis purposes.
|
|
402
|
+
|
|
403
|
+
**History is NOT currently injected into the agent prompt** — the agent
|
|
404
|
+
only sees its current turn context. History accumulation happens server-side
|
|
405
|
+
in the plugin state and is used by the observer for consolidation. History
|
|
406
|
+
injection into subsequent rounds is a planned enhancement.
|
|
407
|
+
|
|
408
|
+
### What the plugin includes in history
|
|
409
|
+
|
|
410
|
+
The accumulated history contains **everything relevant for the next debater**:
|
|
411
|
+
|
|
412
|
+
- `TextPart.text` — agent's textual response
|
|
413
|
+
- `ToolCallSummary` — tool name + **output preview** (first 500 characters).
|
|
414
|
+
If DEV ran `ls src/`, the next agent sees the discovered directory structure.
|
|
415
|
+
- `FilePart` — file references (name only, not full content)
|
|
416
|
+
|
|
417
|
+
### What the plugin does NOT include in history
|
|
418
|
+
|
|
419
|
+
- `ReasoningPart.text` — agent's internal reasoning (stays in S2 session, but
|
|
420
|
+
not injected into the next agent's prompt text; the next agent can browse
|
|
421
|
+
S2 to see it)
|
|
422
|
+
- `StepStartPart` / `StepFinishPart` — execution metadata
|
|
423
|
+
|
|
424
|
+
### Why include tool outputs in the prompt text?
|
|
425
|
+
|
|
426
|
+
Debaters need to see **what was discovered**, not just what was said.
|
|
427
|
+
If DEV ran `ls src/` and found the project structure, PM should see that
|
|
428
|
+
finding to base their argument on it. This is why tool outputs enter the
|
|
429
|
+
textual summary the plugin builds (500-char preview).
|
|
430
|
+
|
|
431
|
+
What the plugin controls is the format: outputs are included as part of each
|
|
432
|
+
agent's discussion block, not as standalone tool calls.
|
|
433
|
+
|
|
434
|
+
---
|
|
435
|
+
|
|
436
|
+
## 7. Edge Case Handling
|
|
437
|
+
|
|
438
|
+
### 7.1 Agent failure (provider error)
|
|
439
|
+
|
|
440
|
+
```
|
|
441
|
+
session.error on S2 with agent = currentAgent
|
|
442
|
+
→ Logs error in state.errors[]
|
|
443
|
+
→ Skips to next agent
|
|
444
|
+
→ Toast: "DEV failed on Round 2. Skipping to RV."
|
|
445
|
+
→ If all agents fail → aborts with error
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
### 7.2 User closes S2 (session.deleted)
|
|
449
|
+
|
|
450
|
+
```
|
|
451
|
+
session.deleted on S2
|
|
452
|
+
→ phase = "aborted"
|
|
453
|
+
→ state.history still has partial responses
|
|
454
|
+
→ Injects into S1:
|
|
455
|
+
"[Roundtable interrupted — session closed.
|
|
456
|
+
Partial history up to interruption:]
|
|
457
|
+
{partial history}"
|
|
458
|
+
→ Toast: "Roundtable interrupted"
|
|
459
|
+
→ Clears state
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
### 7.3 User closes S1 (session.deleted on S1)
|
|
463
|
+
|
|
464
|
+
```
|
|
465
|
+
session.deleted on S1 === state.parentSessionID
|
|
466
|
+
→ Aborts S2 via session.abort({path:{id:S2}})
|
|
467
|
+
→ Clears state
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
### 7.4 User types a message in S2 during the debate
|
|
471
|
+
|
|
472
|
+
S2 is a visible and interactive session. If the user switches to S2
|
|
473
|
+
and types something:
|
|
474
|
+
|
|
475
|
+
- The message enters S2's context
|
|
476
|
+
- If the current agent is still processing, the user's message
|
|
477
|
+
will be processed after the agent finishes
|
|
478
|
+
- The plugin continues the round-robin — the user's message becomes
|
|
479
|
+
part of the history
|
|
480
|
+
- The plugin cannot detect "who" sent the message (there is no
|
|
481
|
+
`trigger` field in the API)
|
|
482
|
+
|
|
483
|
+
**Behavior**: natural and desired. The user can intervene in the debate.
|
|
484
|
+
|
|
485
|
+
### 7.5 Debate loop (agents repeating arguments)
|
|
486
|
+
|
|
487
|
+
After each round, the plugin checks similarity between the latest response
|
|
488
|
+
and previous ones using **Jaccard similarity of bigrams (character pairs)**:
|
|
489
|
+
|
|
490
|
+
```
|
|
491
|
+
tokens1 = set(bigrams(current_text))
|
|
492
|
+
tokens2 = set(bigrams(previous_text))
|
|
493
|
+
similarity = |tokens1 ∩ tokens2| / |tokens1 ∪ tokens2|
|
|
494
|
+
|
|
495
|
+
if similarity > 0.85 (LOOP_SIMILARITY_THRESHOLD):
|
|
496
|
+
→ "Roundtable ended due to repetition — agents reached an impasse"
|
|
497
|
+
→ Injects partial result into S1
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
**Why Jaccard bigrams?**
|
|
501
|
+
- Purely computational (no LLM calls or embeddings needed)
|
|
502
|
+
- Zero external dependencies
|
|
503
|
+
- Reasonable for detecting textual argument repetition
|
|
504
|
+
- Threshold 0.85 is conservative — adjustable via constant
|
|
505
|
+
|
|
506
|
+
**Considerations**: the algorithm operates on cleaned text (without template
|
|
507
|
+
formatting). Comparison is between the last response of the current round
|
|
508
|
+
and the last response of the previous round, not all combinations.
|
|
509
|
+
|
|
510
|
+
### 7.6 Agent timeout
|
|
511
|
+
|
|
512
|
+
- Each agent turn has an implicit **5-minute** timeout
|
|
513
|
+
- Implemented via AbortController + setTimeout
|
|
514
|
+
- If it expires: `session.abort({path:{id:S2}})` → skip to next agent
|
|
515
|
+
|
|
516
|
+
### 7.7 Multiple simultaneous roundtables
|
|
517
|
+
|
|
518
|
+
Each roundtable has its own state in `Map<sessionID, RoundtableState>`.
|
|
519
|
+
They are independent. The plugin allows multiple roundtables running in
|
|
520
|
+
parallel.
|
|
521
|
+
|
|
522
|
+
### 7.8 Volatile state (OpenCode restart)
|
|
523
|
+
|
|
524
|
+
If OpenCode restarts mid-roundtable:
|
|
525
|
+
- In-memory state is lost
|
|
526
|
+
- S2 session remains in the database
|
|
527
|
+
- `noReply` messages with serialized state persist in S2
|
|
528
|
+
|
|
529
|
+
**Recovery**: on initialization, the plugin runs `scanOrphanRoundtables()`:
|
|
530
|
+
```
|
|
531
|
+
1. List all sessions via session.list()
|
|
532
|
+
2. For each session, search for messages tagged [ROUNDTABLE META]
|
|
533
|
+
3. If found:
|
|
534
|
+
a) Check if parent S1 still exists
|
|
535
|
+
b) If S1 exists → notify: "Roundtable #abc123 was interrupted by
|
|
536
|
+
restart. Use mode:'extend' to continue."
|
|
537
|
+
c) If S1 does not exist → abort S2 and clean up
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
### 7.9 Compaction during a roundtable
|
|
541
|
+
|
|
542
|
+
- The plugin does NOT trigger compaction automatically
|
|
543
|
+
- If the user or system compacts S2, `noReply` messages with serialized
|
|
544
|
+
state survive (they are user messages in the session)
|
|
545
|
+
- The plugin uses `experimental.session.compacting` to re-inject critical
|
|
546
|
+
state into the compaction prompt
|
|
547
|
+
|
|
548
|
+
### 7.10 Human message in S2 after conclusion
|
|
549
|
+
|
|
550
|
+
After `phase = "done"` (roundtable concluded):
|
|
551
|
+
- The plugin has already injected the `━━━ Roundtable Concluded ━━━`
|
|
552
|
+
delimiter in S2
|
|
553
|
+
- The plugin stops listening to S2 events
|
|
554
|
+
- If the user types in S2, OpenCode responds normally with the session's
|
|
555
|
+
default agent — **the plugin no longer interferes**
|
|
556
|
+
- The response **does not** alter the result already consolidated in S1
|
|
557
|
+
|
|
558
|
+
```
|
|
559
|
+
[noReply] ━━━ Roundtable Concluded ━━━
|
|
560
|
+
Messages below this line are not part of the original debate.
|
|
561
|
+
The result was consolidated in the main session.
|
|
562
|
+
── You ──
|
|
563
|
+
(conversation can continue here, but the plugin no longer
|
|
564
|
+
manages this session.)
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
**Behavior**: natural and transparent. S2 becomes a regular session after
|
|
568
|
+
the roundtable concludes.
|
|
569
|
+
|
|
570
|
+
---
|
|
571
|
+
|
|
572
|
+
## 8. Observer
|
|
573
|
+
|
|
574
|
+
### Function
|
|
575
|
+
|
|
576
|
+
The observer **always consolidates** the debate at the end. It does not
|
|
577
|
+
participate in rounds — it enters after all rounds are complete to produce
|
|
578
|
+
an executive summary.
|
|
579
|
+
|
|
580
|
+
### Default observer (built-in)
|
|
581
|
+
|
|
582
|
+
If the orchestrator does not specify `observer`, the plugin uses a built-in
|
|
583
|
+
observer with the following prompt:
|
|
584
|
+
|
|
585
|
+
```
|
|
586
|
+
DEFAULT_OBSERVER_PROMPT = `
|
|
587
|
+
You are an impartial roundtable observer.
|
|
588
|
+
Consolidate the debate below into:
|
|
589
|
+
|
|
590
|
+
1. **Executive summary** (2-3 sentences)
|
|
591
|
+
2. **Key points** raised by each participant
|
|
592
|
+
3. **Decisions or convergences** reached
|
|
593
|
+
4. **Remaining open questions**
|
|
594
|
+
5. **Suggested next steps**
|
|
595
|
+
|
|
596
|
+
Debate:
|
|
597
|
+
{full_history}
|
|
598
|
+
`
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
This prompt is sent to the S2 session itself (no specific agent), using the
|
|
602
|
+
session's default model/provider. The plugin extracts the textual response
|
|
603
|
+
as the final summary.
|
|
604
|
+
|
|
605
|
+
### Explicit observer (override)
|
|
606
|
+
|
|
607
|
+
If the orchestrator passes `observer: "rv"`, the plugin:
|
|
608
|
+
1. Builds a similar prompt, but with `Your role: rv. Provide an executive summary...`
|
|
609
|
+
2. Sends via `session.prompt({ agent: "rv" })` on the S2 session
|
|
610
|
+
3. The "rv" agent responds with its own tools and personality
|
|
611
|
+
|
|
612
|
+
### Flow
|
|
613
|
+
|
|
614
|
+
```
|
|
615
|
+
1. All rounds complete → PHASE 3
|
|
616
|
+
2. Plugin decides:
|
|
617
|
+
a) Explicit observer? → sends to that agent
|
|
618
|
+
b) Default observer? → sends DEFAULT_OBSERVER_PROMPT to S2
|
|
619
|
+
3. Observer responds → session.idle
|
|
620
|
+
4. Plugin extracts text → injects into S1 as final result
|
|
621
|
+
5. Plugin injects delimiter noReply into S2
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
---
|
|
625
|
+
|
|
626
|
+
## 9. Extend Mode
|
|
627
|
+
|
|
628
|
+
### Usage
|
|
629
|
+
|
|
630
|
+
```typescript
|
|
631
|
+
roundtable({
|
|
632
|
+
sessionID: "abc123", // Original S2 ID
|
|
633
|
+
rounds: 2,
|
|
634
|
+
prompt: "Dive deeper into operational costs", // new prompt
|
|
635
|
+
})
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
### Prompt semantics in extend mode
|
|
639
|
+
|
|
640
|
+
The `prompt` parameter in `extend` mode can be:
|
|
641
|
+
|
|
642
|
+
| Intent | Example | Effect |
|
|
643
|
+
|--------|---------|--------|
|
|
644
|
+
| **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}"` |
|
|
645
|
+
| **New topic** | "Now plan Y" | New prompt replaces the topic, but previous history is preserved as context. `final_prompt = "Previous discussion history:\n{history}\n\nNew challenge: {prompt}"` |
|
|
646
|
+
|
|
647
|
+
The plugin infers intent heuristically: if the prompt starts with
|
|
648
|
+
"Debate more", "Continue", "Dive deeper" → continuation. Otherwise
|
|
649
|
+
→ new topic. The behavior can be refined during implementation.
|
|
650
|
+
|
|
651
|
+
### Flow
|
|
652
|
+
|
|
653
|
+
```
|
|
654
|
+
1. Plugin fetches S2 by sessionID
|
|
655
|
+
2. Reads S2 → session.messages(S2)
|
|
656
|
+
3. Finds [ROUNDTABLE META ...] tag → deserializes state
|
|
657
|
+
4a. If phase === "done": proceed
|
|
658
|
+
4b. If phase === "active": check if actually running
|
|
659
|
+
→ Count assistant messages in S2 vs expected (totalRounds × agents + observer)
|
|
660
|
+
→ If assistantCount >= expected: session is stuck → auto-recover phase to "done"
|
|
661
|
+
→ Else: return error "still active"
|
|
662
|
+
5. Creates NEW state with:
|
|
663
|
+
- agents, observer, prompt from ORIGINAL
|
|
664
|
+
- rounds += new_rounds (accumulative)
|
|
665
|
+
- history = restored previous history (or empty if missing)
|
|
666
|
+
- extended_prompt = inferred intent (continuation or new topic)
|
|
667
|
+
6. Sets phase = "active"
|
|
668
|
+
7. Sends extended_prompt to agents[0]
|
|
669
|
+
8. Continues normal PHASE 2 flow
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
### Stuck session recovery
|
|
673
|
+
|
|
674
|
+
If an extend is interrupted mid-way (TUI freeze, process kill, etc.), the
|
|
675
|
+
new serialized state in S2 has `phase: "active"` but no agent is running.
|
|
676
|
+
The next extend call detects this by comparing the actual assistant message
|
|
677
|
+
count against the expected count — if the session appears complete, it
|
|
678
|
+
auto-resets to `phase: "done"` and proceeds normally. A log entry is
|
|
679
|
+
emitted with `level: "info"` when recovery occurs.
|
|
680
|
+
|
|
681
|
+
### Constraints
|
|
682
|
+
|
|
683
|
+
- Only works if S2 still exists (was not deleted)
|
|
684
|
+
- Only works if original phase was "done"
|
|
685
|
+
- Agents and observer must be the same as the original roundtable
|
|
686
|
+
- Previous discussion history is always preserved
|
|
687
|
+
- If the original roundtable used an explicit observer, the extend also uses
|
|
688
|
+
it (observer type is stored in serialized state)
|
|
689
|
+
|
|
690
|
+
---
|
|
691
|
+
|
|
692
|
+
## 10. Visual Interface
|
|
693
|
+
|
|
694
|
+
### In the OpenCode TUI
|
|
695
|
+
|
|
696
|
+
| Element | How it works |
|
|
697
|
+
|---------|--------------|
|
|
698
|
+
| Agent colors | Native OpenCode via `agent.color` in config |
|
|
699
|
+
| Navigation | S2 is a child of S1 → `<Leader>+Right/Left` to switch |
|
|
700
|
+
| Toast | Plugin shows toast on start, completion, and errors |
|
|
701
|
+
| S2 title | Default: `"(Roundtable) - {prompt[:57]}..."`. Stays stable during debate. On conclusion: appends ` · CONCLUDED` |
|
|
702
|
+
| Message names | Each response is tagged with the speaking agent's name |
|
|
703
|
+
|
|
704
|
+
### TUI appearance example
|
|
705
|
+
|
|
706
|
+
```
|
|
707
|
+
┌─────────────────────────────────────────────────┐
|
|
708
|
+
│ Main Session (S1) │
|
|
709
|
+
├─────────────────────────────────────────────────┤
|
|
710
|
+
│ You: roundtable({agents:[pm,dev], rounds:2}) │
|
|
711
|
+
│ │
|
|
712
|
+
│ Build: Roundtable started in child session │
|
|
713
|
+
│ #abc123 │
|
|
714
|
+
│ │
|
|
715
|
+
│ [noReply] ── Roundtable Concluded ── │
|
|
716
|
+
│ Topic: What architecture? │
|
|
717
|
+
│ PM: microservices... │
|
|
718
|
+
│ DEV: monolith... │
|
|
719
|
+
│ Conclusion: modular monolith │
|
|
720
|
+
└─────────────────────────────────────────────────┘
|
|
721
|
+
|
|
722
|
+
┌─────────────────────────────────────────────────┐
|
|
723
|
+
│ Roundtable Session #abc123 (child) │
|
|
724
|
+
├─────────────────────────────────────────────────┤
|
|
725
|
+
│ [noReply] Debate rules... │
|
|
726
|
+
│ ── PM ── │
|
|
727
|
+
│ The microservices approach allows... │
|
|
728
|
+
│ ── DEV ── │
|
|
729
|
+
│ Disagree, the complexity is not justified... │
|
|
730
|
+
│ ── PM (R2) ── │
|
|
731
|
+
│ OK, but what if we split only the... │
|
|
732
|
+
│ ── DEV (R2) ── │
|
|
733
|
+
│ That's basically a modular monolith... │
|
|
734
|
+
│ [noReply] ━━━ Roundtable Concluded ━━━ │
|
|
735
|
+
└─────────────────────────────────────────────────┘
|
|
736
|
+
```
|
|
737
|
+
|
|
738
|
+
### Recommended color config
|
|
739
|
+
|
|
740
|
+
```json
|
|
741
|
+
{
|
|
742
|
+
"agent": {
|
|
743
|
+
"pm": { "color": "#3498db" },
|
|
744
|
+
"dev": { "color": "#2ecc71" },
|
|
745
|
+
"rv": { "color": "#e74c3c" },
|
|
746
|
+
"plan": { "color": "#f39c12" },
|
|
747
|
+
"build": { "color": "#9b59b6" }
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
```
|
|
751
|
+
|
|
752
|
+
---
|
|
753
|
+
|
|
754
|
+
## 11. Plugin Structure
|
|
755
|
+
|
|
756
|
+
### Files (local install)
|
|
757
|
+
|
|
758
|
+
```
|
|
759
|
+
~/.config/opencode/plugins/
|
|
760
|
+
└── roundtable.ts # Single-file plugin (dev)
|
|
761
|
+
```
|
|
762
|
+
|
|
763
|
+
### Files (npm package — planned)
|
|
764
|
+
|
|
765
|
+
```
|
|
766
|
+
opencode-roundtable/
|
|
767
|
+
├── package.json # name, version, main: dist/index.js, files: ["dist"]
|
|
768
|
+
├── tsconfig.json # Compiles src/ → dist/
|
|
769
|
+
├── src/
|
|
770
|
+
│ ├── index.ts # Plugin export (entry point)
|
|
771
|
+
│ ├── types.ts # Interfaces: RoundtableState, HistoryEntry, ToolCallSummary, etc.
|
|
772
|
+
│ ├── config.ts # loadConfig, validateConfig, defaults
|
|
773
|
+
│ ├── state.ts # Module-level maps + serializeState / deserializeState
|
|
774
|
+
│ ├── prompts.ts # buildAgentPrompt, buildObserverPrompt
|
|
775
|
+
│ ├── events.ts # Event handlers (processNextTurn, handleAgentError, handleSessionDeleted)
|
|
776
|
+
│ ├── extend.ts # extendRoundtable + helpers
|
|
777
|
+
│ ├── roundtable-core.ts # startNewRoundtable, sendToAgent, finalizeRoundtable
|
|
778
|
+
│ └── utils.ts # detectLoop, extractResponse, generateDefaultTitle, etc.
|
|
779
|
+
├── docs/
|
|
780
|
+
│ ├── SPEC.md
|
|
781
|
+
│ └── roundtable.schema.json
|
|
782
|
+
└── README.md
|
|
783
|
+
```
|
|
784
|
+
|
|
785
|
+
### Dependencies
|
|
786
|
+
|
|
787
|
+
None external. Only `@opencode-ai/plugin` (peer dependency of OpenCode).
|
|
788
|
+
|
|
789
|
+
### Code structure
|
|
790
|
+
|
|
791
|
+
```typescript
|
|
792
|
+
// roundtable.ts
|
|
793
|
+
import { type Plugin, tool } from "@opencode-ai/plugin"
|
|
794
|
+
|
|
795
|
+
// == Types ==
|
|
796
|
+
interface RoundtableState { ... }
|
|
797
|
+
interface HistoryEntry { ... }
|
|
798
|
+
interface ToolCallSummary { ... }
|
|
799
|
+
type Phase = "active" | "observing" | "done" | "aborted"
|
|
800
|
+
|
|
801
|
+
// == Constants ==
|
|
802
|
+
const AGENT_TIMEOUT_MS = 300_000 // 5 min
|
|
803
|
+
const LOOP_SIMILARITY_THRESHOLD = 0.85 // Jaccard bigrams
|
|
804
|
+
const TOOL_OUTPUT_PREVIEW_MAX = 500 // tool output preview chars
|
|
805
|
+
const DEFAULT_OBSERVER_PROMPT = `You are an impartial roundtable observer...`
|
|
806
|
+
|
|
807
|
+
// == Plugin ==
|
|
808
|
+
export const RoundtablePlugin: Plugin = async (ctx) => {
|
|
809
|
+
const states = new Map<string, RoundtableState>()
|
|
810
|
+
|
|
811
|
+
// Init: scan for orphan roundtables (see 7.8)
|
|
812
|
+
scanOrphanRoundtables(ctx, states)
|
|
813
|
+
|
|
814
|
+
return {
|
|
815
|
+
event: async ({ event }) => {
|
|
816
|
+
if (!event.properties?.sessionID) return
|
|
817
|
+
// session.idle → processNextTurn()
|
|
818
|
+
// session.error → handleAgentError()
|
|
819
|
+
// session.deleted → handleSessionDeleted()
|
|
820
|
+
},
|
|
821
|
+
|
|
822
|
+
"experimental.session.compacting": async (input, output) => {
|
|
823
|
+
// Re-inject critical state during compaction
|
|
824
|
+
},
|
|
825
|
+
|
|
826
|
+
tool: {
|
|
827
|
+
roundtable: tool({
|
|
828
|
+
description: "Starts a multi-agent roundtable debate. Agents take turns discussing a topic.",
|
|
829
|
+
args: {
|
|
830
|
+
agents: tool.schema.array(tool.schema.string()).min(2),
|
|
831
|
+
prompt: tool.schema.string(),
|
|
832
|
+
rounds: tool.schema.number().min(1).default(1),
|
|
833
|
+
observer: tool.schema.string().optional(),
|
|
834
|
+
sessionID: tool.schema.string().optional(),
|
|
835
|
+
title: tool.schema.string().optional(),
|
|
836
|
+
},
|
|
837
|
+
async execute(args, toolCtx) {
|
|
838
|
+
if (args.sessionID) return extendRoundtable(ctx, args, toolCtx, states)
|
|
839
|
+
return startNewRoundtable(ctx, args, toolCtx, states)
|
|
840
|
+
},
|
|
841
|
+
}),
|
|
842
|
+
|
|
843
|
+
available_agents: tool({
|
|
844
|
+
description: "Lists all configured agents that can participate in a roundtable.",
|
|
845
|
+
args: {},
|
|
846
|
+
async execute(_args, toolCtx) {
|
|
847
|
+
const agents = await ctx.client.app.agents()
|
|
848
|
+
return `Available agents: ${agents.map(a => a.name).join(", ")}`
|
|
849
|
+
},
|
|
850
|
+
}),
|
|
851
|
+
},
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// == Validation ==
|
|
856
|
+
async function validateAgents(ctx, agentNames: string[]): Promise<ValidationResult> {
|
|
857
|
+
// 1. Calls ctx.client.app.agents() → gets real agent list
|
|
858
|
+
// 2. Checks each name in agentNames
|
|
859
|
+
// 3. Checks for duplicates
|
|
860
|
+
// 4. Checks min 2 agents
|
|
861
|
+
// 5. Returns { valid, available, errors }
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// == Helper functions ==
|
|
865
|
+
async function startNewRoundtable(...) { ... }
|
|
866
|
+
async function extendRoundtable(...) { ... }
|
|
867
|
+
async function processNextTurn(...) { ... }
|
|
868
|
+
async function sendToAgent(...) { ... }
|
|
869
|
+
async function finalizeRoundtable(...) { ... }
|
|
870
|
+
async function handleAgentError(...) { ... }
|
|
871
|
+
async function handleSessionDeleted(...) { ... }
|
|
872
|
+
function buildAgentPrompt(...): string { ... }
|
|
873
|
+
function buildObserverPrompt(...): string { ... }
|
|
874
|
+
function extractResponse(...): string | null { ... }
|
|
875
|
+
function detectLoop(...): boolean { ... } // Jaccard bigrams
|
|
876
|
+
function buildToolSummary(...): ToolCallSummary { ... }
|
|
877
|
+
function injectRoundtableDelimiter(...) { ... }
|
|
878
|
+
function scanOrphanRoundtables(...) { ... }
|
|
879
|
+
function generateDefaultTitle(...): string { ... }
|
|
880
|
+
function serializeState(...): string { ... }
|
|
881
|
+
function deserializeState(...): RoundtableState { ... }
|
|
882
|
+
```
|
|
883
|
+
|
|
884
|
+
### Hooks used
|
|
885
|
+
|
|
886
|
+
| Hook | Purpose |
|
|
887
|
+
|------|---------|
|
|
888
|
+
| `event` | Listens to `session.idle`, `session.error`, `session.deleted` |
|
|
889
|
+
| `experimental.session.compacting` | Preserves state during compaction |
|
|
890
|
+
| `tool` | Defines the `roundtable` and `available_agents` tools |
|
|
891
|
+
|
|
892
|
+
### SDK APIs used
|
|
893
|
+
|
|
894
|
+
| API | Usage |
|
|
895
|
+
|-----|-------|
|
|
896
|
+
| `ctx.client.session.create()` | Create S2 |
|
|
897
|
+
| `ctx.client.session.prompt()` | Send messages to agents |
|
|
898
|
+
| `ctx.client.session.messages()` | Read agent responses |
|
|
899
|
+
| `ctx.client.session.abort()` | Abort timed-out agent |
|
|
900
|
+
| `ctx.client.session.update()` | Update session title |
|
|
901
|
+
| `ctx.client.tui.showToast()` | Notify user |
|
|
902
|
+
| `ctx.client.app.agents()` | Discover available agents |
|
|
903
|
+
|
|
904
|
+
---
|
|
905
|
+
|
|
906
|
+
## 12. Tests & Validation
|
|
907
|
+
|
|
908
|
+
### Test scenarios
|
|
909
|
+
|
|
910
|
+
1. **Basic roundtable**: 2 agents, 1 round, default observer
|
|
911
|
+
2. **Explicit observer**: 3 agents, 2 rounds, observer="rv" consolidates
|
|
912
|
+
3. **Default observer**: same scenario, no observer param → uses built-in
|
|
913
|
+
4. **Agent failure**: provider error → skip → continue
|
|
914
|
+
5. **Timeout**: agent takes too long → abort + skip
|
|
915
|
+
6. **Loop detection**: agents repeat arguments → Jaccard > 0.85 → end debate
|
|
916
|
+
7. **Session deleted**: close S2 → partial result in S1
|
|
917
|
+
8. **Extend (continuation)**: conclude → extend with "Debate more about X"
|
|
918
|
+
9. **Extend (new topic)**: conclude → extend with "Now plan Y"
|
|
919
|
+
10. **Multiple**: 2 roundtables in parallel
|
|
920
|
+
11. **Compaction**: compact S2 during debate → state preserved
|
|
921
|
+
12. **User interjection**: user writes in S2 during debate → continues
|
|
922
|
+
13. **Post-conclusion**: user writes in S2 after done → S2 becomes normal session
|
|
923
|
+
14. **Startup recovery**: restart during ACTIVE → scanOrphanRoundtables detects
|
|
924
|
+
15. **Invalid agent**: agents with nonexistent name → error with available list
|
|
925
|
+
16. **Available agents tool**: orchestrator calls `available_agents()` → gets list
|
|
926
|
+
|
|
927
|
+
### Acceptance criteria
|
|
928
|
+
|
|
929
|
+
- [ ] `roundtable` tool appears in the agent's tool list
|
|
930
|
+
- [ ] `available_agents` tool appears in the agent's tool list
|
|
931
|
+
- [ ] S2 is created as a child of S1
|
|
932
|
+
- [ ] Agents speak in the specified order
|
|
933
|
+
- [ ] Each agent keeps its own personality
|
|
934
|
+
- [ ] History + tool outputs are sent to each turn
|
|
935
|
+
- [ ] Default observer (built-in) consolidates the debate at the end
|
|
936
|
+
- [ ] Explicit observer (named agent) also works
|
|
937
|
+
- [ ] Final result is injected into S1
|
|
938
|
+
- [ ] Toast notifications appear on start, error, and conclusion
|
|
939
|
+
- [ ] Agent errors are skipped with notification
|
|
940
|
+
- [ ] Extend mode resumes from where it stopped (continuation and new topic)
|
|
941
|
+
- [ ] Session deletion is handled gracefully
|
|
942
|
+
- [ ] Loop detection works (Jaccard bigrams, threshold 0.85)
|
|
943
|
+
- [ ] Invalid agent names return an error with the available list
|
|
944
|
+
- [ ] `━━━ Roundtable Concluded ━━━` delimiter appears in S2 at the end
|
|
945
|
+
- [ ] Post-conclusion messages in S2 do not affect the S1 result
|
|
946
|
+
- [ ] Startup recovery detects and notifies about orphan roundtables
|
|
947
|
+
|
|
948
|
+
---
|
|
949
|
+
|
|
950
|
+
## 13. Glossary
|
|
951
|
+
|
|
952
|
+
| Term | Definition |
|
|
953
|
+
|------|------------|
|
|
954
|
+
| **S1** | Main session, where the user interacts |
|
|
955
|
+
| **S2** | Child session, where the roundtable runs |
|
|
956
|
+
| **Round** | One round = all agents speak once |
|
|
957
|
+
| **Turn** | A specific agent's turn to speak |
|
|
958
|
+
| **Observer** | Agent that does not debate, only summarizes |
|
|
959
|
+
| **Orchestrator** | Agent that called `roundtable()` |
|
|
960
|
+
| **History** | Accumulated discussion text |
|
|
961
|
+
| **Phase** | Current roundtable state |
|
|
962
|
+
| **Extend** | Continue a concluded roundtable |
|
|
963
|
+
| **noReply** | Injected message that does not trigger an AI response |
|
|
964
|
+
| **ToolCallSummary** | Record of a tool used by an agent: name + output preview (500 chars) |
|
|
965
|
+
| **Jaccard (bigrams)** | Text similarity algorithm used in loop detection: `|A∩B|/|A∪B|` over character pairs |
|
|
966
|
+
| **Default observer** | Built-in plugin mechanism that consolidates the debate into an executive summary without relying on an external agent |
|
|
967
|
+
|
|
968
|
+
---
|
|
969
|
+
|
|
970
|
+
## 14. References
|
|
971
|
+
|
|
972
|
+
- [OpenCode Plugin SDK](https://opencode.ai/docs/sdk/)
|
|
973
|
+
- [OpenCode Plugin API](https://opencode.ai/docs/plugins/)
|
|
974
|
+
- [OpenCode Agent Config](https://opencode.ai/docs/agents/)
|
|
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`
|