@ci-kit/roundtable 0.0.1 → 0.3.1
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/dist/chunks/example.mjs +194 -0
- package/dist/cli.mjs +71 -13
- package/dist/index.d.mts +270 -18
- package/dist/index.d.ts +270 -18
- package/dist/index.mjs +1830 -10
- package/package.json +3 -3
package/dist/index.mjs
CHANGED
|
@@ -1,15 +1,1835 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { stat, readFile, readdir, mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { resolve, join, basename, dirname } from 'node:path';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import process from 'node:process';
|
|
5
|
+
import { text, isAiError } from '@ci-kit/ai';
|
|
6
|
+
|
|
7
|
+
class YamlError extends Error {
|
|
8
|
+
constructor(message, line) {
|
|
9
|
+
super(line === void 0 ? message : `line ${line}: ${message}`);
|
|
10
|
+
this.line = line;
|
|
11
|
+
this.name = "YamlError";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function parseScalar(raw, line) {
|
|
15
|
+
const value = raw.trim();
|
|
16
|
+
if (!value) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
if (value === "true") {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
if (value === "false") {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
if (value === "null" || value === "~") {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
if (value.startsWith('"') && value.endsWith('"') && value.length >= 2 || value.startsWith("'") && value.endsWith("'") && value.length >= 2) {
|
|
29
|
+
const inner = value.slice(1, -1);
|
|
30
|
+
return value.startsWith('"') ? unescapeDouble(inner, line) : inner.replace(/''/g, "'");
|
|
31
|
+
}
|
|
32
|
+
if (value.startsWith("[")) {
|
|
33
|
+
if (!value.endsWith("]")) {
|
|
34
|
+
throw new YamlError(`Unterminated inline list: ${value}`, line);
|
|
35
|
+
}
|
|
36
|
+
return splitInlineList(value.slice(1, -1), line);
|
|
37
|
+
}
|
|
38
|
+
if (value.startsWith("{")) {
|
|
39
|
+
throw new YamlError("Inline mappings are not supported in frontmatter; use a block mapping", line);
|
|
40
|
+
}
|
|
41
|
+
if (/^-?\d+$/.test(value)) {
|
|
42
|
+
return Number(value);
|
|
43
|
+
}
|
|
44
|
+
if (/^-?\d+\.\d+$/.test(value)) {
|
|
45
|
+
return Number(value);
|
|
46
|
+
}
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
function unescapeDouble(value, line) {
|
|
50
|
+
let result = "";
|
|
51
|
+
for (let i = 0; i < value.length; i++) {
|
|
52
|
+
const char = value[i];
|
|
53
|
+
if (char === "\\" && i + 1 < value.length) {
|
|
54
|
+
const next = value[++i];
|
|
55
|
+
result += next === "n" ? "\n" : next === "t" ? " " : next === '"' ? '"' : next === "\\" ? "\\" : next;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (char === '"') {
|
|
59
|
+
throw new YamlError("Unescaped double quote inside a double-quoted string", line);
|
|
60
|
+
}
|
|
61
|
+
result += char;
|
|
62
|
+
}
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
function splitInlineList(body, line) {
|
|
66
|
+
const items = [];
|
|
67
|
+
let current = "";
|
|
68
|
+
let quote = null;
|
|
69
|
+
for (const char of body) {
|
|
70
|
+
if (quote) {
|
|
71
|
+
current += char;
|
|
72
|
+
if (char === quote) {
|
|
73
|
+
quote = null;
|
|
74
|
+
}
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (char === '"' || char === "'") {
|
|
78
|
+
quote = char;
|
|
79
|
+
current += char;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (char === ",") {
|
|
83
|
+
items.push(parseScalar(current, line));
|
|
84
|
+
current = "";
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
current += char;
|
|
88
|
+
}
|
|
89
|
+
if (current.trim() || body.trim() === "") {
|
|
90
|
+
items.push(parseScalar(current, line));
|
|
91
|
+
}
|
|
92
|
+
return items;
|
|
93
|
+
}
|
|
94
|
+
function splitKey(text) {
|
|
95
|
+
const match = /^([A-Z_][\w-]*)\s*:(.*)$/i.exec(text);
|
|
96
|
+
if (!match) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
key: match[1],
|
|
101
|
+
rest: (match[2] ?? "").trim()
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function toLines(source) {
|
|
105
|
+
return source.replace(/\r\n?/g, "\n").split("\n").map((raw, index) => ({
|
|
106
|
+
indent: raw.length - raw.trimStart().length,
|
|
107
|
+
text: raw.trim(),
|
|
108
|
+
number: index + 1
|
|
109
|
+
})).filter((line) => line.text !== "" && !line.text.startsWith("#"));
|
|
110
|
+
}
|
|
111
|
+
function parseYaml(source) {
|
|
112
|
+
if (/\t/.test(source.split("\n").map((line) => line.match(/^[ \t]+/)?.[0] ?? "").join(""))) {
|
|
113
|
+
throw new YamlError("Tabs are not allowed for indentation; use spaces");
|
|
114
|
+
}
|
|
115
|
+
const lines = toLines(source);
|
|
116
|
+
if (!lines.length) {
|
|
117
|
+
return {};
|
|
118
|
+
}
|
|
119
|
+
const [value, next] = parseBlock(lines, 0, lines[0].indent);
|
|
120
|
+
if (next !== lines.length) {
|
|
121
|
+
throw new YamlError(`Unexpected content`, lines[next].number);
|
|
122
|
+
}
|
|
123
|
+
if (Array.isArray(value)) {
|
|
124
|
+
throw new YamlError("The frontmatter root must be a mapping, not a list", lines[0].number);
|
|
125
|
+
}
|
|
126
|
+
if (value === null || typeof value !== "object") {
|
|
127
|
+
throw new YamlError("The frontmatter root must be a mapping", lines[0].number);
|
|
128
|
+
}
|
|
129
|
+
return value;
|
|
130
|
+
}
|
|
131
|
+
function parseBlock(lines, start, indent) {
|
|
132
|
+
const first = lines[start];
|
|
133
|
+
if (first.indent !== indent) {
|
|
134
|
+
throw new YamlError(`Inconsistent indentation (expected ${indent}, found ${first.indent})`, first.number);
|
|
135
|
+
}
|
|
136
|
+
return first.text === "-" || first.text.startsWith("- ") ? parseList(lines, start, indent) : parseMapping(lines, start, indent);
|
|
137
|
+
}
|
|
138
|
+
function parseMapping(lines, start, indent) {
|
|
139
|
+
const result = {};
|
|
140
|
+
let index = start;
|
|
141
|
+
while (index < lines.length) {
|
|
142
|
+
const line = lines[index];
|
|
143
|
+
if (line.indent < indent) {
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
if (line.indent > indent) {
|
|
147
|
+
throw new YamlError(`Unexpected extra indentation`, line.number);
|
|
148
|
+
}
|
|
149
|
+
if (line.text === "-" || line.text.startsWith("- ")) {
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
const entry = splitKey(line.text);
|
|
153
|
+
if (!entry) {
|
|
154
|
+
throw new YamlError(`Expected "key: value", found "${line.text}"`, line.number);
|
|
155
|
+
}
|
|
156
|
+
if (Object.hasOwn(result, entry.key)) {
|
|
157
|
+
throw new YamlError(`Duplicate key "${entry.key}"`, line.number);
|
|
158
|
+
}
|
|
159
|
+
if (entry.rest !== "") {
|
|
160
|
+
result[entry.key] = parseScalar(entry.rest, line.number);
|
|
161
|
+
index++;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const next = lines[index + 1];
|
|
165
|
+
if (!next || next.indent <= indent) {
|
|
166
|
+
result[entry.key] = null;
|
|
167
|
+
index++;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
const [child, after] = parseBlock(lines, index + 1, next.indent);
|
|
171
|
+
result[entry.key] = child;
|
|
172
|
+
index = after;
|
|
173
|
+
}
|
|
174
|
+
return [result, index];
|
|
175
|
+
}
|
|
176
|
+
function parseList(lines, start, indent) {
|
|
177
|
+
const result = [];
|
|
178
|
+
let index = start;
|
|
179
|
+
while (index < lines.length) {
|
|
180
|
+
const line = lines[index];
|
|
181
|
+
if (line.indent < indent) {
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
if (line.indent > indent) {
|
|
185
|
+
throw new YamlError("Unexpected extra indentation in list", line.number);
|
|
186
|
+
}
|
|
187
|
+
if (line.text !== "-" && !line.text.startsWith("- ")) {
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
const body = line.text === "-" ? "" : line.text.slice(2).trim();
|
|
191
|
+
if (body === "") {
|
|
192
|
+
const next = lines[index + 1];
|
|
193
|
+
if (!next || next.indent <= indent) {
|
|
194
|
+
result.push(null);
|
|
195
|
+
index++;
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
const [child, after] = parseBlock(lines, index + 1, next.indent);
|
|
199
|
+
result.push(child);
|
|
200
|
+
index = after;
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (splitKey(body)) {
|
|
204
|
+
const itemIndent = line.indent + 2;
|
|
205
|
+
const view = [
|
|
206
|
+
{ indent: itemIndent, text: body, number: line.number },
|
|
207
|
+
...lines.slice(index + 1)
|
|
208
|
+
];
|
|
209
|
+
const [mapping, consumed] = parseMapping(view, 0, itemIndent);
|
|
210
|
+
result.push(mapping);
|
|
211
|
+
index += consumed;
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
result.push(parseScalar(body, line.number));
|
|
215
|
+
index++;
|
|
216
|
+
}
|
|
217
|
+
return [result, index];
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
class FrontmatterError extends Error {
|
|
221
|
+
constructor(message) {
|
|
222
|
+
super(message);
|
|
223
|
+
this.name = "FrontmatterError";
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
const DELIMITER = /^---[ \t]*$/;
|
|
227
|
+
function readFrontmatter(source, label) {
|
|
228
|
+
const normalized = source.replace(/\r\n?/g, "\n");
|
|
229
|
+
const lines = normalized.split("\n");
|
|
230
|
+
if (!DELIMITER.test(lines[0] ?? "")) {
|
|
231
|
+
const firstLine = lines[0] ?? "";
|
|
232
|
+
if (!/^[A-Z_][\w-]*\s*:(?:\s|$)/i.test(firstLine)) {
|
|
233
|
+
return {
|
|
234
|
+
data: {},
|
|
235
|
+
body: normalized.trim(),
|
|
236
|
+
hasFrontmatter: false
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
let end = lines.length;
|
|
240
|
+
for (let index = 0; index < lines.length; index++) {
|
|
241
|
+
if (lines[index].trim() === "") {
|
|
242
|
+
end = index;
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
const yamlText2 = lines.slice(0, end).join("\n");
|
|
247
|
+
let data2;
|
|
248
|
+
try {
|
|
249
|
+
data2 = parseYaml(yamlText2);
|
|
250
|
+
} catch (error) {
|
|
251
|
+
const detail = error instanceof YamlError ? error.message : String(error);
|
|
252
|
+
throw new FrontmatterError(`${label}: ${detail}`);
|
|
253
|
+
}
|
|
254
|
+
return {
|
|
255
|
+
data: data2,
|
|
256
|
+
body: lines.slice(end + 1).join("\n").trim(),
|
|
257
|
+
hasFrontmatter: false
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
let close = -1;
|
|
261
|
+
for (let index = 1; index < lines.length; index++) {
|
|
262
|
+
if (DELIMITER.test(lines[index])) {
|
|
263
|
+
close = index;
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (close === -1) {
|
|
268
|
+
throw new FrontmatterError(`${label}: frontmatter opened with --- but never closed`);
|
|
269
|
+
}
|
|
270
|
+
const yamlText = lines.slice(1, close).join("\n");
|
|
271
|
+
let data;
|
|
272
|
+
try {
|
|
273
|
+
data = parseYaml(yamlText);
|
|
274
|
+
} catch (error) {
|
|
275
|
+
const detail = error instanceof YamlError ? error.message : String(error);
|
|
276
|
+
throw new FrontmatterError(`${label}: ${detail}`);
|
|
277
|
+
}
|
|
278
|
+
return {
|
|
279
|
+
data,
|
|
280
|
+
body: lines.slice(close + 1).join("\n").trim(),
|
|
281
|
+
hasFrontmatter: true
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
class SeatError extends Error {
|
|
286
|
+
constructor(label, message) {
|
|
287
|
+
super(`${label}: ${message}`);
|
|
288
|
+
this.label = label;
|
|
289
|
+
this.name = "SeatError";
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
const TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
|
|
293
|
+
"id",
|
|
294
|
+
"role",
|
|
295
|
+
"enabled",
|
|
296
|
+
"identity",
|
|
297
|
+
"persona",
|
|
298
|
+
"seat"
|
|
299
|
+
]);
|
|
300
|
+
const IDENTITY_KEYS = /* @__PURE__ */ new Set([
|
|
301
|
+
"name",
|
|
302
|
+
"avatar",
|
|
303
|
+
"title",
|
|
304
|
+
"background"
|
|
305
|
+
]);
|
|
306
|
+
const PERSONA_KEYS = /* @__PURE__ */ new Set([
|
|
307
|
+
"stance",
|
|
308
|
+
"personality",
|
|
309
|
+
"style",
|
|
310
|
+
"speech",
|
|
311
|
+
"values",
|
|
312
|
+
"quirks",
|
|
313
|
+
"avoid"
|
|
314
|
+
]);
|
|
315
|
+
const SEAT_KEYS = /* @__PURE__ */ new Set([
|
|
316
|
+
"phase",
|
|
317
|
+
"sees",
|
|
318
|
+
"canPropose",
|
|
319
|
+
"canVeto",
|
|
320
|
+
"lead",
|
|
321
|
+
"weight",
|
|
322
|
+
"priority",
|
|
323
|
+
"budget",
|
|
324
|
+
"fallback",
|
|
325
|
+
"output",
|
|
326
|
+
"model",
|
|
327
|
+
"temperature",
|
|
328
|
+
"maxTokens"
|
|
329
|
+
]);
|
|
330
|
+
const FLAT_ALIASES = new Map([
|
|
331
|
+
...[
|
|
332
|
+
"name",
|
|
333
|
+
"avatar",
|
|
334
|
+
"title",
|
|
335
|
+
"background"
|
|
336
|
+
].map((key) => [key, "identity"]),
|
|
337
|
+
...[
|
|
338
|
+
"stance",
|
|
339
|
+
"personality",
|
|
340
|
+
"style",
|
|
341
|
+
"speech",
|
|
342
|
+
"values",
|
|
343
|
+
"quirks",
|
|
344
|
+
"avoid"
|
|
345
|
+
].map((key) => [key, "persona"]),
|
|
346
|
+
...[
|
|
347
|
+
"phase",
|
|
348
|
+
"sees",
|
|
349
|
+
"canPropose",
|
|
350
|
+
"canVeto",
|
|
351
|
+
"lead",
|
|
352
|
+
"weight",
|
|
353
|
+
"priority",
|
|
354
|
+
"budget",
|
|
355
|
+
"fallback",
|
|
356
|
+
"output",
|
|
357
|
+
"model",
|
|
358
|
+
"temperature",
|
|
359
|
+
"maxTokens"
|
|
360
|
+
].map((key) => [key, "seat"])
|
|
361
|
+
]);
|
|
362
|
+
function asString$1(value, label, field) {
|
|
363
|
+
if (value === void 0 || value === null) {
|
|
364
|
+
return void 0;
|
|
365
|
+
}
|
|
366
|
+
if (typeof value !== "string") {
|
|
367
|
+
throw new SeatError(label, `${field} must be a string`);
|
|
368
|
+
}
|
|
369
|
+
const trimmed = value.trim();
|
|
370
|
+
return trimmed === "" ? void 0 : trimmed;
|
|
371
|
+
}
|
|
372
|
+
function requireString(value, label, field) {
|
|
373
|
+
const result = asString$1(value, label, field);
|
|
374
|
+
if (!result) {
|
|
375
|
+
throw new SeatError(label, `${field} is required`);
|
|
376
|
+
}
|
|
377
|
+
return result;
|
|
378
|
+
}
|
|
379
|
+
function asBoolean(value, label, field, fallback) {
|
|
380
|
+
if (value === void 0 || value === null) {
|
|
381
|
+
return fallback;
|
|
382
|
+
}
|
|
383
|
+
if (typeof value !== "boolean") {
|
|
384
|
+
throw new SeatError(label, `${field} must be true or false`);
|
|
385
|
+
}
|
|
386
|
+
return value;
|
|
387
|
+
}
|
|
388
|
+
function asNumber$1(value, label, field) {
|
|
389
|
+
if (value === void 0 || value === null) {
|
|
390
|
+
return void 0;
|
|
391
|
+
}
|
|
392
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
393
|
+
throw new SeatError(label, `${field} must be a number`);
|
|
394
|
+
}
|
|
395
|
+
return value;
|
|
396
|
+
}
|
|
397
|
+
function asStringList$1(value, label, field) {
|
|
398
|
+
if (value === void 0 || value === null) {
|
|
399
|
+
return [];
|
|
400
|
+
}
|
|
401
|
+
const items = Array.isArray(value) ? value : [value];
|
|
402
|
+
return items.map((item) => {
|
|
403
|
+
if (typeof item !== "string" || !item.trim()) {
|
|
404
|
+
throw new SeatError(label, `${field} must contain non-empty strings`);
|
|
405
|
+
}
|
|
406
|
+
return item.trim();
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
function asMapping$1(value, label, field) {
|
|
410
|
+
if (value === void 0 || value === null) {
|
|
411
|
+
return {};
|
|
412
|
+
}
|
|
413
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
414
|
+
throw new SeatError(label, `${field} must be a mapping`);
|
|
415
|
+
}
|
|
416
|
+
return value;
|
|
417
|
+
}
|
|
418
|
+
function rejectUnknownKeys(mapping, allowed, label, where) {
|
|
419
|
+
const unknown = Object.keys(mapping).filter((key) => !allowed.has(key));
|
|
420
|
+
if (unknown.length) {
|
|
421
|
+
throw new SeatError(label, `unknown field${unknown.length > 1 ? "s" : ""} in ${where}: ${unknown.join(", ")}`);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
function groupData(data, label) {
|
|
425
|
+
const groups = {
|
|
426
|
+
identity: {
|
|
427
|
+
...asMapping$1(data.identity, label, "identity")
|
|
428
|
+
},
|
|
429
|
+
persona: {
|
|
430
|
+
...asMapping$1(data.persona, label, "persona")
|
|
431
|
+
},
|
|
432
|
+
seat: {
|
|
433
|
+
...asMapping$1(data.seat, label, "seat")
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
for (const [key, value] of Object.entries(data)) {
|
|
437
|
+
if (TOP_LEVEL_KEYS.has(key)) {
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
const target = FLAT_ALIASES.get(key);
|
|
441
|
+
if (!target) {
|
|
442
|
+
throw new SeatError(label, `unknown field "${key}"; expected one of ${[...TOP_LEVEL_KEYS].join(", ")}`);
|
|
443
|
+
}
|
|
444
|
+
if (Object.hasOwn(groups[target], key)) {
|
|
445
|
+
throw new SeatError(label, `"${key}" is declared both at the top level and inside ${target}`);
|
|
446
|
+
}
|
|
447
|
+
groups[target][key] = value;
|
|
448
|
+
}
|
|
449
|
+
return groups;
|
|
450
|
+
}
|
|
451
|
+
function parseSees(value, label) {
|
|
452
|
+
if (value === void 0 || value === null) {
|
|
453
|
+
return "all";
|
|
454
|
+
}
|
|
455
|
+
if (Array.isArray(value)) {
|
|
456
|
+
const ids = asStringList$1(value, label, "seat.sees");
|
|
457
|
+
if (!ids.length) {
|
|
458
|
+
throw new SeatError(label, 'seat.sees list must not be empty; use "all" or "none"');
|
|
459
|
+
}
|
|
460
|
+
return ids;
|
|
461
|
+
}
|
|
462
|
+
if (value === "all" || value === "none" || value === "lead-only") {
|
|
463
|
+
return value;
|
|
464
|
+
}
|
|
465
|
+
throw new SeatError(label, `seat.sees must be all, none, lead-only or a list of seat ids; got "${String(value)}"`);
|
|
466
|
+
}
|
|
467
|
+
function renderSeatSystemPrompt(seat) {
|
|
468
|
+
const lines = [];
|
|
469
|
+
const { identity, persona } = seat;
|
|
470
|
+
const headline = [identity.title, `\u662F\u4E00\u540D${seat.role}`].filter(Boolean).join("\uFF0C");
|
|
471
|
+
lines.push(`\u4F60\u662F${identity.name}\uFF0C${headline}\u3002`);
|
|
472
|
+
if (identity.background) {
|
|
473
|
+
lines.push("", "## \u80CC\u666F", identity.background);
|
|
474
|
+
}
|
|
475
|
+
if (persona.personality.length) {
|
|
476
|
+
lines.push("", "## \u4F60\u7684\u6027\u683C", persona.personality.join("\u3001"));
|
|
477
|
+
}
|
|
478
|
+
if (persona.stance) {
|
|
479
|
+
lines.push("", "## \u4F60\u7684\u7ACB\u573A", persona.stance);
|
|
480
|
+
}
|
|
481
|
+
const speech = [];
|
|
482
|
+
if (persona.style) {
|
|
483
|
+
speech.push(persona.style);
|
|
484
|
+
}
|
|
485
|
+
if (persona.speech) {
|
|
486
|
+
speech.push(persona.speech);
|
|
487
|
+
}
|
|
488
|
+
if (persona.quirks) {
|
|
489
|
+
speech.push(persona.quirks);
|
|
490
|
+
}
|
|
491
|
+
if (speech.length) {
|
|
492
|
+
lines.push("", "## \u4F60\u7684\u8BF4\u8BDD\u65B9\u5F0F", ...speech.map((line) => `- ${line}`));
|
|
493
|
+
}
|
|
494
|
+
if (persona.values) {
|
|
495
|
+
lines.push("", "## \u4F60\u7684\u539F\u5219", persona.values);
|
|
496
|
+
}
|
|
497
|
+
if (persona.avoid) {
|
|
498
|
+
lines.push("", "## \u4F60\u7684\u8FB9\u754C", persona.avoid);
|
|
499
|
+
}
|
|
500
|
+
if (seat.body) {
|
|
501
|
+
lines.push("", seat.body);
|
|
502
|
+
}
|
|
503
|
+
return lines.join("\n");
|
|
504
|
+
}
|
|
505
|
+
function parseSeat(document, source) {
|
|
506
|
+
const label = `seat ${source}`;
|
|
507
|
+
const { identity, persona, seat } = groupData(document.data, label);
|
|
508
|
+
rejectUnknownKeys(identity, IDENTITY_KEYS, label, "identity");
|
|
509
|
+
rejectUnknownKeys(persona, PERSONA_KEYS, label, "persona");
|
|
510
|
+
rejectUnknownKeys(seat, SEAT_KEYS, label, "seat");
|
|
511
|
+
const id = requireString(document.data.id, label, "id");
|
|
512
|
+
if (!/^[\w-]+$/.test(id)) {
|
|
513
|
+
throw new SeatError(label, `id "${id}" must use letters, digits, underscore or hyphen`);
|
|
514
|
+
}
|
|
515
|
+
const role = requireString(document.data.role, label, "role");
|
|
516
|
+
const weight = asNumber$1(seat.weight, label, "seat.weight") ?? 1;
|
|
517
|
+
if (weight <= 0) {
|
|
518
|
+
throw new SeatError(label, "seat.weight must be greater than 0");
|
|
519
|
+
}
|
|
520
|
+
const budget = asNumber$1(seat.budget, label, "seat.budget");
|
|
521
|
+
if (budget !== void 0 && budget < 0) {
|
|
522
|
+
throw new SeatError(label, "seat.budget must not be negative");
|
|
523
|
+
}
|
|
524
|
+
const fallback = asString$1(seat.fallback, label, "seat.fallback") ?? "skip";
|
|
525
|
+
if (!["skip", "retry", "abort"].includes(fallback)) {
|
|
526
|
+
throw new SeatError(label, "seat.fallback must be skip, retry or abort");
|
|
527
|
+
}
|
|
528
|
+
const output = asString$1(seat.output, label, "seat.output") ?? "text";
|
|
529
|
+
if (!["text", "json", "markdown"].includes(output)) {
|
|
530
|
+
throw new SeatError(label, "seat.output must be text, json or markdown");
|
|
531
|
+
}
|
|
532
|
+
const name = asString$1(identity.name, label, "identity.name") ?? role;
|
|
533
|
+
return {
|
|
534
|
+
id,
|
|
535
|
+
role,
|
|
536
|
+
enabled: asBoolean(document.data.enabled, label, "enabled", true),
|
|
537
|
+
identity: {
|
|
538
|
+
name,
|
|
539
|
+
avatar: asString$1(identity.avatar, label, "identity.avatar"),
|
|
540
|
+
title: asString$1(identity.title, label, "identity.title"),
|
|
541
|
+
background: asString$1(identity.background, label, "identity.background")
|
|
542
|
+
},
|
|
543
|
+
persona: {
|
|
544
|
+
stance: asString$1(persona.stance, label, "persona.stance"),
|
|
545
|
+
personality: asStringList$1(persona.personality, label, "persona.personality"),
|
|
546
|
+
style: asString$1(persona.style, label, "persona.style"),
|
|
547
|
+
speech: asString$1(persona.speech, label, "persona.speech"),
|
|
548
|
+
values: asString$1(persona.values, label, "persona.values"),
|
|
549
|
+
quirks: asString$1(persona.quirks, label, "persona.quirks"),
|
|
550
|
+
avoid: asString$1(persona.avoid, label, "persona.avoid")
|
|
551
|
+
},
|
|
552
|
+
seat: {
|
|
553
|
+
phase: asStringList$1(seat.phase, label, "seat.phase"),
|
|
554
|
+
sees: parseSees(seat.sees, label),
|
|
555
|
+
canPropose: asBoolean(seat.canPropose, label, "seat.canPropose", false),
|
|
556
|
+
canVeto: asBoolean(seat.canVeto, label, "seat.canVeto", false),
|
|
557
|
+
lead: asBoolean(seat.lead, label, "seat.lead", false),
|
|
558
|
+
weight,
|
|
559
|
+
priority: asNumber$1(seat.priority, label, "seat.priority") ?? 99,
|
|
560
|
+
budget,
|
|
561
|
+
fallback,
|
|
562
|
+
output,
|
|
563
|
+
model: asString$1(seat.model, label, "seat.model"),
|
|
564
|
+
temperature: asNumber$1(seat.temperature, label, "seat.temperature"),
|
|
565
|
+
maxTokens: asNumber$1(seat.maxTokens, label, "seat.maxTokens")
|
|
566
|
+
},
|
|
567
|
+
body: document.body,
|
|
568
|
+
source
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
class TableError extends Error {
|
|
573
|
+
constructor(message) {
|
|
574
|
+
super(message);
|
|
575
|
+
this.name = "TableError";
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
const TABLE_KEYS = /* @__PURE__ */ new Set([
|
|
579
|
+
"title",
|
|
580
|
+
"schedule",
|
|
581
|
+
"budget",
|
|
582
|
+
"onVeto",
|
|
583
|
+
"maxAttempts",
|
|
584
|
+
"threshold",
|
|
585
|
+
"timeoutMs",
|
|
586
|
+
"phases",
|
|
587
|
+
"special"
|
|
588
|
+
]);
|
|
589
|
+
const PHASE_KEYS = /* @__PURE__ */ new Set([
|
|
590
|
+
"id",
|
|
591
|
+
"seats",
|
|
592
|
+
"vetoable",
|
|
593
|
+
"requires",
|
|
594
|
+
"mode"
|
|
595
|
+
]);
|
|
596
|
+
function asString(value, field) {
|
|
597
|
+
if (value === void 0 || value === null) {
|
|
598
|
+
return void 0;
|
|
599
|
+
}
|
|
600
|
+
if (typeof value !== "string") {
|
|
601
|
+
throw new TableError(`table: ${field} must be a string`);
|
|
602
|
+
}
|
|
603
|
+
const trimmed = value.trim();
|
|
604
|
+
return trimmed === "" ? void 0 : trimmed;
|
|
605
|
+
}
|
|
606
|
+
function asNumber(value, field) {
|
|
607
|
+
if (value === void 0 || value === null) {
|
|
608
|
+
return void 0;
|
|
609
|
+
}
|
|
610
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
611
|
+
throw new TableError(`table: ${field} must be a number`);
|
|
612
|
+
}
|
|
613
|
+
return value;
|
|
614
|
+
}
|
|
615
|
+
function asStringList(value, field) {
|
|
616
|
+
if (value === void 0 || value === null) {
|
|
617
|
+
return [];
|
|
618
|
+
}
|
|
619
|
+
const items = Array.isArray(value) ? value : [value];
|
|
620
|
+
return items.map((item) => {
|
|
621
|
+
if (typeof item !== "string" || !item.trim()) {
|
|
622
|
+
throw new TableError(`table: ${field} must contain non-empty strings`);
|
|
623
|
+
}
|
|
624
|
+
return item.trim();
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
function asMapping(value, field) {
|
|
628
|
+
if (value === void 0 || value === null) {
|
|
629
|
+
return {};
|
|
630
|
+
}
|
|
631
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
632
|
+
throw new TableError(`table: ${field} must be a mapping`);
|
|
633
|
+
}
|
|
634
|
+
return value;
|
|
635
|
+
}
|
|
636
|
+
function parsePhases(value) {
|
|
637
|
+
if (value === void 0 || value === null) {
|
|
638
|
+
throw new TableError("table: phases is required");
|
|
639
|
+
}
|
|
640
|
+
if (!Array.isArray(value)) {
|
|
641
|
+
throw new TableError("table: phases must be a list");
|
|
642
|
+
}
|
|
643
|
+
return value.map((item, index) => {
|
|
644
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
645
|
+
throw new TableError(`table: phases[${index}] must be a mapping`);
|
|
646
|
+
}
|
|
647
|
+
const raw = item;
|
|
648
|
+
const unknown = Object.keys(raw).filter((key) => !PHASE_KEYS.has(key));
|
|
649
|
+
if (unknown.length) {
|
|
650
|
+
throw new TableError(`table: phases[${index}] has unknown field(s): ${unknown.join(", ")}`);
|
|
651
|
+
}
|
|
652
|
+
const id = asString(raw.id, `phases[${index}].id`);
|
|
653
|
+
if (!id) {
|
|
654
|
+
throw new TableError(`table: phases[${index}].id is required`);
|
|
655
|
+
}
|
|
656
|
+
const mode = asString(raw.mode, `phases[${index}].mode`) ?? "serial";
|
|
657
|
+
if (mode !== "serial" && mode !== "parallel") {
|
|
658
|
+
throw new TableError(`table: phases[${index}].mode must be serial or parallel`);
|
|
659
|
+
}
|
|
660
|
+
if (raw.vetoable !== void 0 && raw.vetoable !== null && typeof raw.vetoable !== "boolean") {
|
|
661
|
+
throw new TableError(`table: phases[${index}].vetoable must be true or false`);
|
|
662
|
+
}
|
|
663
|
+
return {
|
|
664
|
+
id,
|
|
665
|
+
seats: asStringList(raw.seats, `phases[${index}].seats`),
|
|
666
|
+
vetoable: raw.vetoable === true,
|
|
667
|
+
requires: asStringList(raw.requires, `phases[${index}].requires`),
|
|
668
|
+
mode
|
|
669
|
+
};
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
function resolveSpecial(phases, special) {
|
|
673
|
+
const ids = phases.map((phase) => phase.id);
|
|
674
|
+
const declaredPropose = asString(special.propose, "special.propose");
|
|
675
|
+
const declaredConverge = asString(special.converge, "special.converge");
|
|
676
|
+
const propose = declaredPropose ?? (ids.includes("propose") ? "propose" : phases[0].id);
|
|
677
|
+
const converge = declaredConverge ?? (ids.includes("converge") ? "converge" : phases[phases.length - 1].id);
|
|
678
|
+
for (const [field, id] of [
|
|
679
|
+
["propose", propose],
|
|
680
|
+
["converge", converge]
|
|
681
|
+
]) {
|
|
682
|
+
if (!ids.includes(id)) {
|
|
683
|
+
throw new TableError(`table: special.${field} points at unknown phase "${id}"`);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
return { propose, converge };
|
|
687
|
+
}
|
|
688
|
+
function parseTable(document, fallbackTitle) {
|
|
689
|
+
const data = document.data;
|
|
690
|
+
const unknown = Object.keys(data).filter((key) => !TABLE_KEYS.has(key));
|
|
691
|
+
if (unknown.length) {
|
|
692
|
+
throw new TableError(`table: unknown field(s): ${unknown.join(", ")}`);
|
|
693
|
+
}
|
|
694
|
+
const budgetRaw = asMapping(data.budget, "budget");
|
|
695
|
+
const budgetUnknown = Object.keys(budgetRaw).filter((key) => key !== "total" && key !== "perSeat");
|
|
696
|
+
if (budgetUnknown.length) {
|
|
697
|
+
throw new TableError(`table: budget has unknown field(s): ${budgetUnknown.join(", ")}`);
|
|
698
|
+
}
|
|
699
|
+
const total = asNumber(budgetRaw.total, "budget.total");
|
|
700
|
+
const perSeat = asNumber(budgetRaw.perSeat, "budget.perSeat");
|
|
701
|
+
if (total !== void 0 && total < 0) {
|
|
702
|
+
throw new TableError("table: budget.total must not be negative");
|
|
703
|
+
}
|
|
704
|
+
if (perSeat !== void 0 && perSeat < 0) {
|
|
705
|
+
throw new TableError("table: budget.perSeat must not be negative");
|
|
706
|
+
}
|
|
707
|
+
const onVeto = asString(data.onVeto, "onVeto") ?? "re-propose";
|
|
708
|
+
if (!["re-propose", "revise", "abort"].includes(onVeto)) {
|
|
709
|
+
throw new TableError("table: onVeto must be re-propose, revise or abort");
|
|
710
|
+
}
|
|
711
|
+
const maxAttempts = asNumber(data.maxAttempts, "maxAttempts") ?? 3;
|
|
712
|
+
if (!Number.isInteger(maxAttempts) || maxAttempts < 1) {
|
|
713
|
+
throw new TableError("table: maxAttempts must be a positive integer");
|
|
714
|
+
}
|
|
715
|
+
const threshold = asNumber(data.threshold, "threshold") ?? 0.5;
|
|
716
|
+
if (threshold <= 0 || threshold > 1) {
|
|
717
|
+
throw new TableError("table: threshold must be greater than 0 and at most 1");
|
|
718
|
+
}
|
|
719
|
+
const timeoutMs = asNumber(data.timeoutMs, "timeoutMs");
|
|
720
|
+
if (timeoutMs !== void 0 && (!Number.isInteger(timeoutMs) || timeoutMs < 1e3 || timeoutMs > 36e5)) {
|
|
721
|
+
throw new TableError("table: timeoutMs must be an integer between 1000 and 3600000");
|
|
722
|
+
}
|
|
723
|
+
const phases = parsePhases(data.phases);
|
|
724
|
+
if (!phases.length) {
|
|
725
|
+
throw new TableError("table: at least one phase is required");
|
|
726
|
+
}
|
|
727
|
+
const seen = /* @__PURE__ */ new Set();
|
|
728
|
+
for (const phase of phases) {
|
|
729
|
+
if (seen.has(phase.id)) {
|
|
730
|
+
throw new TableError(`table: duplicate phase id "${phase.id}"`);
|
|
731
|
+
}
|
|
732
|
+
seen.add(phase.id);
|
|
733
|
+
}
|
|
734
|
+
return {
|
|
735
|
+
title: asString(data.title, "title") ?? fallbackTitle,
|
|
736
|
+
schedule: asString(data.schedule, "schedule"),
|
|
737
|
+
budget: { total, perSeat },
|
|
738
|
+
onVeto,
|
|
739
|
+
maxAttempts,
|
|
740
|
+
threshold,
|
|
741
|
+
timeoutMs,
|
|
742
|
+
phases,
|
|
743
|
+
special: resolveSpecial(phases, asMapping(data.special, "special"))
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
function resolvePhases(table, seats, allPhaseIds) {
|
|
747
|
+
const byId = new Map(seats.map((seat) => [seat.id, seat]));
|
|
748
|
+
for (const phase of table.phases) {
|
|
749
|
+
for (const seatId of phase.seats) {
|
|
750
|
+
if (!byId.has(seatId)) {
|
|
751
|
+
throw new TableError(`table: phase "${phase.id}" names unknown seat "${seatId}"`);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
for (const required of phase.requires) {
|
|
755
|
+
if (!allPhaseIds.includes(required)) {
|
|
756
|
+
throw new TableError(`table: phase "${phase.id}" requires unknown phase "${required}"`);
|
|
757
|
+
}
|
|
758
|
+
if (required === phase.id) {
|
|
759
|
+
throw new TableError(`table: phase "${phase.id}" cannot require itself`);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
const order = new Map(table.phases.map((phase, index) => [phase.id, index]));
|
|
764
|
+
for (const phase of table.phases) {
|
|
765
|
+
const deps = phase.requires.length ? phase.requires : order.get(phase.id) > 0 ? [table.phases[order.get(phase.id) - 1].id] : [];
|
|
766
|
+
for (const dep of deps) {
|
|
767
|
+
if (order.get(dep) > order.get(phase.id)) {
|
|
768
|
+
throw new TableError(`table: phase "${phase.id}" requires "${dep}" which comes later; phases run in order`);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
return table.phases.map((phase) => {
|
|
773
|
+
const participating = seats.filter((seat) => seat.enabled).filter((seat) => phase.seats.length ? phase.seats.includes(seat.id) : true).filter((seat) => {
|
|
774
|
+
const declared = seat.seat.phase;
|
|
775
|
+
if (!declared.length) {
|
|
776
|
+
return true;
|
|
777
|
+
}
|
|
778
|
+
if (!declared.includes(phase.id)) {
|
|
779
|
+
if (phase.seats.includes(seat.id)) {
|
|
780
|
+
throw new TableError(
|
|
781
|
+
`seat "${seat.id}" is listed in phase "${phase.id}" but its seat.phase does not include it`
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
return false;
|
|
785
|
+
}
|
|
786
|
+
return true;
|
|
787
|
+
}).sort((a, b) => a.seat.priority - b.seat.priority || a.id.localeCompare(b.id)).map((seat) => seat.id);
|
|
788
|
+
return { ...phase, seats: participating };
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
class RoundtableConfigError extends Error {
|
|
793
|
+
constructor(message) {
|
|
794
|
+
super(message);
|
|
795
|
+
this.name = "RoundtableConfigError";
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
const SEAT_DIR = "seats";
|
|
799
|
+
async function readIfPresent(path) {
|
|
800
|
+
try {
|
|
801
|
+
const info = await stat(path);
|
|
802
|
+
if (!info.isFile()) {
|
|
803
|
+
throw new RoundtableConfigError(`${path} exists but is not a file`);
|
|
804
|
+
}
|
|
805
|
+
return await readFile(path, "utf8");
|
|
806
|
+
} catch (error) {
|
|
807
|
+
if (error.code === "ENOENT") {
|
|
808
|
+
return null;
|
|
809
|
+
}
|
|
810
|
+
throw error;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
async function seatFiles(dir) {
|
|
814
|
+
let entries;
|
|
815
|
+
try {
|
|
816
|
+
entries = await readdir(dir);
|
|
817
|
+
} catch (error) {
|
|
818
|
+
if (error.code === "ENOENT") {
|
|
819
|
+
throw new RoundtableConfigError(`No ${SEAT_DIR}/ directory in ${dir}; a roundtable needs at least one seat`);
|
|
820
|
+
}
|
|
821
|
+
throw error;
|
|
822
|
+
}
|
|
823
|
+
return entries.filter((name) => name.endsWith(".md")).sort((a, b) => a.localeCompare(b)).map((name) => join(dir, name));
|
|
824
|
+
}
|
|
825
|
+
function digest(parts) {
|
|
826
|
+
const hash = createHash("sha256");
|
|
827
|
+
for (const part of parts) {
|
|
828
|
+
hash.update(`${part.length}:`);
|
|
829
|
+
hash.update(part);
|
|
830
|
+
}
|
|
831
|
+
return `sha256:${hash.digest("hex")}`;
|
|
832
|
+
}
|
|
833
|
+
async function loadRoundtable(options) {
|
|
834
|
+
const configDir = resolve(options.configDir);
|
|
835
|
+
const info = await stat(configDir).catch(() => null);
|
|
836
|
+
if (!info || !info.isDirectory()) {
|
|
837
|
+
throw new RoundtableConfigError(`Roundtable directory not found: ${configDir}`);
|
|
838
|
+
}
|
|
839
|
+
const tablePath = join(configDir, "table.md");
|
|
840
|
+
const tableSource = await readIfPresent(tablePath);
|
|
841
|
+
if (tableSource === null) {
|
|
842
|
+
throw new RoundtableConfigError(`Missing table.md in ${configDir}`);
|
|
843
|
+
}
|
|
844
|
+
let table;
|
|
845
|
+
try {
|
|
846
|
+
table = parseTable(readFrontmatter(tableSource, "table.md"), basename(configDir));
|
|
847
|
+
} catch (error) {
|
|
848
|
+
if (error instanceof TableError) {
|
|
849
|
+
throw new RoundtableConfigError(error.message);
|
|
850
|
+
}
|
|
851
|
+
throw error;
|
|
852
|
+
}
|
|
853
|
+
const files = await seatFiles(join(configDir, SEAT_DIR));
|
|
854
|
+
if (!files.length) {
|
|
855
|
+
throw new RoundtableConfigError(`No seat files in ${join(configDir, SEAT_DIR)}`);
|
|
856
|
+
}
|
|
857
|
+
const seats = [];
|
|
858
|
+
const rawSeatSources = [];
|
|
859
|
+
for (const file of files) {
|
|
860
|
+
const source = await readIfPresent(file);
|
|
861
|
+
rawSeatSources.push(source);
|
|
862
|
+
try {
|
|
863
|
+
seats.push(parseSeat(readFrontmatter(source, basename(file)), basename(file)));
|
|
864
|
+
} catch (error) {
|
|
865
|
+
if (error instanceof SeatError) {
|
|
866
|
+
throw new RoundtableConfigError(error.message);
|
|
867
|
+
}
|
|
868
|
+
throw error;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
const seen = /* @__PURE__ */ new Map();
|
|
872
|
+
for (const seat of seats) {
|
|
873
|
+
const existing = seen.get(seat.id);
|
|
874
|
+
if (existing) {
|
|
875
|
+
throw new RoundtableConfigError(`Duplicate seat id "${seat.id}" in ${existing} and ${seat.source}`);
|
|
876
|
+
}
|
|
877
|
+
seen.set(seat.id, seat.source);
|
|
878
|
+
}
|
|
879
|
+
const enabled = seats.filter((seat) => seat.enabled);
|
|
880
|
+
if (!enabled.length) {
|
|
881
|
+
throw new RoundtableConfigError("Every seat is disabled; a roundtable needs at least one enabled seat");
|
|
882
|
+
}
|
|
883
|
+
let phases;
|
|
884
|
+
try {
|
|
885
|
+
phases = resolvePhases(table, seats, table.phases.map((phase) => phase.id));
|
|
886
|
+
} catch (error) {
|
|
887
|
+
if (error instanceof TableError) {
|
|
888
|
+
throw new RoundtableConfigError(error.message);
|
|
889
|
+
}
|
|
890
|
+
throw error;
|
|
891
|
+
}
|
|
892
|
+
const emptyPhase = phases.find((phase) => !phase.seats.length);
|
|
893
|
+
if (emptyPhase) {
|
|
894
|
+
throw new RoundtableConfigError(`Phase "${emptyPhase.id}" has no participating seats`);
|
|
895
|
+
}
|
|
896
|
+
return {
|
|
897
|
+
table,
|
|
898
|
+
seats,
|
|
899
|
+
byId: new Map(enabled.map((seat) => [seat.id, seat])),
|
|
900
|
+
phases,
|
|
901
|
+
configDir,
|
|
902
|
+
fingerprint: digest([
|
|
903
|
+
String(options.schema ?? 1),
|
|
904
|
+
tableSource,
|
|
905
|
+
...rawSeatSources
|
|
906
|
+
])
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
async function checkRoundtable(options) {
|
|
910
|
+
return loadRoundtable({ configDir: options.configDir, schema: 1 });
|
|
911
|
+
}
|
|
912
|
+
function defaultOutDir(cwd = process.cwd()) {
|
|
913
|
+
return join(cwd, "roundtable-out");
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
function visibleStatements(viewer, statements, leadIds) {
|
|
917
|
+
const { sees } = viewer.seat;
|
|
918
|
+
if (sees === "none") {
|
|
919
|
+
return [];
|
|
920
|
+
}
|
|
921
|
+
if (sees === "all") {
|
|
922
|
+
return statements.filter((statement) => statement.seat !== viewer.id);
|
|
923
|
+
}
|
|
924
|
+
if (sees === "lead-only") {
|
|
925
|
+
return statements.filter((statement) => statement.seat !== viewer.id && leadIds.has(statement.seat));
|
|
926
|
+
}
|
|
927
|
+
const allowed = new Set(sees);
|
|
928
|
+
return statements.filter((statement) => statement.seat !== viewer.id && allowed.has(statement.seat));
|
|
929
|
+
}
|
|
930
|
+
function renderStatement(statement) {
|
|
931
|
+
const label = [statement.name, statement.role].filter(Boolean).join(" \xB7 ");
|
|
932
|
+
const body = statement.structured !== void 0 && statement.structured !== null ? JSON.stringify(statement.structured, null, 2) : statement.content;
|
|
933
|
+
return `### ${label}
|
|
934
|
+
|
|
935
|
+
${body}`;
|
|
936
|
+
}
|
|
937
|
+
function buildUserPrompt(input) {
|
|
938
|
+
const blocks = [];
|
|
939
|
+
if (input.topic) {
|
|
940
|
+
blocks.push(`## \u672C\u6B21\u8BAE\u9898
|
|
941
|
+
|
|
942
|
+
${input.topic}`);
|
|
943
|
+
}
|
|
944
|
+
if (input.context.length) {
|
|
945
|
+
const items = input.context.map((item, index) => `${index + 1}. ${item}`).join("\n");
|
|
946
|
+
blocks.push(`## \u5DF2\u77E5\u6750\u6599
|
|
947
|
+
|
|
948
|
+
${items}`);
|
|
949
|
+
}
|
|
950
|
+
if (input.phaseInstruction) {
|
|
951
|
+
blocks.push(`## \u5F53\u524D\u9636\u6BB5
|
|
952
|
+
|
|
953
|
+
${input.phaseInstruction}`);
|
|
954
|
+
}
|
|
955
|
+
if (input.visible.length) {
|
|
956
|
+
blocks.push(`## \u524D\u5E8F\u53D1\u8A00
|
|
957
|
+
|
|
958
|
+
${input.visible.map(renderStatement).join("\n\n")}`);
|
|
959
|
+
}
|
|
960
|
+
if (input.revisionNotes?.length) {
|
|
961
|
+
const notes = input.revisionNotes.map((note) => `- ${note}`).join("\n");
|
|
962
|
+
blocks.push(`## \u4E0A\u4E00\u8F6E\u7684\u5426\u51B3\u7406\u7531
|
|
963
|
+
|
|
964
|
+
${notes}`);
|
|
965
|
+
}
|
|
966
|
+
blocks.push(
|
|
967
|
+
"## \u8F93\u51FA\u8981\u6C42\n\n\u53EA\u8F93\u51FA\u4F60\u7684\u53D1\u8A00\u672C\u8EAB\uFF0C\u4E0D\u8981\u590D\u8FF0\u4E0A\u8FF0\u6750\u6599\u3002\n\u82E5\u4F60\u62E5\u6709\u5426\u51B3\u6743\uFF0C\u8BF7\u5728\u9700\u8981\u5426\u51B3\u65F6\u5199\u660E\u7406\u7531\uFF1B\u6CA1\u6709\u7406\u7531\u7684\u5426\u51B3\u4F1A\u88AB\u5FFD\u7565\u3002"
|
|
968
|
+
);
|
|
969
|
+
return blocks.join("\n\n");
|
|
970
|
+
}
|
|
971
|
+
function defaultPhaseInstruction(phaseId) {
|
|
972
|
+
switch (phaseId) {
|
|
973
|
+
case "propose":
|
|
974
|
+
return "\u63D0\u51FA\u4E00\u4E2A\u4F60\u8BA4\u4E3A\u503C\u5F97\u8BA8\u8BBA\u7684\u5177\u4F53\u8BAE\u9898\uFF0C\u5E76\u8BF4\u660E\u4E3A\u4EC0\u4E48\u662F\u73B0\u5728\u3001\u4E3A\u4EC0\u4E48\u503C\u5F97\u505A\u3002";
|
|
975
|
+
case "discuss":
|
|
976
|
+
return "\u5BF9\u4F60\u770B\u5230\u7684\u524D\u5E8F\u89C2\u70B9\u4F5C\u51FA\u56DE\u5E94\uFF1A\u53EF\u4EE5\u9644\u8BAE\u3001\u53CD\u9A73\u6216\u8865\u5145\uFF0C\u4F46\u5FC5\u987B\u7ED9\u51FA\u7406\u7531\u3002";
|
|
977
|
+
case "produce":
|
|
978
|
+
return "\u6309\u5DF2\u8FBE\u6210\u7684\u65B9\u5411\u4EA7\u51FA\u5B9E\u9645\u7ED3\u679C\u3002";
|
|
979
|
+
case "review":
|
|
980
|
+
return "\u6838\u5BF9\u4EA7\u51FA\u662F\u5426\u6EE1\u8DB3\u8981\u6C42\u3002\u53D1\u73B0\u95EE\u9898\u8BF7\u9010\u6761\u5217\u51FA\uFF1B\u9700\u8981\u5426\u51B3\u65F6\u5199\u660E\u7406\u7531\u3002";
|
|
981
|
+
case "converge":
|
|
982
|
+
return "\u6536\u53E3\uFF1A\u7ED9\u51FA\u7ED3\u8BBA\u3001\u9057\u7559\u5206\u6B67\u548C\u4E0B\u4E00\u6B65\u3002";
|
|
983
|
+
default:
|
|
984
|
+
return void 0;
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
function judgePhase(phase, seats, votes, table) {
|
|
989
|
+
const byId = new Map(seats.map((seat) => [seat.id, seat]));
|
|
990
|
+
const vetoed = [];
|
|
991
|
+
const rejected = [];
|
|
992
|
+
const invalidVetoes = [];
|
|
993
|
+
let passWeight = 0;
|
|
994
|
+
let voteWeight = 0;
|
|
995
|
+
const vetoEnabled = phase.vetoable;
|
|
996
|
+
for (const vote of votes) {
|
|
997
|
+
const seat = byId.get(vote.seatId);
|
|
998
|
+
if (!seat || vote.vote === void 0) {
|
|
999
|
+
continue;
|
|
1000
|
+
}
|
|
1001
|
+
voteWeight += vote.weight;
|
|
1002
|
+
if (vote.vote === "pass") {
|
|
1003
|
+
passWeight += vote.weight;
|
|
1004
|
+
continue;
|
|
1005
|
+
}
|
|
1006
|
+
rejected.push(seat.id);
|
|
1007
|
+
if (!vote.veto) {
|
|
1008
|
+
continue;
|
|
1009
|
+
}
|
|
1010
|
+
if (!vetoEnabled || !seat.seat.canVeto) {
|
|
1011
|
+
continue;
|
|
1012
|
+
}
|
|
1013
|
+
if (!vote.hasReason) {
|
|
1014
|
+
invalidVetoes.push(seat.id);
|
|
1015
|
+
continue;
|
|
1016
|
+
}
|
|
1017
|
+
vetoed.push(seat.id);
|
|
1018
|
+
}
|
|
1019
|
+
const score = voteWeight === 0 ? 0 : passWeight / voteWeight;
|
|
1020
|
+
const passed = vetoed.length === 0 && voteWeight > 0 && score >= table.threshold;
|
|
1021
|
+
return {
|
|
1022
|
+
passed,
|
|
1023
|
+
vetoed,
|
|
1024
|
+
rejected,
|
|
1025
|
+
score,
|
|
1026
|
+
invalidVetoes
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
const ARTIFACT_THRESHOLD = 8e3;
|
|
1031
|
+
function remainingTotal(budget) {
|
|
1032
|
+
if (budget.total === void 0) {
|
|
1033
|
+
return Number.POSITIVE_INFINITY;
|
|
1034
|
+
}
|
|
1035
|
+
return budget.total - budget.used;
|
|
1036
|
+
}
|
|
1037
|
+
function seatLimit(seat, budget) {
|
|
1038
|
+
return seat.seat.budget ?? budget.perSeatLimit[seat.id];
|
|
1039
|
+
}
|
|
1040
|
+
function canSpend(seat, budget) {
|
|
1041
|
+
if (remainingTotal(budget) <= 0) {
|
|
1042
|
+
return false;
|
|
1043
|
+
}
|
|
1044
|
+
const limit = seatLimit(seat, budget);
|
|
1045
|
+
if (limit === void 0) {
|
|
1046
|
+
return true;
|
|
1047
|
+
}
|
|
1048
|
+
return (budget.perSeat[seat.id] ?? 0) < limit;
|
|
1049
|
+
}
|
|
1050
|
+
function spend(seatId, budget) {
|
|
1051
|
+
budget.used += 1;
|
|
1052
|
+
budget.perSeat[seatId] = (budget.perSeat[seatId] ?? 0) + 1;
|
|
1053
|
+
}
|
|
1054
|
+
async function callSeat(seat, prompt, options) {
|
|
1055
|
+
const started = Date.now();
|
|
1056
|
+
const wantJson = seat.seat.output === "json";
|
|
1057
|
+
const call = options.callText ?? text;
|
|
1058
|
+
try {
|
|
1059
|
+
const result = await call({
|
|
1060
|
+
prompt,
|
|
1061
|
+
system: renderSeatSystemPrompt(seat),
|
|
1062
|
+
json: wantJson,
|
|
1063
|
+
temperature: seat.seat.temperature,
|
|
1064
|
+
maxTokens: seat.seat.maxTokens,
|
|
1065
|
+
// How long one seat may spend reading and answering. The table sets it
|
|
1066
|
+
// because a seat's work is longer than a short request; leaving it
|
|
1067
|
+
// undefined keeps the transport default.
|
|
1068
|
+
timeoutMs: options.loaded.table.timeoutMs,
|
|
1069
|
+
// Paid, non-idempotent: never resend after an unknown outcome.
|
|
1070
|
+
retries: seat.seat.fallback === "retry" ? void 0 : 0,
|
|
1071
|
+
env: options.env
|
|
1072
|
+
});
|
|
1073
|
+
let structured;
|
|
1074
|
+
if (wantJson) {
|
|
1075
|
+
try {
|
|
1076
|
+
structured = JSON.parse(result.value);
|
|
1077
|
+
} catch {
|
|
1078
|
+
return {
|
|
1079
|
+
ok: false,
|
|
1080
|
+
content: result.value,
|
|
1081
|
+
model: result.model,
|
|
1082
|
+
usage: result.usage,
|
|
1083
|
+
durationMs: Date.now() - started,
|
|
1084
|
+
error: "output: seat returned invalid JSON"
|
|
1085
|
+
};
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
return {
|
|
1089
|
+
ok: true,
|
|
1090
|
+
content: result.value,
|
|
1091
|
+
structured,
|
|
1092
|
+
model: result.model,
|
|
1093
|
+
usage: result.usage,
|
|
1094
|
+
durationMs: Date.now() - started
|
|
1095
|
+
};
|
|
1096
|
+
} catch (error) {
|
|
1097
|
+
const describe = isAiError(error) ? error.describe() : error instanceof Error ? error.message : String(error);
|
|
1098
|
+
return {
|
|
1099
|
+
ok: false,
|
|
1100
|
+
content: "",
|
|
1101
|
+
durationMs: Date.now() - started,
|
|
1102
|
+
error: describe
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
function emptyStatement(seat, phaseId, status, extra) {
|
|
1107
|
+
return {
|
|
1108
|
+
seat: seat.id,
|
|
1109
|
+
name: seat.identity.name,
|
|
1110
|
+
role: seat.role,
|
|
1111
|
+
phase: phaseId,
|
|
1112
|
+
status,
|
|
1113
|
+
content: "",
|
|
1114
|
+
...extra
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1117
|
+
async function runPhase(phase, attempt, options, priorStatements, revisionNotes, dependenciesProduced) {
|
|
1118
|
+
const { loaded, budget, events } = options;
|
|
1119
|
+
const seats = phase.seats.map((id) => loaded.byId.get(id)).filter((seat) => Boolean(seat));
|
|
1120
|
+
const leadIds = new Set(loaded.seats.filter((seat) => seat.seat.lead).map((seat) => seat.id));
|
|
1121
|
+
if (!dependenciesProduced) {
|
|
1122
|
+
return {
|
|
1123
|
+
record: {
|
|
1124
|
+
id: phase.id,
|
|
1125
|
+
status: "skipped",
|
|
1126
|
+
statements: [],
|
|
1127
|
+
pruned: true
|
|
1128
|
+
}
|
|
1129
|
+
};
|
|
1130
|
+
}
|
|
1131
|
+
events?.onPhaseStart?.(phase, attempt);
|
|
1132
|
+
const instruction = options.phaseInstructions?.[phase.id] ?? defaultPhaseInstruction(phase.id);
|
|
1133
|
+
const statements = [];
|
|
1134
|
+
const votes = [];
|
|
1135
|
+
let stop;
|
|
1136
|
+
const runSeat = async (seat) => {
|
|
1137
|
+
if (!canSpend(seat, budget)) {
|
|
1138
|
+
statements.push(emptyStatement(seat, phase.id, "budget-exhausted"));
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
1141
|
+
const visible = visibleStatements(seat, priorStatements.concat(statements), leadIds);
|
|
1142
|
+
const prompt = buildUserPrompt({
|
|
1143
|
+
phaseId: phase.id,
|
|
1144
|
+
topic: options.topic,
|
|
1145
|
+
context: options.context,
|
|
1146
|
+
visible,
|
|
1147
|
+
revisionNotes,
|
|
1148
|
+
phaseInstruction: instruction
|
|
1149
|
+
});
|
|
1150
|
+
spend(seat.id, budget);
|
|
1151
|
+
await options.persist?.();
|
|
1152
|
+
const call = await callSeat(seat, prompt, options);
|
|
1153
|
+
if (!call.ok) {
|
|
1154
|
+
const status2 = "failed";
|
|
1155
|
+
statements.push(emptyStatement(seat, phase.id, status2, {
|
|
1156
|
+
content: call.content,
|
|
1157
|
+
error: call.error,
|
|
1158
|
+
durationMs: call.durationMs,
|
|
1159
|
+
model: call.model,
|
|
1160
|
+
usage: call.usage
|
|
1161
|
+
}));
|
|
1162
|
+
if (seat.seat.fallback === "abort") {
|
|
1163
|
+
stop = "aborted";
|
|
1164
|
+
}
|
|
1165
|
+
return;
|
|
1166
|
+
}
|
|
1167
|
+
const statement = {
|
|
1168
|
+
seat: seat.id,
|
|
1169
|
+
name: seat.identity.name,
|
|
1170
|
+
role: seat.role,
|
|
1171
|
+
phase: phase.id,
|
|
1172
|
+
status: "completed",
|
|
1173
|
+
content: call.content,
|
|
1174
|
+
structured: call.structured,
|
|
1175
|
+
durationMs: call.durationMs,
|
|
1176
|
+
model: call.model,
|
|
1177
|
+
usage: call.usage
|
|
1178
|
+
};
|
|
1179
|
+
if (options.writeArtifact && call.content.length > ARTIFACT_THRESHOLD) {
|
|
1180
|
+
const attempt2 = options.attempt ?? 1;
|
|
1181
|
+
const relative = `artifacts/attempt-${attempt2}-${seat.id}-${phase.id}.md`;
|
|
1182
|
+
const bytes = await options.writeArtifact(relative, call.content);
|
|
1183
|
+
statement.artifacts = [
|
|
1184
|
+
{ path: relative, bytes }
|
|
1185
|
+
];
|
|
1186
|
+
}
|
|
1187
|
+
statements.push(statement);
|
|
1188
|
+
events?.onStatement?.(statement);
|
|
1189
|
+
if (phase.vetoable) {
|
|
1190
|
+
const verdict = readJudgement(call.structured, call.content);
|
|
1191
|
+
const vote = {
|
|
1192
|
+
seatId: seat.id,
|
|
1193
|
+
weight: seat.seat.weight,
|
|
1194
|
+
vote: verdict.verdict,
|
|
1195
|
+
veto: verdict.veto,
|
|
1196
|
+
hasReason: verdict.hasReason
|
|
1197
|
+
};
|
|
1198
|
+
if (verdict.invalidVeto) {
|
|
1199
|
+
statement.status = "invalid-veto";
|
|
1200
|
+
}
|
|
1201
|
+
votes.push(vote);
|
|
1202
|
+
}
|
|
1203
|
+
};
|
|
1204
|
+
if (phase.mode === "parallel") {
|
|
1205
|
+
await Promise.all(seats.map(async (seat) => runSeat(seat)));
|
|
1206
|
+
} else {
|
|
1207
|
+
for (const seat of seats) {
|
|
1208
|
+
await runSeat(seat);
|
|
1209
|
+
if (stop) {
|
|
1210
|
+
break;
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
if (stop) {
|
|
1215
|
+
return {
|
|
1216
|
+
record: { id: phase.id, status: "failed", statements },
|
|
1217
|
+
stop
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1220
|
+
const judgement = phase.vetoable ? judgePhase(phase, seats, votes, loaded.table) : void 0;
|
|
1221
|
+
const exhausted = statements.some((statement) => statement.status === "budget-exhausted");
|
|
1222
|
+
const completed = statements.some((statement) => statement.status === "completed");
|
|
1223
|
+
const status = exhausted && !completed ? "budget-exhausted" : completed ? "completed" : "failed";
|
|
1224
|
+
const record = {
|
|
1225
|
+
id: phase.id,
|
|
1226
|
+
status,
|
|
1227
|
+
statements,
|
|
1228
|
+
judgement
|
|
1229
|
+
};
|
|
1230
|
+
events?.onPhaseEnd?.(phase, record, attempt);
|
|
1231
|
+
return {
|
|
1232
|
+
record,
|
|
1233
|
+
stop: exhausted && !completed ? "budget-exhausted" : void 0
|
|
1234
|
+
};
|
|
1235
|
+
}
|
|
1236
|
+
function readJudgement(structured, content) {
|
|
1237
|
+
if (structured && typeof structured === "object" && !Array.isArray(structured)) {
|
|
1238
|
+
const source = structured;
|
|
1239
|
+
const verdict = source.verdict === "pass" || source.verdict === "reject" ? source.verdict : void 0;
|
|
1240
|
+
const reason = typeof source.reason === "string" ? source.reason.trim() : "";
|
|
1241
|
+
const veto = verdict === "reject";
|
|
1242
|
+
return {
|
|
1243
|
+
verdict,
|
|
1244
|
+
veto,
|
|
1245
|
+
hasReason: reason.length > 0,
|
|
1246
|
+
invalidVeto: veto && reason.length === 0
|
|
1247
|
+
};
|
|
1248
|
+
}
|
|
1249
|
+
const head = content.trim().slice(0, 40);
|
|
1250
|
+
if (/^(?:否决|不通过|reject)/i.test(head)) {
|
|
1251
|
+
const reason = content.trim().slice(head.length).replace(/^[::\s]+/, "").trim();
|
|
1252
|
+
return {
|
|
1253
|
+
verdict: "reject",
|
|
1254
|
+
veto: true,
|
|
1255
|
+
hasReason: reason.length > 0,
|
|
1256
|
+
invalidVeto: reason.length === 0
|
|
1257
|
+
};
|
|
1258
|
+
}
|
|
1259
|
+
if (/^(?:通过|approve|pass)\b/i.test(head)) {
|
|
1260
|
+
return { verdict: "pass", veto: false };
|
|
1261
|
+
}
|
|
1262
|
+
return {};
|
|
1263
|
+
}
|
|
1264
|
+
async function runAttempt(attempt, options) {
|
|
1265
|
+
const { loaded, events } = options;
|
|
1266
|
+
const phases = [];
|
|
1267
|
+
let stop;
|
|
1268
|
+
let judgement;
|
|
1269
|
+
const startIndex = options.startPhaseId ? Math.max(0, loaded.phases.findIndex((item) => item.id === options.startPhaseId)) : 0;
|
|
1270
|
+
const finished = [
|
|
1271
|
+
...options.completedPhases ?? []
|
|
1272
|
+
];
|
|
1273
|
+
let allStatements = finished.flatMap((phase) => phase.statements);
|
|
1274
|
+
for (let index = startIndex; index < loaded.phases.length; index++) {
|
|
1275
|
+
const phase = loaded.phases[index];
|
|
1276
|
+
const previous = loaded.phases[index - 1];
|
|
1277
|
+
const deps = phase.requires.length ? phase.requires : previous ? [previous.id] : [];
|
|
1278
|
+
const dependenciesProduced = deps.every((dep) => {
|
|
1279
|
+
const record2 = phases.find((item) => item.id === dep) ?? finished.find((item) => item.id === dep);
|
|
1280
|
+
return Boolean(record2 && record2.status === "completed");
|
|
1281
|
+
});
|
|
1282
|
+
const notes = attempt > 1 && phase.id === loaded.table.special.propose ? options.revisionNotes ?? [] : [];
|
|
1283
|
+
const { record, stop: phaseStop } = await runPhase(
|
|
1284
|
+
phase,
|
|
1285
|
+
attempt,
|
|
1286
|
+
options,
|
|
1287
|
+
allStatements,
|
|
1288
|
+
notes,
|
|
1289
|
+
deps.length === 0 || dependenciesProduced
|
|
1290
|
+
);
|
|
1291
|
+
phases.push(record);
|
|
1292
|
+
allStatements = allStatements.concat(record.statements);
|
|
1293
|
+
await options.persist?.();
|
|
1294
|
+
if (record.judgement) {
|
|
1295
|
+
judgement = record.judgement;
|
|
1296
|
+
}
|
|
1297
|
+
if (phaseStop) {
|
|
1298
|
+
stop = phaseStop;
|
|
1299
|
+
break;
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
events?.onAttempt?.(attempt, judgement);
|
|
1303
|
+
return {
|
|
1304
|
+
round: { attempt, phases },
|
|
1305
|
+
judgement,
|
|
1306
|
+
stop
|
|
1307
|
+
};
|
|
1308
|
+
}
|
|
1309
|
+
function collectRejectionNotes(round, loaded) {
|
|
1310
|
+
const notes = [];
|
|
1311
|
+
for (const phase of round.phases) {
|
|
1312
|
+
const judgement = phase.judgement;
|
|
1313
|
+
if (!judgement) {
|
|
1314
|
+
continue;
|
|
1315
|
+
}
|
|
1316
|
+
for (const seatId of [
|
|
1317
|
+
...judgement.vetoed,
|
|
1318
|
+
...judgement.rejected
|
|
1319
|
+
]) {
|
|
1320
|
+
const statement = phase.statements.find((item) => item.seat === seatId && item.status === "completed");
|
|
1321
|
+
if (!statement) {
|
|
1322
|
+
continue;
|
|
1323
|
+
}
|
|
1324
|
+
const seat = loaded.byId.get(seatId);
|
|
1325
|
+
const label = seat ? `${seat.identity.name}\uFF08${seat.role}\uFF09` : seatId;
|
|
1326
|
+
const reason = extractReason(statement);
|
|
1327
|
+
notes.push(`${label}\uFF1A${reason || "\u672A\u8BF4\u660E\u5177\u4F53\u7406\u7531"}`);
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
return notes;
|
|
1331
|
+
}
|
|
1332
|
+
function extractReason(statement) {
|
|
1333
|
+
if (statement.structured && typeof statement.structured === "object" && !Array.isArray(statement.structured)) {
|
|
1334
|
+
const source = statement.structured;
|
|
1335
|
+
if (typeof source.reason === "string" && source.reason.trim()) {
|
|
1336
|
+
return source.reason.trim();
|
|
1337
|
+
}
|
|
1338
|
+
if (Array.isArray(source.issues)) {
|
|
1339
|
+
const issues = source.issues.filter((item) => typeof item === "string" && item.trim() !== "");
|
|
1340
|
+
if (issues.length) {
|
|
1341
|
+
return issues.slice(0, 5).join("\uFF1B");
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
return statement.content.trim().slice(0, 300);
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
const RECORD_SCHEMA = 1;
|
|
1349
|
+
function statusWord(status) {
|
|
1350
|
+
switch (status) {
|
|
1351
|
+
case "passed":
|
|
1352
|
+
return "\u901A\u8FC7";
|
|
1353
|
+
case "rejected":
|
|
1354
|
+
return "\u672A\u901A\u8FC7";
|
|
1355
|
+
case "aborted":
|
|
1356
|
+
return "\u4E2D\u6B62";
|
|
1357
|
+
case "failed":
|
|
1358
|
+
return "\u5931\u8D25";
|
|
1359
|
+
default:
|
|
1360
|
+
return "\u8FDB\u884C\u4E2D";
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
function formatDuration(ms) {
|
|
1364
|
+
const seconds = Math.round(ms / 1e3);
|
|
1365
|
+
if (seconds < 60) {
|
|
1366
|
+
return `${seconds} \u79D2`;
|
|
1367
|
+
}
|
|
1368
|
+
const minutes = Math.floor(seconds / 60);
|
|
1369
|
+
const rest = seconds % 60;
|
|
1370
|
+
return rest ? `${minutes} \u5206 ${rest} \u79D2` : `${minutes} \u5206`;
|
|
1371
|
+
}
|
|
1372
|
+
function statementLabel(statement, loaded) {
|
|
1373
|
+
const seat = loaded.byId.get(statement.seat);
|
|
1374
|
+
const name = statement.name || seat?.identity.name || statement.seat;
|
|
1375
|
+
const role = statement.role || seat?.role || "";
|
|
1376
|
+
const avatar = seat?.identity.avatar;
|
|
1377
|
+
const parts = [avatar, name].filter(Boolean).join(" ");
|
|
1378
|
+
return role ? `${parts} \xB7 ${role}` : parts;
|
|
1379
|
+
}
|
|
1380
|
+
function renderRecordMarkdown(record, loaded) {
|
|
1381
|
+
const lines = [];
|
|
1382
|
+
const date = record.startedAt.slice(0, 10);
|
|
1383
|
+
lines.push(`# ${record.title} \xB7 ${date}`, "");
|
|
1384
|
+
const vetoText = record.verdict.vetoed.length ? `\uFF0C\u5426\u51B3\uFF1A${record.verdict.vetoed.join("\u3001")}` : "\uFF0C\u65E0\u5426\u51B3";
|
|
1385
|
+
lines.push(`**\u7ED3\u8BBA**\uFF1A${statusWord(record.status)}\uFF08\u901A\u8FC7\u7387 ${Math.round(record.verdict.score * 100)}%${vetoText}\uFF09`);
|
|
1386
|
+
const elapsed = record.finishedAt ? formatDuration(new Date(record.finishedAt).getTime() - new Date(record.startedAt).getTime()) : "\u8FDB\u884C\u4E2D";
|
|
1387
|
+
const total = record.budget.total === void 0 ? "\u4E0D\u9650" : String(record.budget.total);
|
|
1388
|
+
lines.push(`**\u8F6E\u6B21**\uFF1A\u7B2C ${record.attempts} \u8F6E \xB7 \u9884\u7B97 ${record.budget.used}/${total} \xB7 \u7528\u65F6 ${elapsed}`);
|
|
1389
|
+
const absent = [];
|
|
1390
|
+
const latest = record.rounds[record.rounds.length - 1];
|
|
1391
|
+
if (latest) {
|
|
1392
|
+
for (const phase of latest.phases) {
|
|
1393
|
+
const heading = phaseHeading(phase.id, loaded);
|
|
1394
|
+
const completed = phase.statements.filter((statement) => statement.status === "completed");
|
|
1395
|
+
for (const statement of phase.statements) {
|
|
1396
|
+
if (statement.status !== "completed") {
|
|
1397
|
+
absent.push(`${statementLabel(statement, loaded)}\uFF08${describeStatus(statement.status)}\uFF09`);
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
if (!completed.length) {
|
|
1401
|
+
if (phase.pruned) {
|
|
1402
|
+
lines.push("", `## ${heading}`, "", "\uFF08\u524D\u7F6E\u9636\u6BB5\u6CA1\u6709\u4EA7\u51FA\uFF0C\u672C\u9636\u6BB5\u8DF3\u8FC7\uFF09");
|
|
1403
|
+
}
|
|
1404
|
+
continue;
|
|
1405
|
+
}
|
|
1406
|
+
lines.push("", `## ${heading}`, "");
|
|
1407
|
+
for (const statement of completed) {
|
|
1408
|
+
lines.push(`**${statementLabel(statement, loaded)}**`, "");
|
|
1409
|
+
lines.push(renderStatementBody(statement), "");
|
|
1410
|
+
}
|
|
1411
|
+
if (phase.judgement) {
|
|
1412
|
+
lines.push(renderJudgement(phase.judgement, loaded), "");
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
if (record.verdict.invalidVetoes.length) {
|
|
1417
|
+
lines.push("## \u88AB\u5FFD\u7565\u7684\u5426\u51B3", "", `\u4EE5\u4E0B\u5426\u51B3\u672A\u5199\u660E\u7406\u7531\uFF0C\u6309\u89C4\u5219\u89C6\u4E3A\u65E0\u6548\uFF1A${record.verdict.invalidVetoes.join("\u3001")}`, "");
|
|
1418
|
+
}
|
|
1419
|
+
if (absent.length) {
|
|
1420
|
+
lines.push("## \u7F3A\u5E2D\u540D\u5355", "", ...absent.map((item) => `- ${item}`), "");
|
|
1421
|
+
}
|
|
1422
|
+
if (record.artifacts?.length) {
|
|
1423
|
+
lines.push("## \u4EA7\u7269", "", ...record.artifacts.map((path) => `- ${path}`), "");
|
|
1424
|
+
}
|
|
1425
|
+
return `${lines.join("\n").trimEnd()}
|
|
1426
|
+
`;
|
|
1427
|
+
}
|
|
1428
|
+
function phaseHeading(phaseId, loaded) {
|
|
1429
|
+
const special = loaded.table.special;
|
|
1430
|
+
if (phaseId === special.propose) {
|
|
1431
|
+
return "\u8BAE\u9898";
|
|
1432
|
+
}
|
|
1433
|
+
if (phaseId === special.converge) {
|
|
1434
|
+
return "\u6536\u53E3";
|
|
1435
|
+
}
|
|
1436
|
+
const season = {
|
|
1437
|
+
propose: "\u8BAE\u9898",
|
|
1438
|
+
discuss: "\u8BA8\u8BBA",
|
|
1439
|
+
produce: "\u4EA7\u51FA",
|
|
1440
|
+
review: "\u5BA1\u6838",
|
|
1441
|
+
converge: "\u6536\u53E3"
|
|
1442
|
+
};
|
|
1443
|
+
return season[phaseId] ?? phaseId;
|
|
1444
|
+
}
|
|
1445
|
+
function describeStatus(status) {
|
|
1446
|
+
switch (status) {
|
|
1447
|
+
case "skipped":
|
|
1448
|
+
return "\u8DF3\u8FC7";
|
|
1449
|
+
case "failed":
|
|
1450
|
+
return "\u5931\u8D25";
|
|
1451
|
+
case "budget-exhausted":
|
|
1452
|
+
return "\u9884\u7B97\u8017\u5C3D";
|
|
1453
|
+
case "invalid-veto":
|
|
1454
|
+
return "\u5426\u51B3\u65E0\u6548";
|
|
1455
|
+
default:
|
|
1456
|
+
return status;
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
function renderStatementBody(statement) {
|
|
1460
|
+
const { structured } = statement;
|
|
1461
|
+
if (structured && typeof structured === "object" && !Array.isArray(structured)) {
|
|
1462
|
+
const entries = Object.entries(structured);
|
|
1463
|
+
if (entries.length) {
|
|
1464
|
+
const rows = entries.map(([key, value]) => {
|
|
1465
|
+
const text = typeof value === "string" ? value : JSON.stringify(value);
|
|
1466
|
+
return `| ${key} | ${String(text).replace(/\|/g, "\\|")} |`;
|
|
1467
|
+
});
|
|
1468
|
+
return ["| \u952E | \u503C |", "| --- | --- |", ...rows].join("\n");
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
return statement.content.trim();
|
|
1472
|
+
}
|
|
1473
|
+
function renderJudgement(judgement, loaded) {
|
|
1474
|
+
const parts = [];
|
|
1475
|
+
parts.push(judgement.passed ? "\u88C1\u51B3\uFF1A\u901A\u8FC7" : "\u88C1\u51B3\uFF1A\u672A\u901A\u8FC7");
|
|
1476
|
+
parts.push(`\u901A\u8FC7\u7387 ${Math.round(judgement.score * 100)}%`);
|
|
1477
|
+
if (judgement.vetoed.length) {
|
|
1478
|
+
const names = judgement.vetoed.map((id) => loaded.byId.get(id)?.identity.name ?? id);
|
|
1479
|
+
parts.push(`\u5426\u51B3\uFF1A${names.join("\u3001")}`);
|
|
1480
|
+
}
|
|
1481
|
+
if (judgement.rejected.length && !judgement.vetoed.length) {
|
|
1482
|
+
const names = judgement.rejected.map((id) => loaded.byId.get(id)?.identity.name ?? id);
|
|
1483
|
+
parts.push(`\u53CD\u5BF9\uFF1A${names.join("\u3001")}`);
|
|
1484
|
+
}
|
|
1485
|
+
return `> ${parts.join(" \xB7 ")}`;
|
|
1486
|
+
}
|
|
1487
|
+
async function writeJson(path, value) {
|
|
1488
|
+
await mkdir(dirname(path), { recursive: true });
|
|
1489
|
+
await writeFile(path, `${JSON.stringify(value, null, 2)}
|
|
1490
|
+
`, "utf8");
|
|
1491
|
+
}
|
|
1492
|
+
async function writeText(path, value) {
|
|
1493
|
+
await mkdir(dirname(path), { recursive: true });
|
|
1494
|
+
await writeFile(path, value, "utf8");
|
|
1495
|
+
}
|
|
1496
|
+
function buildRecord(state, loaded) {
|
|
1497
|
+
const finalJudgement = state.rounds.flatMap((round) => round.phases).filter((phase) => Boolean(phase.judgement)).pop()?.judgement;
|
|
1498
|
+
const artifacts = state.rounds.flatMap((round) => round.phases).flatMap((phase) => phase.statements).flatMap((statement) => statement.artifacts ?? []).map((item) => item.path);
|
|
1499
|
+
return {
|
|
1500
|
+
schema: RECORD_SCHEMA,
|
|
1501
|
+
title: loaded.table.title,
|
|
1502
|
+
startedAt: state.startedAt,
|
|
1503
|
+
finishedAt: state.finishedAt,
|
|
1504
|
+
status: state.status,
|
|
1505
|
+
attempts: state.rounds.length,
|
|
1506
|
+
verdict: {
|
|
1507
|
+
passed: state.status === "passed",
|
|
1508
|
+
vetoed: finalJudgement?.vetoed ?? [],
|
|
1509
|
+
rejected: finalJudgement?.rejected ?? [],
|
|
1510
|
+
score: finalJudgement?.score ?? 0,
|
|
1511
|
+
invalidVetoes: state.rounds.flatMap((round) => round.phases).flatMap((phase) => phase.judgement?.invalidVetoes ?? [])
|
|
1512
|
+
},
|
|
1513
|
+
rounds: state.rounds,
|
|
1514
|
+
budget: {
|
|
1515
|
+
total: state.budget.total,
|
|
1516
|
+
used: state.budget.used,
|
|
1517
|
+
perSeat: state.budget.perSeat
|
|
1518
|
+
},
|
|
1519
|
+
artifacts: artifacts.length ? artifacts : void 0,
|
|
1520
|
+
error: state.error
|
|
1521
|
+
};
|
|
1522
|
+
}
|
|
1523
|
+
function makeRunId(date = /* @__PURE__ */ new Date()) {
|
|
1524
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
1525
|
+
return [
|
|
1526
|
+
date.getFullYear(),
|
|
1527
|
+
"-",
|
|
1528
|
+
pad(date.getMonth() + 1),
|
|
1529
|
+
"-",
|
|
1530
|
+
pad(date.getDate()),
|
|
1531
|
+
"T",
|
|
1532
|
+
pad(date.getHours()),
|
|
1533
|
+
"-",
|
|
1534
|
+
pad(date.getMinutes()),
|
|
1535
|
+
"-",
|
|
1536
|
+
pad(date.getSeconds())
|
|
1537
|
+
].join("");
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
function toContextList(context) {
|
|
1541
|
+
if (context === void 0 || context === null) {
|
|
1542
|
+
return [];
|
|
1543
|
+
}
|
|
1544
|
+
if (typeof context === "string") {
|
|
1545
|
+
return context.trim() ? [context] : [];
|
|
1546
|
+
}
|
|
1547
|
+
if (!Array.isArray(context)) {
|
|
1548
|
+
throw new RoundtableConfigError("context must be a string or an array of strings");
|
|
1549
|
+
}
|
|
1550
|
+
return context.map((item, index) => {
|
|
1551
|
+
if (typeof item !== "string") {
|
|
1552
|
+
throw new RoundtableConfigError(`context[${index}] must be a string`);
|
|
1553
|
+
}
|
|
1554
|
+
return item.trim();
|
|
1555
|
+
}).filter(Boolean);
|
|
1556
|
+
}
|
|
1557
|
+
function initialBudget(loaded) {
|
|
1558
|
+
const perSeatLimit = {};
|
|
1559
|
+
for (const seat of loaded.seats) {
|
|
1560
|
+
perSeatLimit[seat.id] = seat.seat.budget ?? loaded.table.budget.perSeat;
|
|
1561
|
+
}
|
|
1562
|
+
return {
|
|
1563
|
+
total: loaded.table.budget.total,
|
|
1564
|
+
used: 0,
|
|
1565
|
+
perSeat: {},
|
|
1566
|
+
perSeatLimit
|
|
1567
|
+
};
|
|
1568
|
+
}
|
|
1569
|
+
function phaseInstructions(loaded, tableBody) {
|
|
1570
|
+
const result = {};
|
|
1571
|
+
if (!tableBody.trim()) {
|
|
1572
|
+
return result;
|
|
1573
|
+
}
|
|
1574
|
+
const sections = tableBody.split(/^##\s+/m).slice(1);
|
|
1575
|
+
for (const section of sections) {
|
|
1576
|
+
const [head, ...rest] = section.split("\n");
|
|
1577
|
+
const id = head?.trim();
|
|
1578
|
+
if (id && loaded.phases.some((phase) => phase.id === id)) {
|
|
1579
|
+
const text = rest.join("\n").trim();
|
|
1580
|
+
if (text) {
|
|
1581
|
+
result[id] = text;
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
return result;
|
|
1586
|
+
}
|
|
1587
|
+
async function readTableBody(configDir) {
|
|
1588
|
+
const { readFile } = await import('node:fs/promises');
|
|
1589
|
+
const source = await readFile(join(resolve(configDir), "table.md"), "utf8");
|
|
1590
|
+
return readFrontmatter(source, "table.md").body;
|
|
4
1591
|
}
|
|
5
|
-
async function
|
|
6
|
-
|
|
1592
|
+
async function runRoundtable(options) {
|
|
1593
|
+
const loaded = await loadRoundtable({ configDir: options.configDir, schema: RECORD_SCHEMA });
|
|
1594
|
+
const context = toContextList(options.context);
|
|
1595
|
+
const outDir = resolve(options.outDir ?? defaultOutDir());
|
|
1596
|
+
const runId = makeRunId();
|
|
1597
|
+
const runDir = join(outDir, runId);
|
|
1598
|
+
await mkdir(runDir, { recursive: true });
|
|
1599
|
+
const state = {
|
|
1600
|
+
schema: RECORD_SCHEMA,
|
|
1601
|
+
runId,
|
|
1602
|
+
status: "running",
|
|
1603
|
+
attempt: 1,
|
|
1604
|
+
phaseIndex: 0,
|
|
1605
|
+
budget: initialBudget(loaded),
|
|
1606
|
+
fingerprint: loaded.fingerprint,
|
|
1607
|
+
resumable: true,
|
|
1608
|
+
rounds: [],
|
|
1609
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1610
|
+
topic: options.topic,
|
|
1611
|
+
context
|
|
1612
|
+
};
|
|
1613
|
+
const statePath = join(runDir, "state.json");
|
|
1614
|
+
const recordPath = join(runDir, "record.json");
|
|
1615
|
+
const minutesPath = join(runDir, "record.md");
|
|
1616
|
+
const persist = () => writeJson(statePath, state);
|
|
1617
|
+
const writeArtifact = async (relativePath, content) => {
|
|
1618
|
+
const target = join(runDir, relativePath);
|
|
1619
|
+
await writeText(target, content);
|
|
1620
|
+
return new TextEncoder().encode(content).byteLength;
|
|
1621
|
+
};
|
|
1622
|
+
let revisionNotes = [];
|
|
1623
|
+
const instructions = phaseInstructions(loaded, await readTableBody(options.configDir));
|
|
1624
|
+
for (let attempt = 1; attempt <= loaded.table.maxAttempts; attempt++) {
|
|
1625
|
+
state.attempt = attempt;
|
|
1626
|
+
options.onProgress?.(`roundtable: \u7B2C ${attempt} \u8F6E\u5F00\u59CB`);
|
|
1627
|
+
const result = await runAttempt(attempt, {
|
|
1628
|
+
loaded,
|
|
1629
|
+
context,
|
|
1630
|
+
topic: options.topic,
|
|
1631
|
+
env: options.env,
|
|
1632
|
+
budget: state.budget,
|
|
1633
|
+
phaseInstructions: instructions,
|
|
1634
|
+
revisionNotes,
|
|
1635
|
+
persist,
|
|
1636
|
+
callText: options.callText,
|
|
1637
|
+
writeArtifact,
|
|
1638
|
+
attempt,
|
|
1639
|
+
events: {
|
|
1640
|
+
onStatement: (statement) => {
|
|
1641
|
+
const seat = loaded.byId.get(statement.seat);
|
|
1642
|
+
const name = seat?.identity.name ?? statement.seat;
|
|
1643
|
+
options.onProgress?.(` ${name} \xB7 ${statement.phase} \xB7 ${statement.status}`);
|
|
1644
|
+
},
|
|
1645
|
+
// Progress is positional only; the authoritative round data comes from
|
|
1646
|
+
// runAttempt's return value so a partially filled state can never leak
|
|
1647
|
+
// into the record.
|
|
1648
|
+
onPhaseStart: (phase) => {
|
|
1649
|
+
state.phaseId = phase.id;
|
|
1650
|
+
state.phaseIndex = loaded.phases.findIndex((item) => item.id === phase.id);
|
|
1651
|
+
},
|
|
1652
|
+
onPhaseEnd: () => {
|
|
1653
|
+
void persist();
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
});
|
|
1657
|
+
state.rounds = state.rounds.filter((round) => round.attempt !== attempt).concat(result.round);
|
|
1658
|
+
if (result.stop) {
|
|
1659
|
+
state.status = "aborted";
|
|
1660
|
+
state.error = result.stop === "budget-exhausted" ? `\u9884\u7B97\u5728\u9636\u6BB5 "${state.phaseId ?? ""}" \u8017\u5C3D` : "\u6709\u5E2D\u4F4D\u8981\u6C42\u4E2D\u6B62";
|
|
1661
|
+
break;
|
|
1662
|
+
}
|
|
1663
|
+
const passed = result.round.phases.filter((phase) => phase.judgement).every((phase) => phase.judgement.passed);
|
|
1664
|
+
const anyJudged = result.round.phases.some((phase) => phase.judgement);
|
|
1665
|
+
const completedAll = result.round.phases.every((phase) => phase.status === "completed");
|
|
1666
|
+
if (anyJudged ? passed : completedAll) {
|
|
1667
|
+
state.status = "passed";
|
|
1668
|
+
break;
|
|
1669
|
+
}
|
|
1670
|
+
revisionNotes = collectRejectionNotes(result.round, loaded);
|
|
1671
|
+
if (attempt === loaded.table.maxAttempts) {
|
|
1672
|
+
state.status = "rejected";
|
|
1673
|
+
break;
|
|
1674
|
+
}
|
|
1675
|
+
if (loaded.table.onVeto === "abort") {
|
|
1676
|
+
state.status = "aborted";
|
|
1677
|
+
state.error = "\u89C4\u5219\u8981\u6C42\u88AB\u5426\u51B3\u5373\u4E2D\u6B62";
|
|
1678
|
+
break;
|
|
1679
|
+
}
|
|
1680
|
+
options.onProgress?.(`roundtable: \u7B2C ${attempt} \u8F6E\u672A\u901A\u8FC7\uFF0C\u8FDB\u5165\u7B2C ${attempt + 1} \u8F6E`);
|
|
1681
|
+
}
|
|
1682
|
+
if (state.status === "running") {
|
|
1683
|
+
state.status = "rejected";
|
|
1684
|
+
}
|
|
1685
|
+
state.finishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1686
|
+
state.resumable = false;
|
|
1687
|
+
const record = buildRecord(state, loaded);
|
|
1688
|
+
await writeJson(statePath, state);
|
|
1689
|
+
await writeJson(recordPath, record);
|
|
1690
|
+
await writeText(minutesPath, renderRecordMarkdown(record, loaded));
|
|
1691
|
+
return {
|
|
1692
|
+
status: record.status,
|
|
1693
|
+
recordPath,
|
|
1694
|
+
runDir,
|
|
1695
|
+
record,
|
|
1696
|
+
used: state.budget.used,
|
|
1697
|
+
budgetTotal: state.budget.total
|
|
1698
|
+
};
|
|
7
1699
|
}
|
|
8
|
-
async function
|
|
9
|
-
|
|
1700
|
+
async function resumeRoundtable(options) {
|
|
1701
|
+
const runDir = resolve(options.runDir);
|
|
1702
|
+
const loaded = await loadRoundtable({ configDir: options.configDir, schema: RECORD_SCHEMA });
|
|
1703
|
+
const state = await readState(runDir);
|
|
1704
|
+
if (!state) {
|
|
1705
|
+
throw new RoundtableConfigError(`No state.json in ${runDir}; nothing to resume`);
|
|
1706
|
+
}
|
|
1707
|
+
if (state.fingerprint !== loaded.fingerprint) {
|
|
1708
|
+
throw new RoundtableConfigError(
|
|
1709
|
+
"Rules changed since this run started; open a new meeting instead of resuming"
|
|
1710
|
+
);
|
|
1711
|
+
}
|
|
1712
|
+
const recordPath = join(runDir, "record.json");
|
|
1713
|
+
const minutesPath = join(runDir, "record.md");
|
|
1714
|
+
if (state.status !== "running") {
|
|
1715
|
+
const existing = buildRecord(state, loaded);
|
|
1716
|
+
await writeJson(recordPath, existing);
|
|
1717
|
+
await writeText(minutesPath, renderRecordMarkdown(existing, loaded));
|
|
1718
|
+
return {
|
|
1719
|
+
status: existing.status,
|
|
1720
|
+
recordPath,
|
|
1721
|
+
runDir,
|
|
1722
|
+
record: existing,
|
|
1723
|
+
used: state.budget.used,
|
|
1724
|
+
budgetTotal: state.budget.total
|
|
1725
|
+
};
|
|
1726
|
+
}
|
|
1727
|
+
const context = state.context.length ? state.context : toContextList(options.context);
|
|
1728
|
+
const instructions = phaseInstructions(loaded, await readTableBody(options.configDir));
|
|
1729
|
+
const persist = () => writeJson(join(runDir, "state.json"), state);
|
|
1730
|
+
const writeArtifact = async (relativePath, content) => {
|
|
1731
|
+
await writeText(join(runDir, relativePath), content);
|
|
1732
|
+
return new TextEncoder().encode(content).byteLength;
|
|
1733
|
+
};
|
|
1734
|
+
for (let attempt = state.rounds.length || 1; attempt <= loaded.table.maxAttempts; attempt++) {
|
|
1735
|
+
state.attempt = attempt;
|
|
1736
|
+
options.onProgress?.(`roundtable: \u7EED\u8DD1\u7B2C ${attempt} \u8F6E`);
|
|
1737
|
+
const donePhases = state.rounds.find((round) => round.attempt === attempt)?.phases ?? [];
|
|
1738
|
+
const startPhase = loaded.phases.findIndex((phase) => {
|
|
1739
|
+
const record2 = donePhases.find((item) => item.id === phase.id);
|
|
1740
|
+
return !record2 || record2.status !== "completed";
|
|
1741
|
+
});
|
|
1742
|
+
if (startPhase === -1) {
|
|
1743
|
+
state.status = donePhases.some((phase) => phase.judgement && !phase.judgement.passed) ? "rejected" : "passed";
|
|
1744
|
+
break;
|
|
1745
|
+
}
|
|
1746
|
+
const startPhaseId = loaded.phases[startPhase]?.id;
|
|
1747
|
+
const revisionNotes = collectRejectionNotes(
|
|
1748
|
+
{ phases: state.rounds.at(-1)?.phases ?? [] },
|
|
1749
|
+
loaded
|
|
1750
|
+
);
|
|
1751
|
+
const result = await runAttempt(attempt, {
|
|
1752
|
+
loaded,
|
|
1753
|
+
context,
|
|
1754
|
+
topic: state.topic ?? options.topic,
|
|
1755
|
+
env: options.env,
|
|
1756
|
+
budget: state.budget,
|
|
1757
|
+
phaseInstructions: instructions,
|
|
1758
|
+
revisionNotes,
|
|
1759
|
+
persist,
|
|
1760
|
+
callText: options.callText,
|
|
1761
|
+
writeArtifact,
|
|
1762
|
+
attempt,
|
|
1763
|
+
// Finished phases stay available as input for the resumed ones.
|
|
1764
|
+
completedPhases: donePhases.filter((phase) => phase.status === "completed"),
|
|
1765
|
+
events: {
|
|
1766
|
+
onStatement: (statement) => {
|
|
1767
|
+
const seat = loaded.byId.get(statement.seat);
|
|
1768
|
+
options.onProgress?.(` ${seat?.identity.name ?? statement.seat} \xB7 ${statement.phase} \xB7 ${statement.status}`);
|
|
1769
|
+
},
|
|
1770
|
+
onPhaseStart: (phase) => {
|
|
1771
|
+
state.phaseId = phase.id;
|
|
1772
|
+
state.phaseIndex = loaded.phases.findIndex((item) => item.id === phase.id);
|
|
1773
|
+
}
|
|
1774
|
+
},
|
|
1775
|
+
startPhaseId
|
|
1776
|
+
});
|
|
1777
|
+
const merged = loaded.phases.map((phase) => {
|
|
1778
|
+
const fresh = result.round.phases.find((item) => item.id === phase.id);
|
|
1779
|
+
if (fresh) {
|
|
1780
|
+
return fresh;
|
|
1781
|
+
}
|
|
1782
|
+
const previous = donePhases.find((item) => item.id === phase.id);
|
|
1783
|
+
return previous ?? { id: phase.id, status: "skipped", statements: [] };
|
|
1784
|
+
});
|
|
1785
|
+
state.rounds = state.rounds.filter((round) => round.attempt !== attempt).concat({ attempt, phases: merged });
|
|
1786
|
+
if (result.stop) {
|
|
1787
|
+
state.status = "aborted";
|
|
1788
|
+
state.error = result.stop === "budget-exhausted" ? `\u9884\u7B97\u5728\u9636\u6BB5 "${state.phaseId ?? ""}" \u8017\u5C3D` : "\u6709\u5E2D\u4F4D\u8981\u6C42\u4E2D\u6B62";
|
|
1789
|
+
break;
|
|
1790
|
+
}
|
|
1791
|
+
const anyJudged = merged.some((phase) => phase.judgement);
|
|
1792
|
+
const passed = anyJudged && merged.every((phase) => !phase.judgement || phase.judgement.passed);
|
|
1793
|
+
if (anyJudged ? passed : merged.every((phase) => phase.status === "completed")) {
|
|
1794
|
+
state.status = "passed";
|
|
1795
|
+
break;
|
|
1796
|
+
}
|
|
1797
|
+
if (attempt === loaded.table.maxAttempts) {
|
|
1798
|
+
state.status = "rejected";
|
|
1799
|
+
break;
|
|
1800
|
+
}
|
|
1801
|
+
if (loaded.table.onVeto === "abort") {
|
|
1802
|
+
state.status = "aborted";
|
|
1803
|
+
state.error = "\u89C4\u5219\u8981\u6C42\u88AB\u5426\u51B3\u5373\u4E2D\u6B62";
|
|
1804
|
+
break;
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
if (state.status === "running") {
|
|
1808
|
+
state.status = "rejected";
|
|
1809
|
+
}
|
|
1810
|
+
state.finishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1811
|
+
state.resumable = false;
|
|
1812
|
+
const record = buildRecord(state, loaded);
|
|
1813
|
+
await writeJson(join(runDir, "state.json"), state);
|
|
1814
|
+
await writeJson(recordPath, record);
|
|
1815
|
+
await writeText(minutesPath, renderRecordMarkdown(record, loaded));
|
|
1816
|
+
return {
|
|
1817
|
+
status: record.status,
|
|
1818
|
+
recordPath,
|
|
1819
|
+
runDir,
|
|
1820
|
+
record,
|
|
1821
|
+
used: state.budget.used,
|
|
1822
|
+
budgetTotal: state.budget.total
|
|
1823
|
+
};
|
|
10
1824
|
}
|
|
11
|
-
async function
|
|
12
|
-
|
|
1825
|
+
async function readState(runDir) {
|
|
1826
|
+
const { readFile } = await import('node:fs/promises');
|
|
1827
|
+
try {
|
|
1828
|
+
const raw = await readFile(join(runDir, "state.json"), "utf8");
|
|
1829
|
+
return JSON.parse(raw);
|
|
1830
|
+
} catch {
|
|
1831
|
+
return null;
|
|
1832
|
+
}
|
|
13
1833
|
}
|
|
14
1834
|
|
|
15
|
-
export { ROUNDTABLE_SCHEMA, checkRoundtable, loadRoundtable, runRoundtable };
|
|
1835
|
+
export { RECORD_SCHEMA as ROUNDTABLE_SCHEMA, RoundtableConfigError, checkRoundtable, defaultOutDir, loadRoundtable, resumeRoundtable, runRoundtable };
|