palabre 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,192 @@
1
+ export const tuiMessages = {
2
+ fr: {
3
+ tagline: "Orchestrez des conversations entre agents IA",
4
+ modeLabel: (mode) => mode === "ask" ? "Ask" : "Debat",
5
+ modeValue: (mode) => mode === "ask" ? "Ask" : "Debat",
6
+ noValue: "non definis",
7
+ lastAskAgent: "dernier agent ask",
8
+ roles: "Roles",
9
+ summary: "Synthese",
10
+ responses: "Reponses",
11
+ folder: "Dossier",
12
+ docs: "Docs",
13
+ commands: "commandes",
14
+ settings: "reglages",
15
+ changeMode: "changer de mode",
16
+ tipContext: "* Tip Ajoute du contexte avec --context <dossier> ou --files <fichier>.",
17
+ helpTitle: "Commandes TUI",
18
+ helpAsk: "passer en mode Ask pour collecter plusieurs reponses independantes",
19
+ helpDebate: "passer en mode Debat pour lancer une conversation entre deux agents",
20
+ helpAgents: "afficher et choisir les agents actifs",
21
+ helpRoles: "afficher les roles disponibles",
22
+ helpConfig: "configurer Palabre sans sortir de la TUI",
23
+ helpNew: "ouvrir l'assistant guide",
24
+ helpHelp: "afficher cette aide",
25
+ helpQuit: "quitter la TUI",
26
+ helpFallback: "Tout autre texte est utilise comme demande a envoyer aux agents.",
27
+ agentsTitle: "Agents Palabre",
28
+ activeMode: "Mode actif",
29
+ activeAgents: "Agents actifs",
30
+ availableAgents: "Agents disponibles",
31
+ example: "Exemple",
32
+ rolesTitle: "Roles Palabre",
33
+ currentConfig: "Config actuelle",
34
+ availableRoles: "Roles disponibles",
35
+ roleImplementer: "propose une solution concrete",
36
+ roleCritic: "challenge les hypotheses et demande des preuves",
37
+ roleArchitect: "structure les options et compromis",
38
+ roleScout: "explore les pistes et inconnues",
39
+ roleReviewer: "cherche risques et tests manquants",
40
+ roleSummarizer: "synthetise fidelement",
41
+ configTitle: "Configuration Palabre",
42
+ configFile: "Config",
43
+ interface: "Interface",
44
+ language: "Langue",
45
+ availableAgentsShort: "Agents dispo",
46
+ availableCommands: "Commandes disponibles",
47
+ noConfiguredAgents: "aucun agent configure",
48
+ or: "ou",
49
+ defaultModeCommand: (mode) => `utiliser ${mode === "ask" ? "Ask" : "Debat"} par defaut`,
50
+ modeConfigCommand: "changer de mode de configuration",
51
+ backCommand: "revenir a l'accueil",
52
+ quitCommand: "quitter",
53
+ subject: "Sujet",
54
+ configPrompt: "Config",
55
+ agentsPrompt: "Agents",
56
+ rolesPrompt: "Roles",
57
+ sessionDone: "Session terminee",
58
+ askResponse: (response, totalResponses) => `reponse ${response}/${totalResponses}`,
59
+ turnLabel: (turn) => `tour ${turn}`,
60
+ planTitle: "Plan de session",
61
+ planCollectAsk: "Collecter les reponses",
62
+ planLaunchDebate: "Lancer le debat",
63
+ planSummaryComparative: "Synthese comparative",
64
+ planSummaryDisabled: "Synthese desactivee",
65
+ planExport: "Export Markdown",
66
+ ptyNotice: (agents) => `Note: ${agents} utilise un pseudo-terminal; une fenetre peut apparaitre brievement.`,
67
+ unknownCommand: "Commande inconnue. Utilise /back pour revenir.",
68
+ turnsUsage: "Usage: /turns <nombre>",
69
+ summaryUsage: "Usage: /summary <agent|none>",
70
+ interfaceDefault: (value) => `Interface par defaut: ${value}.`,
71
+ languageUpdated: (value) => `Langue mise a jour: ${value}.`,
72
+ askConfigMode: "Configuration Ask.",
73
+ debateConfigMode: "Configuration Debat.",
74
+ askDefaultMode: "Ask devient le mode par defaut.",
75
+ debateDefaultMode: "Debat devient le mode par defaut.",
76
+ agentsUnchanged: "Agents inchanges.",
77
+ rolesUnchanged: "Roles inchanges.",
78
+ askTurnsNotice: "En mode Ask, le nombre de reponses depend des agents selectionnes avec /agents.",
79
+ turnsUpdated: (value) => `Tours mis a jour: ${value}.`,
80
+ askSummaryFallback: "Synthese Ask revenue au fallback.",
81
+ debateSummaryFallback: "Synthese Debat revenue au fallback.",
82
+ askSummaryAgent: (value) => `Synthese Ask: ${value}.`,
83
+ debateSummaryAgent: (value) => `Synthese Debat: ${value}.`,
84
+ askAgentsUpdated: (value) => `Agents Ask mis a jour: ${value}.`,
85
+ debateAgentsUpdated: (value) => `Agents Debat mis a jour: ${value}.`,
86
+ askRolesUpdated: (value) => `Roles Ask mis a jour: ${value}.`,
87
+ debateRolesUpdated: (value) => `Roles Debat mis a jour: ${value}.`,
88
+ rolesError: (message) => `Erreur roles: ${message}`,
89
+ agentsError: (message) => `Erreur agents: ${message}`,
90
+ noAskAgentsConfigured: "Aucun agent Ask configure.",
91
+ noDebateAgentsConfigured: "Agents Debat non definis.",
92
+ rolesCountError: (count, expected, agents) => `${count} role(s) saisi(s), ${expected} attendu(s). Saisis au moins ${expected} roles pour: ${agents}.`,
93
+ unknownRole: (role, available) => `Role inconnu: ${role}. Roles disponibles: ${available}.`,
94
+ debateAgentsUsage: "Usage: /agents <agentA> <agentB>",
95
+ askAgentsUsage: "Usage: /agents <agent...>"
96
+ },
97
+ en: {
98
+ tagline: "Orchestrate conversations between AI agents",
99
+ modeLabel: (mode) => mode === "ask" ? "Ask" : "Debate",
100
+ modeValue: (mode) => mode === "ask" ? "Ask" : "Debate",
101
+ noValue: "not set",
102
+ lastAskAgent: "last ask agent",
103
+ roles: "Roles",
104
+ summary: "Summary",
105
+ responses: "Responses",
106
+ folder: "Folder",
107
+ docs: "Docs",
108
+ commands: "commands",
109
+ settings: "settings",
110
+ changeMode: "change mode",
111
+ tipContext: "* Tip Add context with --context <folder> or --files <file>.",
112
+ helpTitle: "TUI Commands",
113
+ helpAsk: "switch to Ask mode to collect independent responses",
114
+ helpDebate: "switch to Debate mode for a conversation between two agents",
115
+ helpAgents: "show and choose active agents",
116
+ helpRoles: "show available roles",
117
+ helpConfig: "configure Palabre without leaving the TUI",
118
+ helpNew: "open the guided assistant",
119
+ helpHelp: "show this help",
120
+ helpQuit: "quit the TUI",
121
+ helpFallback: "Any other text is used as the request sent to agents.",
122
+ agentsTitle: "Palabre Agents",
123
+ activeMode: "Active mode",
124
+ activeAgents: "Active agents",
125
+ availableAgents: "Available agents",
126
+ example: "Example",
127
+ rolesTitle: "Palabre Roles",
128
+ currentConfig: "Current config",
129
+ availableRoles: "Available roles",
130
+ roleImplementer: "proposes a concrete solution",
131
+ roleCritic: "challenges assumptions and asks for evidence",
132
+ roleArchitect: "structures options and trade-offs",
133
+ roleScout: "explores paths and unknowns",
134
+ roleReviewer: "looks for risks and missing tests",
135
+ roleSummarizer: "summarizes faithfully",
136
+ configTitle: "Palabre Configuration",
137
+ configFile: "Config",
138
+ interface: "Interface",
139
+ language: "Language",
140
+ availableAgentsShort: "Agents",
141
+ availableCommands: "Available commands",
142
+ noConfiguredAgents: "no configured agent",
143
+ or: "or",
144
+ defaultModeCommand: (mode) => `use ${mode === "ask" ? "Ask" : "Debate"} as default`,
145
+ modeConfigCommand: "change configuration mode",
146
+ backCommand: "return to home",
147
+ quitCommand: "quit",
148
+ subject: "Subject",
149
+ configPrompt: "Config",
150
+ agentsPrompt: "Agents",
151
+ rolesPrompt: "Roles",
152
+ sessionDone: "Session complete",
153
+ askResponse: (response, totalResponses) => `response ${response}/${totalResponses}`,
154
+ turnLabel: (turn) => `turn ${turn}`,
155
+ planTitle: "Session plan",
156
+ planCollectAsk: "Collect responses",
157
+ planLaunchDebate: "Start debate",
158
+ planSummaryComparative: "Comparative summary",
159
+ planSummaryDisabled: "Summary disabled",
160
+ planExport: "Markdown export",
161
+ ptyNotice: (agents) => `Note: ${agents} uses a pseudo-terminal; a window may briefly appear.`,
162
+ unknownCommand: "Unknown command. Use /back to return.",
163
+ turnsUsage: "Usage: /turns <number>",
164
+ summaryUsage: "Usage: /summary <agent|none>",
165
+ interfaceDefault: (value) => `Default interface: ${value}.`,
166
+ languageUpdated: (value) => `Language updated: ${value}.`,
167
+ askConfigMode: "Ask configuration.",
168
+ debateConfigMode: "Debate configuration.",
169
+ askDefaultMode: "Ask is now the default mode.",
170
+ debateDefaultMode: "Debate is now the default mode.",
171
+ agentsUnchanged: "Agents unchanged.",
172
+ rolesUnchanged: "Roles unchanged.",
173
+ askTurnsNotice: "In Ask mode, the number of responses depends on agents selected with /agents.",
174
+ turnsUpdated: (value) => `Turns updated: ${value}.`,
175
+ askSummaryFallback: "Ask summary returned to fallback.",
176
+ debateSummaryFallback: "Debate summary returned to fallback.",
177
+ askSummaryAgent: (value) => `Ask summary: ${value}.`,
178
+ debateSummaryAgent: (value) => `Debate summary: ${value}.`,
179
+ askAgentsUpdated: (value) => `Ask agents updated: ${value}.`,
180
+ debateAgentsUpdated: (value) => `Debate agents updated: ${value}.`,
181
+ askRolesUpdated: (value) => `Ask roles updated: ${value}.`,
182
+ debateRolesUpdated: (value) => `Debate roles updated: ${value}.`,
183
+ rolesError: (message) => `Roles error: ${message}`,
184
+ agentsError: (message) => `Agents error: ${message}`,
185
+ noAskAgentsConfigured: "No Ask agent configured.",
186
+ noDebateAgentsConfigured: "Debate agents are not set.",
187
+ rolesCountError: (count, expected, agents) => `${count} role(s) entered, ${expected} expected. Enter at least ${expected} roles for: ${agents}.`,
188
+ unknownRole: (role, available) => `Unknown role: ${role}. Available roles: ${available}.`,
189
+ debateAgentsUsage: "Usage: /agents <agentA> <agentB>",
190
+ askAgentsUsage: "Usage: /agents <agent...>"
191
+ }
192
+ };
@@ -13,7 +13,14 @@ export const updateMessages = {
13
13
  lines.push("", "Installation depuis le repo source detectee.", "", ` cd "${options.projectRoot}"`, " git pull --ff-only", " pnpm install", " pnpm build", " pnpm link --global", "", "Pour executer ces etapes automatiquement:", "", " palabre update --apply");
14
14
  }
15
15
  else {
16
- lines.push("", "Installation package detectee.", "", " pnpm add --global palabre@latest", "", "Si tu utilises npm:", "", " npm install --global palabre@latest");
16
+ lines.push("", "Installation package detectee.", "", " pnpm add --global palabre@latest");
17
+ if (options.latestVersion) {
18
+ lines.push("", "Si pnpm garde une ancienne version malgré @latest:", "", ` pnpm add --global palabre@${options.latestVersion}`);
19
+ }
20
+ else {
21
+ lines.push("", "Pour verifier la derniere version disponible:", "", " pnpm view palabre version");
22
+ }
23
+ lines.push("", "Si tu utilises npm:", "", " npm install --global palabre@latest");
17
24
  }
18
25
  return lines.join("\n");
19
26
  }
@@ -32,7 +39,14 @@ export const updateMessages = {
32
39
  lines.push("", "Source repository installation detected.", "", ` cd "${options.projectRoot}"`, " git pull --ff-only", " pnpm install", " pnpm build", " pnpm link --global", "", "To run these steps automatically:", "", " palabre update --apply");
33
40
  }
34
41
  else {
35
- lines.push("", "Package installation detected.", "", " pnpm add --global palabre@latest", "", "If you use npm:", "", " npm install --global palabre@latest");
42
+ lines.push("", "Package installation detected.", "", " pnpm add --global palabre@latest");
43
+ if (options.latestVersion) {
44
+ lines.push("", "If pnpm keeps an older version despite @latest:", "", ` pnpm add --global palabre@${options.latestVersion}`);
45
+ }
46
+ else {
47
+ lines.push("", "To check the latest available version:", "", " pnpm view palabre version");
48
+ }
49
+ lines.push("", "If you use npm:", "", " npm install --global palabre@latest");
36
50
  }
37
51
  return lines.join("\n");
38
52
  }
package/dist/new.js CHANGED
@@ -21,6 +21,75 @@ export async function runNewWizard(config, messages) {
21
21
  console.log(messages.new.quitHint);
22
22
  console.log(messages.new.defaultHint);
23
23
  console.log("");
24
+ const mode = await askMode(rl, config.defaults?.mode ?? "debate", messages);
25
+ if (!mode)
26
+ return undefined;
27
+ if (mode === "ask") {
28
+ const askAgents = await askAgentList(rl, choices, config.defaults?.askAgents ?? defaultAskAgents(config), messages);
29
+ if (!askAgents)
30
+ return undefined;
31
+ const [agentA, agentB] = [askAgents[0], askAgents[1] ?? askAgents[0]];
32
+ if (!agentA || !agentB)
33
+ return undefined;
34
+ const topic = await askRequiredText(rl, messages.new.topic, messages);
35
+ if (!topic)
36
+ return undefined;
37
+ printCommandPreview({ mode, agentA, agentB, askAgents, topic }, messages);
38
+ console.log(messages.new.advancedHint);
39
+ const launchMinimal = await askYesNo(rl, messages.new.launchMinimal, true, messages);
40
+ if (launchMinimal === undefined)
41
+ return undefined;
42
+ if (launchMinimal) {
43
+ return {
44
+ mode,
45
+ agentA,
46
+ agentB,
47
+ askAgents,
48
+ topic,
49
+ files: [],
50
+ context: [],
51
+ showPrompt: false,
52
+ plainOutput: false
53
+ };
54
+ }
55
+ const summaryEnabled = await askYesNo(rl, messages.new.summaryEnabled, true, messages);
56
+ if (summaryEnabled === undefined)
57
+ return undefined;
58
+ let summaryAgent;
59
+ let summaryModel;
60
+ if (summaryEnabled) {
61
+ summaryAgent = await askAgent(rl, choices, messages.new.summaryAgent, config.defaults?.askSummaryAgent ?? config.defaults?.summaryAgent ?? askAgents[askAgents.length - 1], messages);
62
+ if (!summaryAgent)
63
+ return undefined;
64
+ summaryModel = await askOptionalText(rl, messages.new.summaryModelFor(summaryAgent));
65
+ if (summaryModel === undefined)
66
+ return undefined;
67
+ }
68
+ const context = splitPaths(await askOptionalText(rl, messages.new.contextPaths));
69
+ const files = splitPaths(await askOptionalText(rl, messages.new.filesPaths));
70
+ const showPrompt = await askYesNo(rl, messages.new.showPrompt, false, messages);
71
+ if (showPrompt === undefined)
72
+ return undefined;
73
+ const plainOutput = await askYesNo(rl, messages.new.plainOutput, false, messages);
74
+ if (plainOutput === undefined)
75
+ return undefined;
76
+ const selection = {
77
+ mode,
78
+ agentA,
79
+ agentB,
80
+ askAgents,
81
+ topic,
82
+ summaryAgent,
83
+ summaryModel,
84
+ summaryEnabled,
85
+ files,
86
+ context,
87
+ showPrompt,
88
+ plainOutput
89
+ };
90
+ printCommandPreview(selection, messages);
91
+ return selection;
92
+ }
24
93
  const agentA = await askAgent(rl, choices, messages.new.agentA, config.defaults?.agentA, messages);
25
94
  if (!agentA)
26
95
  return undefined;
@@ -40,6 +109,7 @@ export async function runNewWizard(config, messages) {
40
109
  agentA,
41
110
  agentB,
42
111
  topic,
112
+ mode,
43
113
  files: [],
44
114
  context: [],
45
115
  showPrompt: false,
@@ -80,6 +150,7 @@ export async function runNewWizard(config, messages) {
80
150
  agentA,
81
151
  agentB,
82
152
  topic,
153
+ mode,
83
154
  modelA,
84
155
  modelB,
85
156
  turns,
@@ -98,6 +169,35 @@ export async function runNewWizard(config, messages) {
98
169
  rl.close();
99
170
  }
100
171
  }
172
+ async function askMode(rl, defaultMode, messages) {
173
+ const choices = [
174
+ { value: "debate", label: messages.new.modeDebate },
175
+ { value: "ask", label: messages.new.modeAsk }
176
+ ];
177
+ const fallback = choices.find((choice) => choice.value === defaultMode)?.value ?? "debate";
178
+ console.log(messages.new.mode);
179
+ choices.forEach((choice, index) => {
180
+ const marker = choice.value === fallback ? "(*)" : " ";
181
+ console.log(` ${index + 1}) ${marker} ${choice.label}`);
182
+ });
183
+ while (true) {
184
+ const answer = await rl.question(`${messages.new.mode} [${fallback}]: `);
185
+ const value = answer.trim().toLowerCase();
186
+ if (isQuit(value))
187
+ return undefined;
188
+ if (!value)
189
+ return fallback;
190
+ const number = Number(value);
191
+ if (Number.isInteger(number) && number >= 1 && number <= choices.length) {
192
+ return choices[number - 1]?.value;
193
+ }
194
+ if (value === "debate" || value === "débat" || value === "debat")
195
+ return "debate";
196
+ if (value === "ask" || value === "demande" || value === "question")
197
+ return "ask";
198
+ console.log(messages.new.invalidModeChoice);
199
+ }
200
+ }
101
201
  async function createQuestioner() {
102
202
  if (input.isTTY) {
103
203
  return createInterface({ input, output });
@@ -171,6 +271,44 @@ async function askAgent(rl, choices, label, defaultName, messages) {
171
271
  console.log(messages.new.invalidAgentChoice);
172
272
  }
173
273
  }
274
+ async function askAgentList(rl, choices, defaultNames, messages) {
275
+ const availableNames = choices.map((choice) => choice.name);
276
+ const fallback = uniqueNames(defaultNames.filter((name) => availableNames.includes(name))).slice(0, 4);
277
+ const defaultSelection = fallback.length > 0 ? fallback : availableNames.slice(0, Math.min(2, availableNames.length));
278
+ console.log(messages.new.askAgents);
279
+ choices.forEach((choice, index) => {
280
+ const marker = defaultSelection.includes(choice.name) ? "(*)" : " ";
281
+ console.log(` ${index + 1}) ${marker} ${choice.name} - ${choice.status}`);
282
+ });
283
+ while (true) {
284
+ const answer = await rl.question(`${messages.new.askAgentsPrompt(defaultSelection.join(" "))}: `);
285
+ const value = answer.trim();
286
+ if (isQuit(value))
287
+ return undefined;
288
+ if (!value)
289
+ return defaultSelection;
290
+ const tokens = value.split(/[,\s]+/).map((token) => token.trim()).filter(Boolean);
291
+ const resolved = uniqueNames(tokens.map((token) => {
292
+ const number = Number(token);
293
+ if (Number.isInteger(number) && number >= 1 && number <= choices.length) {
294
+ return choices[number - 1]?.name;
295
+ }
296
+ return token;
297
+ }).filter((name) => Boolean(name)));
298
+ if (resolved.length === 0) {
299
+ console.log(messages.new.invalidAskAgentsChoice);
300
+ continue;
301
+ }
302
+ if (resolved.length > 4) {
303
+ console.log(messages.common.tooManyAskAgents(4));
304
+ continue;
305
+ }
306
+ if (resolved.every((name) => availableNames.includes(name))) {
307
+ return resolved;
308
+ }
309
+ console.log(messages.new.invalidAskAgentsChoice);
310
+ }
311
+ }
174
312
  async function askRequiredText(rl, label, messages) {
175
313
  while (true) {
176
314
  const answer = await rl.question(`${label}: `);
@@ -230,6 +368,12 @@ function splitPaths(value) {
230
368
  .map((entry) => entry.trim())
231
369
  .filter(Boolean) ?? [];
232
370
  }
371
+ function defaultAskAgents(config) {
372
+ return [config.defaults?.agentA, config.defaults?.agentB].filter((agent) => Boolean(agent));
373
+ }
374
+ function uniqueNames(names) {
375
+ return names.filter((name, index) => names.indexOf(name) === index);
376
+ }
233
377
  function isQuit(value) {
234
378
  return ["q", "quit", "exit"].includes(value.toLowerCase());
235
379
  }
@@ -246,13 +390,23 @@ function printCommandPreview(selection, messages) {
246
390
  }
247
391
  function buildExplicitCommand(selection) {
248
392
  const args = ["palabre"];
249
- args.push("--agent-a", selection.agentA);
250
- args.push("--agent-b", selection.agentB);
251
- args.push(quoteShellArg(selection.topic));
393
+ if (selection.mode === "ask") {
394
+ args.push("ask", quoteShellArg(selection.topic));
395
+ const askAgents = selection.askAgents && selection.askAgents.length > 0 ? selection.askAgents : [selection.agentA, selection.agentB];
396
+ args.push("--agents", ...askAgents);
397
+ }
398
+ else {
399
+ args.push("--agent-a", selection.agentA);
400
+ args.push("--agent-b", selection.agentB);
401
+ args.push(quoteShellArg(selection.topic));
402
+ }
252
403
  appendOptionalArgs(args, selection);
253
404
  return args.join(" ");
254
405
  }
255
406
  function buildShortCommand(selection) {
407
+ if (selection.mode === "ask") {
408
+ return undefined;
409
+ }
256
410
  const presetName = findPresetNameForPair(selection.agentA, selection.agentB);
257
411
  if (!presetName) {
258
412
  return undefined;
@@ -281,7 +435,7 @@ function appendOptionalArgs(args, selection) {
281
435
  if (selection.showPrompt)
282
436
  args.push("--show-prompt");
283
437
  if (selection.plainOutput)
284
- args.push("--plain");
438
+ args.push("--terminal");
285
439
  }
286
440
  function quoteShellArg(value) {
287
441
  if (/^[A-Za-z0-9._/:\\-]+$/.test(value)) {
@@ -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,7 @@ 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),
108
109
  turn: transcript.length + 1
109
110
  });
110
111
  if (cancellation) {
@@ -121,7 +122,7 @@ export async function runDebate(config, options, renderer, messages = createTran
121
122
  catch (error) {
122
123
  failure = toDebateFailure(error, {
123
124
  phase: "summary",
124
- agent: options.summaryAgent ?? options.agentB,
125
+ agent: resolveSummaryAgentName(options),
125
126
  turn: transcript.length + 1
126
127
  });
127
128
  renderer?.error(failure);
@@ -135,6 +136,141 @@ export async function runDebate(config, options, renderer, messages = createTran
135
136
  failure
136
137
  };
137
138
  }
139
+ /**
140
+ * Lance le mode ask : plusieurs agents répondent indépendamment au même sujet,
141
+ * puis un agent de synthèse résume fidèlement chaque réponse et les compare.
142
+ */
143
+ export async function runAsk(config, options, renderer, messages = createTranslator("fr")) {
144
+ const askAgentNames = resolveAskAgentNames(options);
145
+ if (askAgentNames.length === 0) {
146
+ throw new Error(messages.common.noAgentDefined("ask agent"));
147
+ }
148
+ if (askAgentNames.length > MAX_ASK_AGENTS) {
149
+ throw new Error(messages.common.tooManyAskAgents(MAX_ASK_AGENTS));
150
+ }
151
+ const agentEntries = askAgentNames.map((name) => {
152
+ const agentConfig = withRuntimeOverrides(config.agents[name], modelForAgent(options, name), options.pullModels);
153
+ if (!agentConfig) {
154
+ throw new Error(messages.common.unknownAgent(name));
155
+ }
156
+ return [name, agentConfig];
157
+ });
158
+ warnIfOllamaHasNoContext(options, agentEntries.map(([name, agentConfig]) => [name, agentConfig]), renderer, messages);
159
+ renderer?.start(options, agentEntries.map(([name, agentConfig]) => ({
160
+ name,
161
+ role: agentConfig.role,
162
+ type: agentConfig.type
163
+ })));
164
+ const agents = agentEntries.map(([name, agentConfig]) => createAgent(name, agentConfig));
165
+ const transcript = [];
166
+ for (let index = 0; index < agents.length; index += 1) {
167
+ const current = agents[index];
168
+ const response = index + 1;
169
+ const cancellation = cancellationFailureIfAborted(options, messages, {
170
+ phase: "ask",
171
+ agent: current.name,
172
+ role: current.role,
173
+ turn: response
174
+ });
175
+ if (cancellation) {
176
+ renderer?.error(cancellation);
177
+ return {
178
+ options,
179
+ messages: transcript,
180
+ failure: cancellation
181
+ };
182
+ }
183
+ if (renderer?.askResponseStart) {
184
+ renderer.askResponseStart(response, agents.length, current.name, current.role);
185
+ }
186
+ else {
187
+ renderer?.turnStart(response, agents.length, current.name, current.role);
188
+ }
189
+ renderer?.thinkingStart(current.name, current.role);
190
+ let agentResponse;
191
+ try {
192
+ agentResponse = await current.generate({
193
+ topic: options.topic,
194
+ turn: response,
195
+ totalTurns: agents.length,
196
+ selfName: current.name,
197
+ peerName: "independent-agents",
198
+ selfRole: current.role,
199
+ mode: "ask",
200
+ language: options.language,
201
+ session: options.session,
202
+ files: options.files,
203
+ transcript: [],
204
+ signal: options.signal
205
+ });
206
+ }
207
+ catch (error) {
208
+ const failure = toDebateFailure(error, {
209
+ phase: "ask",
210
+ agent: current.name,
211
+ role: current.role,
212
+ turn: response
213
+ });
214
+ renderer?.error(failure);
215
+ return {
216
+ options,
217
+ messages: transcript,
218
+ failure
219
+ };
220
+ }
221
+ finally {
222
+ renderer?.thinkingEnd();
223
+ }
224
+ const message = {
225
+ agent: current.name,
226
+ role: current.role,
227
+ content: agentResponse.content,
228
+ createdAt: new Date().toISOString()
229
+ };
230
+ transcript.push(message);
231
+ if (renderer?.askResponseMessage) {
232
+ renderer.askResponseMessage(message.content);
233
+ }
234
+ else {
235
+ renderer?.message(message.content);
236
+ }
237
+ }
238
+ let summary;
239
+ let failure;
240
+ if (options.summaryEnabled) {
241
+ try {
242
+ const summaryAgentName = resolveSummaryAgentName(options);
243
+ const cancellation = cancellationFailureIfAborted(options, messages, {
244
+ phase: "summary",
245
+ agent: summaryAgentName,
246
+ turn: transcript.length + 1
247
+ });
248
+ if (cancellation) {
249
+ renderer?.error(cancellation);
250
+ return {
251
+ options,
252
+ messages: transcript,
253
+ failure: cancellation
254
+ };
255
+ }
256
+ summary = await generateSummary(config, options, transcript, renderer, messages);
257
+ }
258
+ catch (error) {
259
+ failure = toDebateFailure(error, {
260
+ phase: "summary",
261
+ agent: resolveSummaryAgentName(options),
262
+ turn: transcript.length + 1
263
+ });
264
+ renderer?.error(failure);
265
+ }
266
+ }
267
+ return {
268
+ options,
269
+ messages: transcript,
270
+ summary,
271
+ failure
272
+ };
273
+ }
138
274
  /**
139
275
  * Heuristique d'arrêt sur accord explicite.
140
276
  * Ne s'active qu'après un tour complet (nombre pair de messages) pour éviter les faux positifs.
@@ -189,7 +325,7 @@ function warnIfOllamaHasNoContext(options, agents, renderer, messages = createTr
189
325
  * @throws {Error} si l'agent de synthèse est absent de `config.agents`.
190
326
  */
191
327
  async function generateSummary(config, options, transcript, renderer, messages = createTranslator("fr")) {
192
- const summaryAgentName = options.summaryAgent ?? options.agentB;
328
+ const summaryAgentName = resolveSummaryAgentName(options);
193
329
  const summaryModel = options.summaryModel ?? modelForAgent(options, summaryAgentName);
194
330
  const summaryConfig = withRuntimeOverrides(config.agents[summaryAgentName], summaryModel, options.pullModels);
195
331
  if (!summaryConfig) {
@@ -201,9 +337,9 @@ async function generateSummary(config, options, transcript, renderer, messages =
201
337
  const response = await summaryAgent.generate({
202
338
  topic: options.topic,
203
339
  turn: transcript.length + 1,
204
- totalTurns: options.turns,
340
+ totalTurns: options.mode === "ask" ? transcript.length : options.turns,
205
341
  selfName: summaryAgent.name,
206
- peerName: "transcript",
342
+ peerName: options.mode === "ask" ? "ask-responses" : "transcript",
207
343
  selfRole: summaryAgent.role,
208
344
  mode: "summary",
209
345
  language: options.language,
@@ -234,6 +370,21 @@ function cancellationFailureIfAborted(options, messages, context) {
234
370
  message: messages.orchestrator.cancelled
235
371
  };
236
372
  }
373
+ function resolveAskAgentNames(options) {
374
+ const agents = options.askAgents && options.askAgents.length > 0
375
+ ? options.askAgents
376
+ : [options.agentA, options.agentB];
377
+ return agents.filter((agent, index) => Boolean(agent) && agents.indexOf(agent) === index);
378
+ }
379
+ function resolveSummaryAgentName(options) {
380
+ if (options.summaryAgent) {
381
+ return options.summaryAgent;
382
+ }
383
+ if (options.mode === "ask" && options.askAgents && options.askAgents.length > 0) {
384
+ return options.askAgents[options.askAgents.length - 1] ?? options.agentB;
385
+ }
386
+ return options.agentB;
387
+ }
237
388
  function toDebateFailure(error, context) {
238
389
  if (error instanceof AdapterError) {
239
390
  return {