palabre 0.7.0 → 0.8.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.
@@ -1,6 +1,7 @@
1
1
  import { createAgent } from "./adapters/index.js";
2
2
  import { AdapterError } from "./errors.js";
3
3
  import { createTranslator } from "./i18n.js";
4
+ export const MAX_ASK_AGENTS = 4;
4
5
  /**
5
6
  * Point d'entrée de l'orchestration.
6
7
  * Lance le ping-pong entre `agentA` et `agentB` pendant `options.turns` tours,
@@ -104,7 +105,8 @@ export async function runDebate(config, options, renderer, messages = createTran
104
105
  try {
105
106
  const cancellation = cancellationFailureIfAborted(options, messages, {
106
107
  phase: "summary",
107
- agent: options.summaryAgent ?? options.agentB,
108
+ agent: resolveSummaryAgentName(options),
109
+ role: summaryRole(),
108
110
  turn: transcript.length + 1
109
111
  });
110
112
  if (cancellation) {
@@ -121,7 +123,8 @@ export async function runDebate(config, options, renderer, messages = createTran
121
123
  catch (error) {
122
124
  failure = toDebateFailure(error, {
123
125
  phase: "summary",
124
- agent: options.summaryAgent ?? options.agentB,
126
+ agent: resolveSummaryAgentName(options),
127
+ role: summaryRole(),
125
128
  turn: transcript.length + 1
126
129
  });
127
130
  renderer?.error(failure);
@@ -135,6 +138,143 @@ export async function runDebate(config, options, renderer, messages = createTran
135
138
  failure
136
139
  };
137
140
  }
141
+ /**
142
+ * Lance le mode ask : plusieurs agents répondent indépendamment au même sujet,
143
+ * puis un agent de synthèse résume fidèlement chaque réponse et les compare.
144
+ */
145
+ export async function runAsk(config, options, renderer, messages = createTranslator("fr")) {
146
+ const askAgentNames = resolveAskAgentNames(options);
147
+ if (askAgentNames.length === 0) {
148
+ throw new Error(messages.common.noAgentDefined("ask agent"));
149
+ }
150
+ if (askAgentNames.length > MAX_ASK_AGENTS) {
151
+ throw new Error(messages.common.tooManyAskAgents(MAX_ASK_AGENTS));
152
+ }
153
+ const agentEntries = askAgentNames.map((name) => {
154
+ const agentConfig = withRuntimeOverrides(config.agents[name], modelForAgent(options, name), options.pullModels);
155
+ if (!agentConfig) {
156
+ throw new Error(messages.common.unknownAgent(name));
157
+ }
158
+ return [name, agentConfig];
159
+ });
160
+ warnIfOllamaHasNoContext(options, agentEntries.map(([name, agentConfig]) => [name, agentConfig]), renderer, messages);
161
+ renderer?.start(options, agentEntries.map(([name, agentConfig]) => ({
162
+ name,
163
+ role: agentConfig.role,
164
+ type: agentConfig.type
165
+ })));
166
+ const agents = agentEntries.map(([name, agentConfig]) => createAgent(name, agentConfig));
167
+ const transcript = [];
168
+ for (let index = 0; index < agents.length; index += 1) {
169
+ const current = agents[index];
170
+ const response = index + 1;
171
+ const cancellation = cancellationFailureIfAborted(options, messages, {
172
+ phase: "ask",
173
+ agent: current.name,
174
+ role: current.role,
175
+ turn: response
176
+ });
177
+ if (cancellation) {
178
+ renderer?.error(cancellation);
179
+ return {
180
+ options,
181
+ messages: transcript,
182
+ failure: cancellation
183
+ };
184
+ }
185
+ if (renderer?.askResponseStart) {
186
+ renderer.askResponseStart(response, agents.length, current.name, current.role);
187
+ }
188
+ else {
189
+ renderer?.turnStart(response, agents.length, current.name, current.role);
190
+ }
191
+ renderer?.thinkingStart(current.name, current.role);
192
+ let agentResponse;
193
+ try {
194
+ agentResponse = await current.generate({
195
+ topic: options.topic,
196
+ turn: response,
197
+ totalTurns: agents.length,
198
+ selfName: current.name,
199
+ peerName: "independent-agents",
200
+ selfRole: current.role,
201
+ mode: "ask",
202
+ language: options.language,
203
+ session: options.session,
204
+ files: options.files,
205
+ transcript: [],
206
+ signal: options.signal
207
+ });
208
+ }
209
+ catch (error) {
210
+ const failure = toDebateFailure(error, {
211
+ phase: "ask",
212
+ agent: current.name,
213
+ role: current.role,
214
+ turn: response
215
+ });
216
+ renderer?.error(failure);
217
+ return {
218
+ options,
219
+ messages: transcript,
220
+ failure
221
+ };
222
+ }
223
+ finally {
224
+ renderer?.thinkingEnd();
225
+ }
226
+ const message = {
227
+ agent: current.name,
228
+ role: current.role,
229
+ content: agentResponse.content,
230
+ createdAt: new Date().toISOString()
231
+ };
232
+ transcript.push(message);
233
+ if (renderer?.askResponseMessage) {
234
+ renderer.askResponseMessage(message.content);
235
+ }
236
+ else {
237
+ renderer?.message(message.content);
238
+ }
239
+ }
240
+ let summary;
241
+ let failure;
242
+ if (options.summaryEnabled) {
243
+ try {
244
+ const summaryAgentName = resolveSummaryAgentName(options);
245
+ const cancellation = cancellationFailureIfAborted(options, messages, {
246
+ phase: "summary",
247
+ agent: summaryAgentName,
248
+ role: summaryRole(),
249
+ turn: transcript.length + 1
250
+ });
251
+ if (cancellation) {
252
+ renderer?.error(cancellation);
253
+ return {
254
+ options,
255
+ messages: transcript,
256
+ failure: cancellation
257
+ };
258
+ }
259
+ summary = await generateSummary(config, options, transcript, renderer, messages);
260
+ }
261
+ catch (error) {
262
+ failure = toDebateFailure(error, {
263
+ phase: "summary",
264
+ agent: resolveSummaryAgentName(options),
265
+ role: summaryRole(),
266
+ turn: transcript.length + 1
267
+ });
268
+ renderer?.error(failure);
269
+ }
270
+ }
271
+ return {
272
+ options,
273
+ messages: transcript,
274
+ summary,
275
+ failure
276
+ };
277
+ }
138
278
  /**
139
279
  * Heuristique d'arrêt sur accord explicite.
140
280
  * Ne s'active qu'après un tour complet (nombre pair de messages) pour éviter les faux positifs.
@@ -189,22 +329,23 @@ function warnIfOllamaHasNoContext(options, agents, renderer, messages = createTr
189
329
  * @throws {Error} si l'agent de synthèse est absent de `config.agents`.
190
330
  */
191
331
  async function generateSummary(config, options, transcript, renderer, messages = createTranslator("fr")) {
192
- const summaryAgentName = options.summaryAgent ?? options.agentB;
332
+ const summaryAgentName = resolveSummaryAgentName(options);
193
333
  const summaryModel = options.summaryModel ?? modelForAgent(options, summaryAgentName);
194
334
  const summaryConfig = withRuntimeOverrides(config.agents[summaryAgentName], summaryModel, options.pullModels);
195
335
  if (!summaryConfig) {
196
336
  throw new Error(messages.orchestrator.unknownSummaryAgent(summaryAgentName));
197
337
  }
198
338
  const summaryAgent = createAgent(summaryAgentName, summaryConfig);
199
- renderer?.summaryStart(summaryAgent.name, summaryAgent.role);
200
- renderer?.thinkingStart(summaryAgent.name, summaryAgent.role);
339
+ const role = summaryRole();
340
+ renderer?.summaryStart(summaryAgent.name, role);
341
+ renderer?.thinkingStart(summaryAgent.name, role);
201
342
  const response = await summaryAgent.generate({
202
343
  topic: options.topic,
203
344
  turn: transcript.length + 1,
204
- totalTurns: options.turns,
345
+ totalTurns: options.mode === "ask" ? transcript.length : options.turns,
205
346
  selfName: summaryAgent.name,
206
- peerName: "transcript",
207
- selfRole: summaryAgent.role,
347
+ peerName: options.mode === "ask" ? "ask-responses" : "transcript",
348
+ selfRole: role,
208
349
  mode: "summary",
209
350
  language: options.language,
210
351
  session: options.session,
@@ -214,13 +355,16 @@ async function generateSummary(config, options, transcript, renderer, messages =
214
355
  }).finally(() => renderer?.thinkingEnd());
215
356
  const summary = {
216
357
  agent: summaryAgent.name,
217
- role: summaryAgent.role,
358
+ role,
218
359
  content: response.content,
219
360
  createdAt: new Date().toISOString()
220
361
  };
221
362
  renderer?.message(summary.content);
222
363
  return summary;
223
364
  }
365
+ function summaryRole() {
366
+ return "summarizer";
367
+ }
224
368
  function cancellationFailureIfAborted(options, messages, context) {
225
369
  if (!options.signal?.aborted) {
226
370
  return undefined;
@@ -234,6 +378,21 @@ function cancellationFailureIfAborted(options, messages, context) {
234
378
  message: messages.orchestrator.cancelled
235
379
  };
236
380
  }
381
+ function resolveAskAgentNames(options) {
382
+ const agents = options.askAgents && options.askAgents.length > 0
383
+ ? options.askAgents
384
+ : [options.agentA, options.agentB];
385
+ return agents.filter((agent, index) => Boolean(agent) && agents.indexOf(agent) === index);
386
+ }
387
+ function resolveSummaryAgentName(options) {
388
+ if (options.summaryAgent) {
389
+ return options.summaryAgent;
390
+ }
391
+ if (options.mode === "ask" && options.askAgents && options.askAgents.length > 0) {
392
+ return options.askAgents[options.askAgents.length - 1] ?? options.agentB;
393
+ }
394
+ return options.agentB;
395
+ }
237
396
  function toDebateFailure(error, context) {
238
397
  if (error instanceof AdapterError) {
239
398
  return {
package/dist/output.js CHANGED
@@ -7,7 +7,8 @@ import { createTranslator } from "./i18n.js";
7
7
  */
8
8
  export async function writeDebateMarkdown(outputDir, options, debateMessages, summary, stopReason, messages = createTranslator("fr"), failure) {
9
9
  const safeDate = new Date().toISOString().replace(/[:.]/g, "-");
10
- const fileName = `palabre-${slugifyTopic(options.topic)}-${safeDate}.debate.md`;
10
+ const extension = options.mode === "ask" ? "ask" : "debate";
11
+ const fileName = `palabre-${slugifyTopic(options.topic)}-${safeDate}.${extension}.md`;
11
12
  const filePath = path.resolve(outputDir, fileName);
12
13
  await mkdir(path.dirname(filePath), { recursive: true });
13
14
  await writeFile(filePath, renderDebateMarkdown(options, debateMessages, summary, stopReason, messages, failure), "utf8");
@@ -30,7 +31,7 @@ function slugifyTopic(topic) {
30
31
  */
31
32
  export function renderDebateMarkdown(options, debateMessages, summary, stopReason, messages = createTranslator("fr"), failure) {
32
33
  const lines = [
33
- messages.output.title,
34
+ options.mode === "ask" ? messages.output.askTitle : messages.output.title,
34
35
  "",
35
36
  ...renderSessionHeader(options, debateMessages, stopReason, messages),
36
37
  "",
@@ -38,7 +39,7 @@ export function renderDebateMarkdown(options, debateMessages, summary, stopReaso
38
39
  "",
39
40
  ...renderFileList(options.files, messages),
40
41
  "",
41
- messages.output.exchangesTitle,
42
+ options.mode === "ask" ? messages.output.askResponsesTitle : messages.output.exchangesTitle,
42
43
  ""
43
44
  ];
44
45
  for (const message of debateMessages) {
@@ -90,12 +91,19 @@ function normalizeMarkdownForWindowsPreview(content) {
90
91
  function renderSessionHeader(options, debateMessages, stopReason, messages) {
91
92
  const rows = [
92
93
  [messages.output.fields.subject, options.topic],
93
- [messages.output.fields.agents, `${options.agentA} <-> ${options.agentB}`],
94
+ [messages.output.fields.mode, options.mode],
95
+ [messages.output.fields.agents, formatAgentsForHeader(options)],
94
96
  [messages.output.fields.autoPullOllama, options.pullModels ? messages.output.yes : messages.output.no],
95
- [messages.output.fields.summary, options.summaryEnabled ? options.summaryAgent ?? options.agentB : messages.output.disabled],
96
- [messages.output.fields.requestedTurns, String(options.turns)],
97
- [messages.output.fields.playedTurns, String(debateMessages.length)],
98
- [messages.output.fields.earlyStop, stopReason ?? messages.output.no],
97
+ [messages.output.fields.summary, options.summaryEnabled ? formatSummaryAgent(options) : messages.output.disabled],
98
+ [
99
+ options.mode === "ask" ? messages.output.fields.requestedResponses : messages.output.fields.requestedTurns,
100
+ String(options.mode === "ask" ? options.askAgents?.length ?? debateMessages.length : options.turns)
101
+ ],
102
+ [
103
+ options.mode === "ask" ? messages.output.fields.receivedResponses : messages.output.fields.playedTurns,
104
+ String(debateMessages.length)
105
+ ],
106
+ [messages.output.fields.earlyStop, options.mode === "debate" ? stopReason ?? messages.output.no : messages.output.no],
99
107
  [messages.output.fields.localDate, options.session.localDate],
100
108
  [messages.output.fields.timeZone, options.session.timeZone],
101
109
  [messages.output.fields.cwd, options.session.cwd],
@@ -116,3 +124,18 @@ function renderFileList(files, messages) {
116
124
  }
117
125
  return files.map((file) => `- \`${file.path}\` (${file.sizeBytes} ${messages.output.fileSizeUnit})`);
118
126
  }
127
+ function formatSummaryAgent(options) {
128
+ if (options.summaryAgent) {
129
+ return options.summaryAgent;
130
+ }
131
+ if (options.mode === "ask" && options.askAgents && options.askAgents.length > 0) {
132
+ return options.askAgents[options.askAgents.length - 1] ?? options.agentB;
133
+ }
134
+ return options.agentB;
135
+ }
136
+ function formatAgentsForHeader(options) {
137
+ if (options.mode === "ask") {
138
+ return (options.askAgents && options.askAgents.length > 0 ? options.askAgents : [options.agentA, options.agentB]).join(", ");
139
+ }
140
+ return `${options.agentA} <-> ${options.agentB}`;
141
+ }
package/dist/presets.js CHANGED
@@ -16,6 +16,14 @@ const presets = {
16
16
  agentA: "opencode",
17
17
  agentB: "codex"
18
18
  },
19
+ "codex-vibe": {
20
+ agentA: "codex",
21
+ agentB: "vibe"
22
+ },
23
+ "vibe-codex": {
24
+ agentA: "vibe",
25
+ agentB: "codex"
26
+ },
19
27
  "codex-antigravity": {
20
28
  agentA: "codex",
21
29
  agentB: "antigravity"
@@ -32,6 +40,14 @@ const presets = {
32
40
  agentA: "opencode",
33
41
  agentB: "claude"
34
42
  },
43
+ "claude-vibe": {
44
+ agentA: "claude",
45
+ agentB: "vibe"
46
+ },
47
+ "vibe-claude": {
48
+ agentA: "vibe",
49
+ agentB: "claude"
50
+ },
35
51
  "claude-antigravity": {
36
52
  agentA: "claude",
37
53
  agentB: "antigravity"
@@ -40,30 +56,30 @@ const presets = {
40
56
  agentA: "antigravity",
41
57
  agentB: "claude"
42
58
  },
43
- "gemini-opencode": {
44
- agentA: "gemini",
45
- agentB: "opencode"
46
- },
47
- "opencode-gemini": {
59
+ "opencode-antigravity": {
48
60
  agentA: "opencode",
49
- agentB: "gemini"
50
- },
51
- "gemini-antigravity": {
52
- agentA: "gemini",
53
61
  agentB: "antigravity"
54
62
  },
55
- "antigravity-gemini": {
63
+ "antigravity-opencode": {
56
64
  agentA: "antigravity",
57
- agentB: "gemini"
65
+ agentB: "opencode"
58
66
  },
59
- "opencode-antigravity": {
67
+ "opencode-vibe": {
60
68
  agentA: "opencode",
61
- agentB: "antigravity"
69
+ agentB: "vibe"
62
70
  },
63
- "antigravity-opencode": {
64
- agentA: "antigravity",
71
+ "vibe-opencode": {
72
+ agentA: "vibe",
65
73
  agentB: "opencode"
66
74
  },
75
+ "antigravity-vibe": {
76
+ agentA: "antigravity",
77
+ agentB: "vibe"
78
+ },
79
+ "vibe-antigravity": {
80
+ agentA: "vibe",
81
+ agentB: "antigravity"
82
+ },
67
83
  "opencode-ollama": {
68
84
  agentA: "opencode",
69
85
  agentB: "ollama-local"
@@ -72,6 +88,14 @@ const presets = {
72
88
  agentA: "ollama-local",
73
89
  agentB: "opencode"
74
90
  },
91
+ "vibe-ollama": {
92
+ agentA: "vibe",
93
+ agentB: "ollama-local"
94
+ },
95
+ "ollama-vibe": {
96
+ agentA: "ollama-local",
97
+ agentB: "vibe"
98
+ },
75
99
  "codex-ollama": {
76
100
  agentA: "codex",
77
101
  agentB: "ollama-local"
@@ -88,14 +112,6 @@ const presets = {
88
112
  agentA: "ollama-local",
89
113
  agentB: "claude"
90
114
  },
91
- "gemini-ollama": {
92
- agentA: "gemini",
93
- agentB: "ollama-local"
94
- },
95
- "ollama-gemini": {
96
- agentA: "ollama-local",
97
- agentB: "gemini"
98
- },
99
115
  "antigravity-ollama": {
100
116
  agentA: "antigravity",
101
117
  agentB: "ollama-local"
@@ -103,22 +119,6 @@ const presets = {
103
119
  "ollama-antigravity": {
104
120
  agentA: "ollama-local",
105
121
  agentB: "antigravity"
106
- },
107
- "codex-gemini": {
108
- agentA: "codex",
109
- agentB: "gemini"
110
- },
111
- "gemini-codex": {
112
- agentA: "gemini",
113
- agentB: "codex"
114
- },
115
- "claude-gemini": {
116
- agentA: "claude",
117
- agentB: "gemini"
118
- },
119
- "gemini-claude": {
120
- agentA: "gemini",
121
- agentB: "claude"
122
122
  }
123
123
  };
124
124
  /** Retourne la paire d'agents pour `name`. Lève une erreur avec la liste des presets disponibles si inconnu. */
package/dist/prompt.js CHANGED
@@ -1,13 +1,16 @@
1
1
  import { createTranslator } from "./i18n.js";
2
2
  /**
3
3
  * Formate le prompt complet transmis à l'adapter.
4
- * Dispatche vers le format de synthèse si `input.mode === "summary"`, sinon construit le prompt de débat standard.
4
+ * Dispatche vers le format ask ou synthèse si demandé, sinon construit le prompt de débat standard.
5
5
  */
6
6
  export function formatAgentPrompt(input) {
7
7
  const messages = createTranslator(input.language ?? "fr").prompt;
8
8
  if (input.mode === "summary") {
9
9
  return formatSummaryPrompt(input, messages);
10
10
  }
11
+ if (input.mode === "ask") {
12
+ return formatAskPrompt(input, messages);
13
+ }
11
14
  const transcript = formatTranscript(input.transcript);
12
15
  return [
13
16
  messages.subject(input.topic),
@@ -41,13 +44,43 @@ export function formatAgentPrompt(input) {
41
44
  .filter(Boolean)
42
45
  .join("\n");
43
46
  }
47
+ /** Formate le prompt d'une réponse indépendante en mode ask. */
48
+ function formatAskPrompt(input, messages) {
49
+ return [
50
+ messages.subject(input.topic),
51
+ "",
52
+ messages.askIntro(input.selfName),
53
+ messages.role(input.selfName, input.selfRole),
54
+ messages.roleInstruction(input.selfRole),
55
+ "",
56
+ messages.sessionTitle,
57
+ messages.sessionSource,
58
+ messages.localDate(input.session.localDate),
59
+ messages.timeZone(input.session.timeZone),
60
+ messages.cwd(input.session.cwd),
61
+ messages.sessionStartedAt(input.session.startedAt),
62
+ "",
63
+ messages.responseLanguageInstruction,
64
+ "",
65
+ messages.objectiveTitle,
66
+ ...messages.askObjectives,
67
+ "",
68
+ input.files.length > 0 ? messages.fileContextTitle : "",
69
+ formatFileContext(input.files),
70
+ input.files.length > 0 ? "" : "",
71
+ messages.answerTitle
72
+ ]
73
+ .filter(Boolean)
74
+ .join("\n");
75
+ }
44
76
  /** Formate le prompt de synthèse finale. Impose un format structuré : Consensus / Désaccords / Actions / Conclusion. */
45
77
  function formatSummaryPrompt(input, messages) {
46
78
  const transcript = formatTranscript(input.transcript);
79
+ const isAskSummary = input.peerName === "ask-responses";
47
80
  return [
48
81
  messages.subject(input.topic),
49
82
  "",
50
- messages.summaryIntro(input.selfName),
83
+ isAskSummary ? messages.askSummaryIntro(input.selfName) : messages.summaryIntro(input.selfName),
51
84
  messages.role(input.selfName, input.selfRole),
52
85
  messages.roleInstruction("summarizer"),
53
86
  "",
@@ -61,29 +94,47 @@ function formatSummaryPrompt(input, messages) {
61
94
  messages.responseLanguageInstruction,
62
95
  "",
63
96
  messages.objectiveTitle,
64
- ...messages.summaryObjectives,
97
+ ...(isAskSummary ? messages.askSummaryObjectives : messages.summaryObjectives),
65
98
  "",
66
99
  input.files.length > 0 ? messages.fileContextTitle : "",
67
100
  formatFileContext(input.files),
68
101
  input.files.length > 0 ? "" : "",
69
- messages.transcriptTitle,
102
+ isAskSummary ? messages.askResponsesTitle : messages.transcriptTitle,
70
103
  transcript || messages.noMessage,
71
104
  "",
72
105
  messages.expectedFormatTitle,
106
+ ...(isAskSummary ? askSummaryHeadings(messages) : debateSummaryHeadings(messages)),
107
+ "",
108
+ isAskSummary ? messages.askFinalProseInstruction : messages.finalProseInstruction,
109
+ "",
110
+ messages.summaryAnswerTitle
111
+ ]
112
+ .filter(Boolean)
113
+ .join("\n");
114
+ }
115
+ function debateSummaryHeadings(messages) {
116
+ return [
73
117
  messages.consensusHeading,
74
118
  "",
75
119
  messages.disagreementsHeading,
76
120
  "",
77
121
  messages.actionsHeading,
78
122
  "",
79
- messages.conclusionHeading,
123
+ messages.conclusionHeading
124
+ ];
125
+ }
126
+ function askSummaryHeadings(messages) {
127
+ return [
128
+ messages.askAgentSummariesHeading,
80
129
  "",
81
- messages.finalProseInstruction,
130
+ messages.askComparisonHeading,
82
131
  "",
83
- messages.summaryAnswerTitle
84
- ]
85
- .filter(Boolean)
86
- .join("\n");
132
+ messages.askWatchpointsHeading,
133
+ "",
134
+ messages.actionsHeading,
135
+ "",
136
+ messages.conclusionHeading
137
+ ];
87
138
  }
88
139
  /** Formate les fichiers projet en blocs de code annotés pour l'injection dans le prompt. */
89
140
  function formatFileContext(files) {
@@ -33,7 +33,7 @@ class PrettyConsoleRenderer {
33
33
  this.c("cyan", `┌─ ${title} ${"─".repeat(Math.max(1, 54 - title.length))}`),
34
34
  this.c("cyan", `│`) + ` ${this.messages.renderers.subject(options.topic)}`,
35
35
  this.c("cyan", `│`) + ` ${this.messages.renderers.agents(formatAgentPair(options, agents))}`,
36
- this.c("cyan", `│`) + ` ${this.messages.renderers.responsesSummary(options.turns, formatSummary(options, this.messages))}`,
36
+ this.c("cyan", `│`) + ` ${this.messages.renderers.responsesSummary(formatResponseCount(options), formatSummary(options, this.messages))}`,
37
37
  this.c("cyan", `│`) + ` ${this.messages.renderers.context(formatContext(options, this.messages))}`,
38
38
  this.c("cyan", `│`) + ` ${this.messages.renderers.options(options.earlyStopOnAgreement, options.pullModels)}`,
39
39
  this.c("cyan", `└${"─".repeat(57)}`),
@@ -58,6 +58,15 @@ class PrettyConsoleRenderer {
58
58
  ""
59
59
  ].join("\n"));
60
60
  }
61
+ askResponseStart(response, totalResponses, agent, role) {
62
+ this.renderingSummary = false;
63
+ process.stdout.write([
64
+ "",
65
+ this.c("orange", `◆ ${agent}`) + this.dim(` · ${role} · réponse ${response}/${totalResponses}`),
66
+ this.dim("─".repeat(60)),
67
+ ""
68
+ ].join("\n"));
69
+ }
61
70
  /** Démarre le spinner de réflexion (ou affiche une ligne fixe si non interactif). */
62
71
  thinkingStart(agent, role) {
63
72
  this.thinkingEnd();
@@ -89,6 +98,9 @@ class PrettyConsoleRenderer {
89
98
  const trimmed = content.trim();
90
99
  process.stdout.write(`${this.renderingSummary ? this.formatSummaryMessage(trimmed) : trimmed}\n`);
91
100
  }
101
+ askResponseMessage(content) {
102
+ this.message(content);
103
+ }
92
104
  /** Affiche l'en-tête de section synthèse et active le mode formatage de résumé. */
93
105
  summaryStart(agent, role) {
94
106
  this.renderingSummary = true;
@@ -153,7 +165,7 @@ class PlainConsoleRenderer {
153
165
  start(options, agents = []) {
154
166
  process.stdout.write(this.messages.renderers.subject(options.topic) + "\n");
155
167
  process.stdout.write(this.messages.renderers.agents(formatAgentPair(options, agents)) + "\n");
156
- process.stdout.write(this.messages.renderers.responsesSummaryContext(options.turns, formatSummary(options, this.messages), formatContext(options, this.messages)) + "\n");
168
+ process.stdout.write(this.messages.renderers.responsesSummaryContext(formatResponseCount(options), formatSummary(options, this.messages), formatContext(options, this.messages)) + "\n");
157
169
  }
158
170
  /** Écrit un avertissement sur `stderr`. */
159
171
  warning(message) {
@@ -167,6 +179,9 @@ class PlainConsoleRenderer {
167
179
  turnStart(turn, totalTurns, agent, role) {
168
180
  process.stdout.write(`\n[${turn}/${totalTurns}] ${agent} (${role})...\n`);
169
181
  }
182
+ askResponseStart(response, totalResponses, agent, role) {
183
+ process.stdout.write(`\n[${response}/${totalResponses}] ${agent} (${role})...\n`);
184
+ }
170
185
  /** No-op : pas de spinner en mode plain. */
171
186
  thinkingStart(_agent, _role) { }
172
187
  /** No-op : pas de spinner à arrêter en mode plain. */
@@ -175,6 +190,9 @@ class PlainConsoleRenderer {
175
190
  message(content) {
176
191
  process.stdout.write(`${content.trim()}\n`);
177
192
  }
193
+ askResponseMessage(content) {
194
+ this.message(content);
195
+ }
178
196
  /** Affiche l'en-tête de la section synthèse en texte brut. */
179
197
  summaryStart(agent, role) {
180
198
  process.stdout.write(`\n[${this.messages.renderers.summaryTitle}] ${agent} (${role})...\n`);
@@ -194,6 +212,12 @@ class PlainConsoleRenderer {
194
212
  * @param agents - Infos de démarrage des agents (type, rôle, nom).
195
213
  */
196
214
  function formatAgentPair(options, agents) {
215
+ if (options.mode === "ask") {
216
+ if (agents.length > 0) {
217
+ return agents.map(formatAgentLabel).join(", ");
218
+ }
219
+ return (options.askAgents ?? [options.agentA, options.agentB]).join(", ");
220
+ }
197
221
  if (agents.length >= 2) {
198
222
  return `${formatAgentLabel(agents[0])} <-> ${formatAgentLabel(agents[1])}`;
199
223
  }
@@ -214,7 +238,19 @@ function formatAgentLabel(agent) {
214
238
  * @param options - Options du débat.
215
239
  */
216
240
  function formatSummary(options, messages) {
217
- return options.summaryEnabled ? options.summaryAgent ?? options.agentB : messages.renderers.disabled;
241
+ if (!options.summaryEnabled) {
242
+ return messages.renderers.disabled;
243
+ }
244
+ if (options.summaryAgent) {
245
+ return options.summaryAgent;
246
+ }
247
+ if (options.mode === "ask" && options.askAgents && options.askAgents.length > 0) {
248
+ return options.askAgents[options.askAgents.length - 1] ?? options.agentB;
249
+ }
250
+ return options.agentB;
251
+ }
252
+ function formatResponseCount(options) {
253
+ return options.mode === "ask" ? options.askAgents?.length ?? 2 : options.turns;
218
254
  }
219
255
  /**
220
256
  * Renvoie un résumé du contexte injecté (nombre de fichiers ou mention d'absence).