multiarena 0.1.1 → 0.1.4

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.
@@ -0,0 +1,475 @@
1
+ import { runTurn } from "./turn.js";
2
+ import { ToolRegistry } from "../tools/registry.js";
3
+ import { PermissionManager } from "../tools/permission.js";
4
+ const ROLE_LABELS = {
5
+ draft: "起草",
6
+ revise: "修订",
7
+ polish: "润色",
8
+ review: "终审",
9
+ };
10
+ function buildSystemPrompt(role, task, isFinalRound, constraint, previousDocument, draftAuthor) {
11
+ const constraintBlock = constraint
12
+ ? `\n\n## 约束文档(必须遵守)\n${constraint}`
13
+ : "";
14
+ const header = "你正在参与一个多模型协作起草流程(R2D2:轮转审议起草)。";
15
+ switch (role) {
16
+ case "draft":
17
+ return `${header}你的角色是**起草者**。
18
+
19
+ ## 任务
20
+ ${task}
21
+ ${constraintBlock}
22
+
23
+ ## 要求
24
+ - 写出一份完整的初稿
25
+ - 严格对照约束文档,确保无违规
26
+ - 覆盖任务描述中的所有要点
27
+ - 后续其他模型会在你的基础上修改,请尽量全面
28
+ - 输出完整的文档内容`;
29
+ case "revise": {
30
+ const finalRoundBlock = isFinalRound
31
+ ? `\n## 重要:这是最后一轮
32
+ 你是最终输出者。请输出一份面向用户的干净终稿:
33
+ - 直接在你的修订版正文中完成所有修改,不要使用 [修订:] 标注
34
+ - 如果之前文档中有 [修订:] 或 [补充:] 标注,将它们全部清理掉,只保留修改后的干净正文
35
+ - 文档读起来应该像一篇自然的成品,没有任何过程标记`
36
+ : "";
37
+ return `${header}你的角色是**修订者**。
38
+ ${finalRoundBlock}
39
+ ## 原始任务
40
+ ${task}
41
+ ${constraintBlock}
42
+
43
+ ## ${draftAuthor ?? "起草者"} 的初稿
44
+ ${previousDocument}
45
+
46
+ ## 要求
47
+ - 在初稿基础上修订,不要推倒重写
48
+ - 对照约束文档,逐条检查违规项并修正
49
+ - 补充你发现遗漏的要点
50
+ - 改进表达不清或逻辑不严谨的地方
51
+ ${isFinalRound
52
+ ? "- 输出完整的干净终稿,不要包含任何过程标注"
53
+ : "- 在修改处用 [修订: 原文片段 → 修改后文本] 标注\n- 禁止笼统赞美,只做实质性修改\n- 输出完整的修订版文档"}`;
54
+ }
55
+ case "polish": {
56
+ const finalRoundBlock = isFinalRound
57
+ ? `\n## 重要:这是最后一轮
58
+ 你是最终输出者。请输出一份面向用户的干净终稿:
59
+ - 如果之前文档中有 [修订:] 或 [补充:] 标注,将它们全部清理掉,只保留修改后的干净正文
60
+ - 你新增的改进直接写入正文,不要使用 [补充:] 标注
61
+ - 文档读起来应该像一篇自然的成品,没有任何过程标记`
62
+ : "";
63
+ return `${header}你的角色是**润色者**。
64
+ ${finalRoundBlock}
65
+ ## 原始任务
66
+ ${task}
67
+ ${constraintBlock}
68
+
69
+ ## 经过修订的文档
70
+ ${previousDocument}
71
+
72
+ ## 要求
73
+ - 在现有基础上最终润色
74
+ - 再次对照约束文档做合规检查
75
+ - 优化语言流畅度和可读性
76
+ ${isFinalRound
77
+ ? "- 输出完整的干净终稿,不要包含任何过程标注"
78
+ : "- 补充你独有的见解(标注 [补充: 你的贡献])\n- 保留所有之前的修订标注\n- 输出完整的文档"}`;
79
+ }
80
+ case "review":
81
+ return `${header}你的角色是**终审者**(最后一轮)。
82
+
83
+ ## 原始任务
84
+ ${task}
85
+ ${constraintBlock}
86
+
87
+ ## 经过多轮修改的文档
88
+ ${previousDocument}
89
+
90
+ ## 要求
91
+ - 审查是否有修改偏离了原意
92
+ - 对照约束文档逐条再过一遍
93
+ - 清理所有 [修订:] 和 [补充:] 等过程标注,输出干净的最终版
94
+ - 如果认可某处修改,直接保留正文;如果需要回退,直接改回并保持正文流畅
95
+ - 这是一份面向用户的交付文档,不要包含任何过程标记或审查意见
96
+ - 输出最终确认版`;
97
+ default:
98
+ return "";
99
+ }
100
+ }
101
+ /**
102
+ * Build a private "think" prompt for the given role.
103
+ * The model analyses the current state before acting — this output is NOT
104
+ * shared with other models, but is fed back into the same model's main
105
+ * round system prompt so its public output is informed by private reasoning.
106
+ */
107
+ function buildThinkPrompt(role, task, isFirstRound, constraint, previousDocument) {
108
+ const docBlock = previousDocument
109
+ ? `\n\n## 当前文档(请仔细分析)\n${previousDocument}`
110
+ : "";
111
+ const taskBlock = `\n\n## 用户任务/反馈\n${task}`;
112
+ const constraintBlock = constraint
113
+ ? `\n\n## 约束文档\n${constraint}`
114
+ : "";
115
+ switch (role) {
116
+ case "draft":
117
+ return `你即将以**起草者**的身份撰写一份初稿。在此之前,请先进行私有分析:
118
+ ${taskBlock}${constraintBlock}
119
+
120
+ 请简短回答以下问题(用你自己的话,不要长篇大论):
121
+ 1. 任务的核心目标是什么?需要覆盖哪些关键要点?
122
+ 2. 文档应该是什么结构?(章节/段落规划)
123
+ 3. 有什么需要特别注意的约束或陷阱?
124
+ 4. 有什么地方信息不足,需要合理假设的?`;
125
+ case "revise":
126
+ return `你即将以**修订者**的身份修改一份文档。在此之前,请先进行私有分析:
127
+ ${taskBlock}${constraintBlock}${docBlock}
128
+
129
+ 请简短回答以下问题:
130
+ 1. 这份文档的优点是什么?哪些部分写得不错?
131
+ 2. 存在哪些问题?(偏离任务、遗漏要点、逻辑不严谨、表达不清、潜在的事实错误或幻觉)
132
+ 3. 对照约束文档,哪些地方违反了约束?
133
+ 4. 你计划做哪些具体修改?按优先级列出。${isFirstRound ? "\n注意:这是第一轮修订,你看到的是初稿。重点关注初稿是否忠实地回应了用户的任务。" : ""}`;
134
+ case "polish":
135
+ return `你即将以**润色者**的身份最终润色一份文档。在此之前,请先进行私有分析:
136
+ ${taskBlock}${constraintBlock}${docBlock}
137
+
138
+ 请简短回答以下问题:
139
+ 1. 文档的整体语言质量如何?(流畅度、可读性、语气一致性)
140
+ 2. 有哪些表达可以更优雅或更精准?
141
+ 3. 对照约束文档,还有什么需要修正的?
142
+ 4. 你独有的补充见解是什么?(如果有的话)`;
143
+ case "review":
144
+ return `你即将以**终审者**的身份做最终审查。在此之前,请先进行私有分析:
145
+ ${taskBlock}${constraintBlock}${docBlock}
146
+
147
+ 请简短回答以下问题:
148
+ 1. 经过多轮修改后,文档是否偏离了用户的原始意图?
149
+ 2. 逐条对照约束文档检查——还有违规项吗?
150
+ 3. 有没有任何模型引入了事实错误或幻觉?
151
+ 4. 最终交付前,还有什么必须清理或修复的?`;
152
+ default:
153
+ return "";
154
+ }
155
+ }
156
+ /**
157
+ * Run the R2D2 (Round-Robin Deliberative Drafting) pipeline.
158
+ *
159
+ * Models take turns in sequence: the first drafts, the second revises,
160
+ * the third polishes, and optionally a fourth reviews. Each round's
161
+ * system prompt injects the task, constraint document, and previous
162
+ * round output for context. No central synthesizer — the document
163
+ * emerges through sequential refinement.
164
+ */
165
+ export async function* runDeliberation(sharedMessages, roundConfigs, constraint, worktreePath) {
166
+ const documents = [];
167
+ const totalRounds = roundConfigs.length;
168
+ // Derive the task from the last user message in the shared context.
169
+ const lastUser = [...sharedMessages].reverse().find((m) => m.role === "user");
170
+ const task = lastUser?.content ?? "";
171
+ for (let i = 0; i < roundConfigs.length; i++) {
172
+ const rc = roundConfigs[i];
173
+ const previousDocument = i > 0 ? documents[i - 1] : undefined;
174
+ const draftAuthor = i > 0 ? roundConfigs[0].modelName : undefined;
175
+ const isFinalRound = i === roundConfigs.length - 1;
176
+ yield {
177
+ type: "round_start",
178
+ round: i + 1,
179
+ totalRounds,
180
+ modelName: rc.modelName,
181
+ role: rc.role,
182
+ };
183
+ // ── Private Think Phase ──────────────────────────────────────
184
+ // The model analyses the current state privately before acting.
185
+ // This output is NOT shared with other models — it only informs
186
+ // this model's own main round via the system prompt.
187
+ let thinkOutput = "";
188
+ const thinkPrompt = buildThinkPrompt(rc.role, task, i === 0, constraint, previousDocument);
189
+ if (thinkPrompt) {
190
+ yield {
191
+ type: "think_start",
192
+ round: i + 1,
193
+ totalRounds,
194
+ modelName: rc.modelName,
195
+ role: rc.role,
196
+ };
197
+ try {
198
+ const thinkStream = runTurn({
199
+ modelName: rc.modelName,
200
+ config: rc.config,
201
+ messages: [{ role: "user", content: "请按上述要求进行分析。用简洁的语言回答,不要长篇大论。" }],
202
+ systemPrompt: thinkPrompt,
203
+ tools: [],
204
+ registry: new ToolRegistry(),
205
+ permission: new PermissionManager(),
206
+ worktreePath: worktreePath ?? process.cwd(),
207
+ });
208
+ for await (const event of thinkStream) {
209
+ if (event.type === "text") {
210
+ thinkOutput += event.content;
211
+ yield {
212
+ type: "think_text",
213
+ round: i + 1,
214
+ totalRounds,
215
+ modelName: rc.modelName,
216
+ role: rc.role,
217
+ content: event.content,
218
+ };
219
+ }
220
+ else if (event.type === "error") {
221
+ // Think failure is non-fatal — proceed without think context
222
+ thinkOutput = "";
223
+ break;
224
+ }
225
+ }
226
+ }
227
+ catch {
228
+ thinkOutput = "";
229
+ }
230
+ yield {
231
+ type: "think_end",
232
+ round: i + 1,
233
+ totalRounds,
234
+ modelName: rc.modelName,
235
+ role: rc.role,
236
+ };
237
+ }
238
+ // ── Main Round System Prompt (informed by private think) ──────
239
+ const thinkBlock = thinkOutput
240
+ ? `\n\n## 你的私有分析结果\n以下是你刚才对当前状态的分析。请基于这些洞察来完成你的任务:\n\n${thinkOutput}`
241
+ : "";
242
+ const systemPrompt = buildSystemPrompt(rc.role, task, isFinalRound, constraint, previousDocument, draftAuthor) + thinkBlock;
243
+ // Build messages from the shared context plus this round's role instruction.
244
+ const instruction = {
245
+ role: "user",
246
+ content: rc.role === "draft"
247
+ ? `请起草以下文档:\n\n${task}`
248
+ : "请根据你的角色要求和上述文档内容,输出修改后的完整文档。不要输出任何前言或后记,直接输出文档内容。",
249
+ };
250
+ const messages = [...sharedMessages, instruction];
251
+ let buffer = "";
252
+ try {
253
+ const stream = runTurn({
254
+ modelName: rc.modelName,
255
+ config: rc.config,
256
+ messages,
257
+ systemPrompt,
258
+ tools: [],
259
+ registry: new ToolRegistry(),
260
+ permission: new PermissionManager(),
261
+ worktreePath: worktreePath ?? process.cwd(),
262
+ });
263
+ for await (const event of stream) {
264
+ if (event.type === "text") {
265
+ buffer += event.content;
266
+ yield {
267
+ type: "text",
268
+ round: i + 1,
269
+ totalRounds,
270
+ modelName: rc.modelName,
271
+ role: rc.role,
272
+ content: event.content,
273
+ };
274
+ }
275
+ else if (event.type === "error") {
276
+ buffer += `\n[错误: ${event.message}]`;
277
+ yield {
278
+ type: "text",
279
+ round: i + 1,
280
+ totalRounds,
281
+ modelName: rc.modelName,
282
+ role: rc.role,
283
+ content: `\n[错误: ${event.message}]`,
284
+ };
285
+ break;
286
+ }
287
+ }
288
+ }
289
+ catch (err) {
290
+ yield {
291
+ type: "error",
292
+ round: i + 1,
293
+ totalRounds,
294
+ modelName: rc.modelName,
295
+ role: rc.role,
296
+ error: err.message,
297
+ };
298
+ return;
299
+ }
300
+ documents.push(buffer);
301
+ sharedMessages.push({ role: "assistant", content: buffer });
302
+ // Extract revision annotations for the process summary
303
+ const revisionMatches = buffer.match(/\[修订:\s*([^\]]+?)\]/g) ?? [];
304
+ const changeSamples = revisionMatches
305
+ .map((m) => m.replace(/^\[修订:\s*/, "").replace(/\]$/, ""))
306
+ .slice(0, 5);
307
+ yield {
308
+ type: "round_end",
309
+ round: i + 1,
310
+ totalRounds,
311
+ modelName: rc.modelName,
312
+ role: rc.role,
313
+ document: buffer,
314
+ changeCount: revisionMatches.length,
315
+ changeSamples: changeSamples.length > 0 ? changeSamples : undefined,
316
+ };
317
+ // Yield to the event loop so React renders the round's output
318
+ // before the next round_start event arrives, preventing batch
319
+ // collapse between round_end and round_start.
320
+ if (i < roundConfigs.length - 1) {
321
+ await new Promise((resolve) => setTimeout(resolve, 0));
322
+ }
323
+ }
324
+ yield {
325
+ type: "done",
326
+ round: totalRounds,
327
+ totalRounds,
328
+ document: documents[documents.length - 1],
329
+ };
330
+ }
331
+ /** Build round configs with mirror pattern: A→B→C→B→A. */
332
+ export function autoAssignRounds(modelNames, models) {
333
+ const active = modelNames.filter((n) => models[n]);
334
+ if (active.length < 2)
335
+ return [];
336
+ const forwardRoles = ["draft", "revise", "polish"];
337
+ if (active.length >= 4)
338
+ forwardRoles.push("review");
339
+ // Forward pass: assign roles to first N models
340
+ const result = [];
341
+ const fwdCount = Math.min(active.length, forwardRoles.length);
342
+ for (let i = 0; i < fwdCount; i++) {
343
+ result.push({
344
+ modelName: active[i],
345
+ role: forwardRoles[i],
346
+ config: models[active[i]],
347
+ });
348
+ }
349
+ // Reverse pass: middle models revise again, first model reviews
350
+ if (active.length >= 2) {
351
+ // Middle models in reverse (skip first and last of forward pass)
352
+ for (let i = fwdCount - 2; i >= 1; i--) {
353
+ result.push({
354
+ modelName: active[i],
355
+ role: "revise",
356
+ config: models[active[i]],
357
+ });
358
+ }
359
+ // First model does final review
360
+ result.push({
361
+ modelName: active[0],
362
+ role: "review",
363
+ config: models[active[0]],
364
+ });
365
+ }
366
+ return result;
367
+ }
368
+ /** Human-readable label for a round role. */
369
+ export function roundLabel(role) {
370
+ return ROLE_LABELS[role];
371
+ }
372
+ /**
373
+ * Synthesize existing model outputs into one final document.
374
+ * Single-turn: one model acts as the merger, combining all outputs
375
+ * into a coherent document with source annotations and conflict notes.
376
+ */
377
+ export async function* runMerge(task, outputs, mergerConfig, mergerName) {
378
+ const outputBlock = outputs
379
+ .map((o) => `### ${o.modelName}\n\n${o.content}`)
380
+ .join("\n\n---\n\n");
381
+ const systemPrompt = `你正在执行多模型输出的合并合成任务。
382
+
383
+ ## 原始任务
384
+ ${task}
385
+
386
+ ## 各模型的输出
387
+ ${outputBlock}
388
+
389
+ ## 合并要求
390
+
391
+ 1. **识别共识** — 找出所有模型中一致或高度相似的论点、事实、建议,合并后作为 [共识] 部分
392
+ 2. **保留独有贡献** — 每个模型独有的观点、细节、角度,标注 [来源: 模型名]
393
+ 3. **标注分歧** — 如果模型之间对某个问题有不同意见,客观列出各方观点,标注 [分歧]
394
+ 4. **去重合并** — 相似内容合并而不是重复
395
+ 5. **保持完整** — 不要遗漏任何模型的任何实质内容
396
+ 6. **语言流畅** — 最终输出应读起来像一篇连贯的文档,而不是拼凑
397
+
398
+ ## 输出格式
399
+ 直接输出合并后的完整文档。每个段落/章节末尾用 [] 标注来源。
400
+ 禁止输出前言或后记。`;
401
+ yield {
402
+ type: "round_start",
403
+ round: 1,
404
+ totalRounds: 1,
405
+ modelName: mergerName,
406
+ role: "draft",
407
+ };
408
+ const messages = [
409
+ { role: "user", content: "请根据各模型的输出,合并生成一份最终文档。" },
410
+ ];
411
+ let buffer = "";
412
+ try {
413
+ const stream = runTurn({
414
+ modelName: mergerName,
415
+ config: mergerConfig,
416
+ messages,
417
+ systemPrompt,
418
+ tools: [],
419
+ registry: new ToolRegistry(),
420
+ permission: new PermissionManager(),
421
+ worktreePath: process.cwd(),
422
+ });
423
+ for await (const event of stream) {
424
+ if (event.type === "text") {
425
+ buffer += event.content;
426
+ yield {
427
+ type: "text",
428
+ round: 1,
429
+ totalRounds: 1,
430
+ modelName: mergerName,
431
+ role: "draft",
432
+ content: event.content,
433
+ };
434
+ }
435
+ else if (event.type === "error") {
436
+ buffer += `\n[错误: ${event.message}]`;
437
+ yield {
438
+ type: "text",
439
+ round: 1,
440
+ totalRounds: 1,
441
+ modelName: mergerName,
442
+ role: "draft",
443
+ content: `\n[错误: ${event.message}]`,
444
+ };
445
+ break;
446
+ }
447
+ }
448
+ }
449
+ catch (err) {
450
+ yield {
451
+ type: "error",
452
+ round: 1,
453
+ totalRounds: 1,
454
+ modelName: mergerName,
455
+ error: err.message,
456
+ };
457
+ return;
458
+ }
459
+ yield {
460
+ type: "round_end",
461
+ round: 1,
462
+ totalRounds: 1,
463
+ modelName: mergerName,
464
+ role: "draft",
465
+ document: buffer,
466
+ };
467
+ // Yield to the event loop so the UI renders before "done"
468
+ await new Promise((resolve) => setTimeout(resolve, 0));
469
+ yield {
470
+ type: "done",
471
+ round: 1,
472
+ totalRounds: 1,
473
+ document: buffer,
474
+ };
475
+ }
@@ -16,12 +16,14 @@ export interface SessionSnapshot {
16
16
  }>;
17
17
  targetMode: TargetMode;
18
18
  worktreeBase: string;
19
+ teamMessages?: Message[];
19
20
  }
20
21
  export declare class Session {
21
22
  private state;
22
23
  constructor(config: ArenaConfig, worktreeBase: string, snapshot?: SessionSnapshot);
23
24
  get models(): ModelState[];
24
25
  get targetMode(): TargetMode;
26
+ get teamMessages(): Message[];
25
27
  /** Add a user message to the target model(s). Returns affected models. */
26
28
  addUserMessage(content: string): ModelState[];
27
29
  /** Append assistant response to a model's history */
@@ -29,7 +31,7 @@ export declare class Session {
29
31
  /** Append tool result to a model's history */
30
32
  addToolResult(modelName: string, toolCallId: string, result: string): void;
31
33
  setTarget(target: TargetMode): void;
32
- /** Cycle Tab through targets: broadcast → model1 → model2 → ... → broadcast */
34
+ /** Cycle Tab through unmuted targets: broadcast → model1 → model2 → ... → broadcast */
33
35
  cycleTarget(): TargetMode;
34
36
  jumpToModel(modelName: string): void;
35
37
  jumpToBroadcast(): void;
@@ -10,6 +10,7 @@ export class Session {
10
10
  })),
11
11
  targetMode: snapshot.targetMode,
12
12
  worktreeBase: snapshot.worktreeBase,
13
+ teamMessages: snapshot.teamMessages ?? [],
13
14
  };
14
15
  }
15
16
  else {
@@ -30,6 +31,7 @@ export class Session {
30
31
  models,
31
32
  targetMode: { type: "broadcast" },
32
33
  worktreeBase,
34
+ teamMessages: [],
33
35
  };
34
36
  }
35
37
  }
@@ -39,6 +41,9 @@ export class Session {
39
41
  get targetMode() {
40
42
  return this.state.targetMode;
41
43
  }
44
+ get teamMessages() {
45
+ return this.state.teamMessages;
46
+ }
42
47
  /** Add a user message to the target model(s). Returns affected models. */
43
48
  addUserMessage(content) {
44
49
  if (this.state.targetMode.type === "broadcast") {
@@ -74,18 +79,19 @@ export class Session {
74
79
  setTarget(target) {
75
80
  this.state.targetMode = target;
76
81
  }
77
- /** Cycle Tab through targets: broadcast → model1 → model2 → ... → broadcast */
82
+ /** Cycle Tab through unmuted targets: broadcast → model1 → model2 → ... → broadcast */
78
83
  cycleTarget() {
79
84
  const current = this.state.targetMode;
85
+ const unmuted = this.state.models.filter((m) => !m.muted);
80
86
  if (current.type === "broadcast") {
81
- const first = this.state.models[0];
87
+ const first = unmuted[0];
82
88
  this.state.targetMode = first
83
89
  ? { type: "directed", modelName: first.name }
84
90
  : current;
85
91
  }
86
92
  else {
87
- const idx = this.state.models.findIndex((m) => m.name === current.modelName);
88
- const next = this.state.models[idx + 1];
93
+ const idx = unmuted.findIndex((m) => m.name === current.modelName);
94
+ const next = unmuted[idx + 1];
89
95
  this.state.targetMode = next
90
96
  ? { type: "directed", modelName: next.name }
91
97
  : { type: "broadcast" };
@@ -135,6 +141,7 @@ export class Session {
135
141
  })),
136
142
  targetMode: this.state.targetMode,
137
143
  worktreeBase: this.state.worktreeBase,
144
+ teamMessages: [...this.state.teamMessages],
138
145
  };
139
146
  }
140
147
  findModel(name) {
package/dist/index.js CHANGED
@@ -1,62 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
  import React from "react";
3
3
  import { render } from "ink";
4
- import * as fs from "node:fs";
5
- import * as path from "node:path";
6
- import { fileURLToPath } from "node:url";
7
4
  import { App } from "./ui/app.js";
8
5
  import { listSessions } from "./persistence/session.js";
9
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
- const PKG_VERSION = (() => {
11
- try {
12
- const pkgPath = path.join(__dirname, "..", "package.json");
13
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
14
- return pkg.version ?? "0.1.0";
15
- }
16
- catch {
17
- return "0.1.0";
18
- }
19
- })();
20
- const HELP = `multiarena — Multi-Model AI Coding Assistant
21
-
22
- Usage:
23
- multiarena [options]
24
-
25
- Options:
26
- --new Start a new session (default)
27
- --resume <id> Resume a saved session
28
- --list List saved sessions
29
- --help Show this help
30
- --version Show version`;
31
- function parseArgs() {
32
- const args = process.argv.slice(2);
33
- if (args.includes("--help") || args.includes("-h")) {
34
- return { listOnly: false, showHelp: true, showVersion: false };
35
- }
36
- if (args.includes("--version") || args.includes("-v")) {
37
- return { listOnly: false, showHelp: false, showVersion: true };
38
- }
39
- const resumeIdx = args.indexOf("--resume");
40
- if (resumeIdx >= 0 && args[resumeIdx + 1]) {
41
- return {
42
- sessionId: args[resumeIdx + 1],
43
- listOnly: false,
44
- showHelp: false,
45
- showVersion: false,
46
- };
47
- }
48
- if (args.includes("--list") || args.includes("--list-sessions")) {
49
- return { listOnly: true, showHelp: false, showVersion: false };
50
- }
51
- return { listOnly: false, showHelp: false, showVersion: false };
52
- }
53
- const { sessionId, listOnly, showHelp, showVersion } = parseArgs();
6
+ import { parseArgs, getPkgVersion, HELP } from "./cli/args.js";
7
+ const { sessionId, listOnly, showHelp, showVersion } = parseArgs(process.argv.slice(2));
54
8
  if (showHelp) {
55
9
  console.log(HELP);
56
10
  process.exit(0);
57
11
  }
58
12
  if (showVersion) {
59
- console.log(`multiarena v${PKG_VERSION}`);
13
+ console.log(`multiarena v${getPkgVersion()}`);
60
14
  process.exit(0);
61
15
  }
62
16
  if (listOnly) {
@@ -1,5 +1,20 @@
1
1
  import { Provider } from "../provider.js";
2
2
  import { ChatRequest, StreamEvent } from "../types.js";
3
+ export interface ThinkFilterState {
4
+ inThink: boolean;
5
+ buf: string;
6
+ }
7
+ /**
8
+ * Filter think blocks from reasoning model output (DeepSeek-R1, MiniMax).
9
+ * Returns the text that should be yielded to the user, and updates state.
10
+ * Callers yield the returned text and replace their state with the returned state.
11
+ */
12
+ export declare function filterThinkText(deltaText: string, state: ThinkFilterState): {
13
+ text: string;
14
+ state: ThinkFilterState;
15
+ };
16
+ /** Called when the stream ends (finish_reason = stop). Flush any buffered think content. */
17
+ export declare function flushThinkBuf(state: ThinkFilterState): string;
3
18
  export declare class OpenAIProvider implements Provider {
4
19
  private client;
5
20
  private activeController;