actoviq-agent-sdk 0.4.1 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README-zh.md +8 -4
- package/README.md +8 -4
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/router/modelRouter.d.ts +46 -0
- package/dist/src/router/modelRouter.d.ts.map +1 -0
- package/dist/src/router/modelRouter.js +227 -0
- package/dist/src/router/modelRouter.js.map +1 -0
- package/dist/src/runtime/agentClient.d.ts +3 -1
- package/dist/src/runtime/agentClient.d.ts.map +1 -1
- package/dist/src/runtime/agentClient.js +6 -2
- package/dist/src/runtime/agentClient.js.map +1 -1
- package/dist/src/team/modelTeam.d.ts +34 -7
- package/dist/src/team/modelTeam.d.ts.map +1 -1
- package/dist/src/team/modelTeam.js +312 -649
- package/dist/src/team/modelTeam.js.map +1 -1
- package/dist/src/team/teamDefinitions.d.ts.map +1 -1
- package/dist/src/team/teamDefinitions.js +0 -11
- package/dist/src/team/teamDefinitions.js.map +1 -1
- package/dist/src/tui/actoviqTui.d.ts.map +1 -1
- package/dist/src/tui/actoviqTui.js +82 -10
- package/dist/src/tui/actoviqTui.js.map +1 -1
- package/dist/src/types.d.ts +80 -66
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/types.js.map +1 -1
- package/package.json +1 -1
|
@@ -101,567 +101,125 @@ async function mapWithConcurrency(items, limit, fn) {
|
|
|
101
101
|
return results;
|
|
102
102
|
}
|
|
103
103
|
// ═══════════════════════════════════════════════════════════════════
|
|
104
|
-
//
|
|
104
|
+
// Reviewer Mode — single read-only ReAct reviewer the main agent invokes
|
|
105
105
|
// ═══════════════════════════════════════════════════════════════════
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
// Round 1: parallel panel
|
|
118
|
-
rounds++;
|
|
119
|
-
const round1Start = Date.now();
|
|
120
|
-
const pool = getGlobalAgentPool();
|
|
121
|
-
const panelResults = await mapWithConcurrency(definition.members, definition.maxParallel ?? definition.members.length, async (member, i) => {
|
|
122
|
-
let slot;
|
|
123
|
-
try {
|
|
124
|
-
slot = await pool.acquire(definition.timeoutMs);
|
|
125
|
-
const result = await singleModelCall(memberApis[i], prompt, member.systemPrompt, memberSignal(signal, definition.timeoutMs));
|
|
126
|
-
const resp = {
|
|
127
|
-
round: 1,
|
|
128
|
-
model: member.model,
|
|
129
|
-
content: result.content,
|
|
130
|
-
tokens: { input: result.inputTokens, output: result.outputTokens },
|
|
131
|
-
durationMs: result.durationMs,
|
|
132
|
-
};
|
|
133
|
-
// Track tokens
|
|
134
|
-
const existing = perModelTokens.get(member.model) ?? { input: 0, output: 0 };
|
|
135
|
-
existing.input += result.inputTokens;
|
|
136
|
-
existing.output += result.outputTokens;
|
|
137
|
-
perModelTokens.set(member.model, existing);
|
|
138
|
-
totalInput += result.inputTokens;
|
|
139
|
-
totalOutput += result.outputTokens;
|
|
140
|
-
return resp;
|
|
141
|
-
}
|
|
142
|
-
catch {
|
|
143
|
-
// Graceful degradation: return error marker
|
|
144
|
-
return {
|
|
145
|
-
round: 1,
|
|
146
|
-
model: member.model,
|
|
147
|
-
content: `[ERROR: ${member.model} failed to respond]`,
|
|
148
|
-
tokens: { input: 0, output: 0 },
|
|
149
|
-
durationMs: Date.now() - round1Start,
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
finally {
|
|
153
|
-
slot?.release();
|
|
154
|
-
}
|
|
155
|
-
});
|
|
156
|
-
allResponses.push(...panelResults);
|
|
157
|
-
// Primary model analyzes and decides
|
|
158
|
-
const panelSummary = allResponses
|
|
159
|
-
.map((r) => `### ${r.model}\n${r.content}`)
|
|
160
|
-
.join('\n\n---\n\n');
|
|
161
|
-
const analysisPrompt = [
|
|
162
|
-
'You are the primary decision-maker in a multi-model panel.',
|
|
163
|
-
'Below are responses from multiple models to the same question.',
|
|
164
|
-
'',
|
|
165
|
-
'Your task:',
|
|
166
|
-
'1. Analyze the responses — note agreements, contradictions, unique insights',
|
|
167
|
-
'2. Decide whether the answer is complete or needs another round',
|
|
168
|
-
'3. If complete, synthesize a comprehensive final answer',
|
|
169
|
-
'4. If not complete, specify what needs further exploration',
|
|
170
|
-
'',
|
|
171
|
-
'Original question:',
|
|
172
|
-
prompt,
|
|
173
|
-
'',
|
|
174
|
-
'Panel responses:',
|
|
175
|
-
panelSummary,
|
|
176
|
-
'',
|
|
177
|
-
'Respond in one of these formats:',
|
|
178
|
-
'',
|
|
179
|
-
'To FINALIZE (answer is complete):',
|
|
180
|
-
'FINALIZE',
|
|
181
|
-
'<your comprehensive synthesized answer>',
|
|
182
|
-
'',
|
|
183
|
-
'To CONTINUE (need another round):',
|
|
184
|
-
'CONTINUE',
|
|
185
|
-
'<refined question for the next round>',
|
|
186
|
-
'<specific aspects to explore>',
|
|
187
|
-
].join('\n');
|
|
188
|
-
const primaryResult = await singleModelCall(primaryApi, analysisPrompt, definition.primary?.systemPrompt, memberSignal(signal, definition.timeoutMs));
|
|
189
|
-
totalInput += primaryResult.inputTokens;
|
|
190
|
-
totalOutput += primaryResult.outputTokens;
|
|
191
|
-
const primaryModel = definition.primary.model;
|
|
192
|
-
const existing = perModelTokens.get(primaryModel) ?? { input: 0, output: 0 };
|
|
193
|
-
existing.input += primaryResult.inputTokens;
|
|
194
|
-
existing.output += primaryResult.outputTokens;
|
|
195
|
-
perModelTokens.set(primaryModel, existing);
|
|
196
|
-
// Parse primary decision
|
|
197
|
-
const isContinue = primaryResult.content.trim().startsWith('CONTINUE');
|
|
198
|
-
let finalAnswer;
|
|
199
|
-
if (isContinue) {
|
|
200
|
-
// Additional rounds — primary model continues to deliberate
|
|
201
|
-
const refinedQuestion = primaryResult.content.replace(/^CONTINUE\s*/i, '').trim();
|
|
202
|
-
let currentQuestion = refinedQuestion || prompt;
|
|
203
|
-
let converged = false;
|
|
204
|
-
// Carry the most recent round's responses forward for panel members.
|
|
205
|
-
let previousContext = panelSummary;
|
|
206
|
-
// Full deliberation history visible to the primary every round: all prior
|
|
207
|
-
// panel responses AND the primary's own prior analyses (plan: the primary
|
|
208
|
-
// accumulates full context across rounds to deliberate, not just round 1).
|
|
209
|
-
const deliberationLog = [
|
|
210
|
-
`## Round 1 — Panel responses\n${panelSummary}`,
|
|
211
|
-
`## Round 1 — Primary analysis\n${primaryResult.content}`,
|
|
212
|
-
];
|
|
213
|
-
const maxRounds = definition.maxRounds ?? 100;
|
|
214
|
-
while (!converged && rounds < maxRounds) { // Safety cap, but primary decides convergence
|
|
215
|
-
rounds++;
|
|
216
|
-
const roundStart = Date.now();
|
|
217
|
-
const roundResults = await mapWithConcurrency(definition.members, definition.maxParallel ?? definition.members.length, async (member, i) => {
|
|
218
|
-
let slot;
|
|
219
|
-
try {
|
|
220
|
-
slot = await pool.acquire(definition.timeoutMs);
|
|
221
|
-
const result = await singleModelCall(memberApis[i], `[Round ${rounds}] ${currentQuestion}\n\nPrevious round context:\n${previousContext}`, member.systemPrompt, memberSignal(signal, definition.timeoutMs));
|
|
222
|
-
const resp = {
|
|
223
|
-
round: rounds,
|
|
224
|
-
model: member.model,
|
|
225
|
-
content: result.content,
|
|
226
|
-
tokens: { input: result.inputTokens, output: result.outputTokens },
|
|
227
|
-
durationMs: Date.now() - roundStart,
|
|
228
|
-
};
|
|
229
|
-
const ex = perModelTokens.get(member.model) ?? { input: 0, output: 0 };
|
|
230
|
-
ex.input += result.inputTokens;
|
|
231
|
-
ex.output += result.outputTokens;
|
|
232
|
-
perModelTokens.set(member.model, ex);
|
|
233
|
-
totalInput += result.inputTokens;
|
|
234
|
-
totalOutput += result.outputTokens;
|
|
235
|
-
return resp;
|
|
236
|
-
}
|
|
237
|
-
catch {
|
|
238
|
-
return {
|
|
239
|
-
round: rounds,
|
|
240
|
-
model: member.model,
|
|
241
|
-
content: `[ERROR: ${member.model} failed]`,
|
|
242
|
-
tokens: { input: 0, output: 0 },
|
|
243
|
-
durationMs: Date.now() - roundStart,
|
|
244
|
-
};
|
|
245
|
-
}
|
|
246
|
-
finally {
|
|
247
|
-
slot?.release();
|
|
248
|
-
}
|
|
249
|
-
});
|
|
250
|
-
allResponses.push(...roundResults);
|
|
251
|
-
const roundSummary = roundResults
|
|
252
|
-
.map((r) => `### ${r.model}\n${r.content}`)
|
|
253
|
-
.join('\n\n---\n\n');
|
|
254
|
-
previousContext = roundSummary;
|
|
255
|
-
const followUpPrompt = [
|
|
256
|
-
'You are the primary decision-maker in a multi-model panel.',
|
|
257
|
-
`This is round ${rounds}. Use the full deliberation history below (all prior rounds' panel responses and your own prior analyses), then weigh this round's new responses.`,
|
|
258
|
-
'',
|
|
259
|
-
'Original question:',
|
|
260
|
-
prompt,
|
|
261
|
-
'',
|
|
262
|
-
'Full deliberation history:',
|
|
263
|
-
deliberationLog.join('\n\n---\n\n'),
|
|
264
|
-
'',
|
|
265
|
-
`Round ${rounds} new panel responses to "${currentQuestion}":`,
|
|
266
|
-
roundSummary,
|
|
267
|
-
'',
|
|
268
|
-
'Decide: FINALIZE (then give the comprehensive synthesized answer) or CONTINUE (then give a refined question and the specific aspects to explore).',
|
|
269
|
-
].join('\n');
|
|
270
|
-
const followUpResult = await singleModelCall(primaryApi, followUpPrompt, definition.primary?.systemPrompt, memberSignal(signal, definition.timeoutMs));
|
|
271
|
-
totalInput += followUpResult.inputTokens;
|
|
272
|
-
totalOutput += followUpResult.outputTokens;
|
|
273
|
-
const pex = perModelTokens.get(primaryModel) ?? { input: 0, output: 0 };
|
|
274
|
-
pex.input += followUpResult.inputTokens;
|
|
275
|
-
pex.output += followUpResult.outputTokens;
|
|
276
|
-
perModelTokens.set(primaryModel, pex);
|
|
277
|
-
if (followUpResult.content.trim().startsWith('FINALIZE')) {
|
|
278
|
-
finalAnswer = followUpResult.content.replace(/^FINALIZE\s*/i, '').trim();
|
|
279
|
-
converged = true;
|
|
280
|
-
}
|
|
281
|
-
else {
|
|
282
|
-
currentQuestion = followUpResult.content.replace(/^CONTINUE\s*/i, '').trim() || currentQuestion;
|
|
283
|
-
}
|
|
284
|
-
deliberationLog.push(`## Round ${rounds} — Panel responses\n${roundSummary}`);
|
|
285
|
-
deliberationLog.push(`## Round ${rounds} — Primary decision\n${followUpResult.content}`);
|
|
286
|
-
}
|
|
287
|
-
if (!converged) {
|
|
288
|
-
finalAnswer = primaryResult.content;
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
else {
|
|
292
|
-
finalAnswer = primaryResult.content.replace(/^FINALIZE\s*/i, '').trim();
|
|
293
|
-
}
|
|
294
|
-
const cost = computeCost([...modelSet], totalInput, totalOutput, perModelTokens);
|
|
295
|
-
return {
|
|
296
|
-
answer: finalAnswer,
|
|
297
|
-
mode: 'panel',
|
|
298
|
-
rounds,
|
|
299
|
-
panelResponses: allResponses,
|
|
300
|
-
cost,
|
|
301
|
-
durationMs: Date.now() - startedAt,
|
|
302
|
-
};
|
|
106
|
+
/** Read-only tool set for expert/reviewer agents (no write/edit/bash/delegation). */
|
|
107
|
+
async function buildReadOnlyExpertTools(cwd) {
|
|
108
|
+
const { createActoviqFileTools } = await import('../tools/actoviqFileTools.js');
|
|
109
|
+
const { createActoviqWebTools } = await import('../tools/actoviqWebTools.js');
|
|
110
|
+
const { createTavilySearchTool } = await import('../tools/tavilySearch.js');
|
|
111
|
+
const READ_ONLY_FILE_TOOLS = new Set(['Read', 'Glob', 'Grep']);
|
|
112
|
+
return [
|
|
113
|
+
...createActoviqFileTools({ cwd }).filter((t) => READ_ONLY_FILE_TOOLS.has(t.name)),
|
|
114
|
+
...createActoviqWebTools().filter((t) => t.name === 'WebFetch'),
|
|
115
|
+
createTavilySearchTool(),
|
|
116
|
+
];
|
|
303
117
|
}
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
118
|
+
/**
|
|
119
|
+
* A single read-only ReAct agent (Read/Glob/Grep + TavilySearch/WebFetch) that
|
|
120
|
+
* inspects the project and returns confirmed issues. The main agent (the
|
|
121
|
+
* "executor") invokes it as a tool, decides what `context` to inject — what it
|
|
122
|
+
* did and the results it obtained, placed in the reviewer's system prompt — and
|
|
123
|
+
* keeps final authority over the findings. The reviewer is told to scrutinize
|
|
124
|
+
* hard but confirm ONLY issues it can actually verify: no speculation, no
|
|
125
|
+
* fabricated or padded findings.
|
|
126
|
+
*/
|
|
127
|
+
async function runReviewerMode(task, definition, signal, context, workDir) {
|
|
308
128
|
const startedAt = Date.now();
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
'
|
|
321
|
-
'',
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
'',
|
|
326
|
-
|
|
327
|
-
prompt,
|
|
328
|
-
'',
|
|
329
|
-
'Return ONLY the specialist name (one word, lowercase).',
|
|
129
|
+
const reviewer = definition.reviewer;
|
|
130
|
+
// Lazy import avoids a circular dependency (agentClient imports modelTeam).
|
|
131
|
+
const { createAgentSdk } = await import('../runtime/agentClient.js');
|
|
132
|
+
const cwd = workDir ?? process.cwd();
|
|
133
|
+
const reviewerFraming = [
|
|
134
|
+
'You are a meticulous reviewer on a multi-model team.',
|
|
135
|
+
'Inspect the project using ONLY your read-only tools (Read/Glob/Grep) and the web',
|
|
136
|
+
'(TavilySearch/WebFetch). You cannot modify files, write code, or run commands.',
|
|
137
|
+
'Scrutinize as thoroughly as you can and surface every genuine problem — bugs, broken',
|
|
138
|
+
'logic, security holes, missed edge cases, violated requirements — each with concrete',
|
|
139
|
+
'file:line evidence. Critically: confirm ONLY issues you can actually verify from the',
|
|
140
|
+
'code or files. Do not speculate, invent, or pad the list; if something is uncertain,',
|
|
141
|
+
'say so explicitly instead of asserting it. If you find no real issues, say so plainly.',
|
|
142
|
+
].join(' ');
|
|
143
|
+
const systemPrompt = [
|
|
144
|
+
reviewerFraming,
|
|
145
|
+
context ? `\n## Context from the requesting agent (what it did and obtained)\n${context}` : '',
|
|
146
|
+
reviewer.systemPrompt ? `\n${reviewer.systemPrompt}` : '',
|
|
330
147
|
].filter(Boolean).join('\n');
|
|
331
|
-
const classifyResult = await singleModelCall(routerApi, classificationPrompt, definition.router?.systemPrompt, memberSignal(signal, definition.timeoutMs));
|
|
332
|
-
const specialistKey = classifyResult.content.trim().toLowerCase();
|
|
333
|
-
const validKeys = Object.keys(definition.specialists ?? {});
|
|
334
|
-
let chosenApi;
|
|
335
|
-
let chosenKey;
|
|
336
|
-
let chosenMember;
|
|
337
|
-
if (validKeys.includes(specialistKey)) {
|
|
338
|
-
chosenApi = specialistApis.get(specialistKey);
|
|
339
|
-
chosenKey = specialistKey;
|
|
340
|
-
chosenMember = definition.specialists[specialistKey];
|
|
341
|
-
}
|
|
342
|
-
else if (fallbackApi) {
|
|
343
|
-
chosenApi = fallbackApi;
|
|
344
|
-
chosenKey = 'fallback';
|
|
345
|
-
chosenMember = definition.fallback;
|
|
346
|
-
}
|
|
347
|
-
else {
|
|
348
|
-
// No fallback, pick first specialist
|
|
349
|
-
chosenKey = validKeys[0] ?? 'default';
|
|
350
|
-
chosenApi = specialistApis.get(chosenKey);
|
|
351
|
-
chosenMember = definition.specialists[chosenKey];
|
|
352
|
-
}
|
|
353
|
-
const result = await singleModelCall(chosenApi, prompt, chosenMember.systemPrompt, memberSignal(signal, definition.timeoutMs));
|
|
354
|
-
const modelSet = new Set();
|
|
355
|
-
modelSet.add(definition.router.model);
|
|
356
|
-
modelSet.add(chosenMember.model);
|
|
357
|
-
const perModelTokens = new Map();
|
|
358
|
-
perModelTokens.set(definition.router.model, { input: classifyResult.inputTokens, output: classifyResult.outputTokens });
|
|
359
|
-
perModelTokens.set(chosenMember.model, { input: result.inputTokens, output: result.outputTokens });
|
|
360
|
-
const cost = computeCost([...modelSet], classifyResult.inputTokens + result.inputTokens, classifyResult.outputTokens + result.outputTokens, perModelTokens);
|
|
361
|
-
return {
|
|
362
|
-
answer: result.content,
|
|
363
|
-
mode: 'router',
|
|
364
|
-
specialist: chosenKey,
|
|
365
|
-
classification: classifyResult.content,
|
|
366
|
-
cost,
|
|
367
|
-
durationMs: Date.now() - startedAt,
|
|
368
|
-
};
|
|
369
|
-
}
|
|
370
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
371
|
-
// Discussion Mode — roundtable for hard problems
|
|
372
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
373
|
-
async function runDiscussionMode(prompt, definition, signal) {
|
|
374
|
-
const startedAt = Date.now();
|
|
375
|
-
const primaryApi = await createMemberApi(definition.primary);
|
|
376
|
-
const memberApis = await Promise.all(definition.members.map((m) => createMemberApi(m)));
|
|
377
|
-
const facilitatorApi = definition.facilitator
|
|
378
|
-
? await createMemberApi(definition.facilitator)
|
|
379
|
-
: primaryApi; // fallback: primary acts as facilitator
|
|
380
|
-
let rounds = 0;
|
|
381
|
-
let converged = false;
|
|
382
|
-
let finalAnswer = '';
|
|
383
|
-
const facilitatorVerdicts = [];
|
|
384
|
-
const modelSet = new Set();
|
|
385
|
-
definition.members.forEach((m) => modelSet.add(m.model));
|
|
386
|
-
modelSet.add(definition.primary.model);
|
|
387
|
-
if (definition.facilitator)
|
|
388
|
-
modelSet.add(definition.facilitator.model);
|
|
389
148
|
const perModelTokens = new Map();
|
|
390
149
|
let totalInput = 0;
|
|
391
150
|
let totalOutput = 0;
|
|
392
|
-
let
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
const result = await singleModelCall(memberApis[i], speakPrompt, member.systemPrompt, memberSignal(signal, definition.timeoutMs));
|
|
411
|
-
discussionTranscript += `\n### ${member.model} (Round ${rounds})\n${result.content}\n`;
|
|
412
|
-
const ex = perModelTokens.get(member.model) ?? { input: 0, output: 0 };
|
|
413
|
-
ex.input += result.inputTokens;
|
|
414
|
-
ex.output += result.outputTokens;
|
|
415
|
-
perModelTokens.set(member.model, ex);
|
|
416
|
-
totalInput += result.inputTokens;
|
|
417
|
-
totalOutput += result.outputTokens;
|
|
418
|
-
}
|
|
419
|
-
// Facilitator subagent rules after the round
|
|
420
|
-
const facilitatorPrompt = [
|
|
421
|
-
'You are a discussion facilitator. Review the roundtable transcript and provide:',
|
|
422
|
-
'',
|
|
423
|
-
'1. **Summary**: Key points raised, areas of agreement and disagreement',
|
|
424
|
-
'2. **Progress Assessment**: Is the discussion converging? Are there blockers?',
|
|
425
|
-
'3. **Verdict**: Should discussion CONTINUE or FINALIZE?',
|
|
426
|
-
'',
|
|
427
|
-
'Respond in this format:',
|
|
428
|
-
'',
|
|
429
|
-
'SUMMARY:',
|
|
430
|
-
'<summary>',
|
|
431
|
-
'',
|
|
432
|
-
'VERDICT: CONTINUE|FINALIZE',
|
|
433
|
-
'',
|
|
434
|
-
'If CONTINUE, add:',
|
|
435
|
-
'NEXT_TOPIC: <what the next round should focus on>',
|
|
436
|
-
'',
|
|
437
|
-
'Discussion transcript:',
|
|
438
|
-
discussionTranscript,
|
|
439
|
-
].join('\n');
|
|
440
|
-
const facilitatorResult = await singleModelCall(facilitatorApi, facilitatorPrompt, definition.facilitator?.systemPrompt, memberSignal(signal, definition.timeoutMs));
|
|
441
|
-
if (definition.facilitator) {
|
|
442
|
-
const fex = perModelTokens.get(definition.facilitator.model) ?? { input: 0, output: 0 };
|
|
443
|
-
fex.input += facilitatorResult.inputTokens;
|
|
444
|
-
fex.output += facilitatorResult.outputTokens;
|
|
445
|
-
perModelTokens.set(definition.facilitator.model, fex);
|
|
446
|
-
}
|
|
447
|
-
totalInput += facilitatorResult.inputTokens;
|
|
448
|
-
totalOutput += facilitatorResult.outputTokens;
|
|
449
|
-
const summaryMatch = facilitatorResult.content.match(/SUMMARY:\s*\n([\s\S]*?)(?=\nVERDICT:|$)/i);
|
|
450
|
-
const verdictMatch = facilitatorResult.content.match(/VERDICT:\s*(CONTINUE|FINALIZE)/i);
|
|
451
|
-
const nextTopicMatch = facilitatorResult.content.match(/NEXT_TOPIC:\s*(.+)/i);
|
|
452
|
-
const verdict = verdictMatch?.[1]?.toUpperCase() === 'FINALIZE' ? 'finalize' : 'continue';
|
|
453
|
-
facilitatorVerdicts.push({
|
|
454
|
-
round: rounds,
|
|
455
|
-
summary: summaryMatch?.[1]?.trim() ?? facilitatorResult.content,
|
|
456
|
-
verdict,
|
|
151
|
+
let report;
|
|
152
|
+
let toolCalls = 0;
|
|
153
|
+
const pool = getGlobalAgentPool();
|
|
154
|
+
let slot;
|
|
155
|
+
let sdk;
|
|
156
|
+
try {
|
|
157
|
+
slot = await pool.acquire(definition.timeoutMs);
|
|
158
|
+
sdk = await createAgentSdk({
|
|
159
|
+
model: reviewer.model,
|
|
160
|
+
provider: reviewer.provider,
|
|
161
|
+
baseURL: reviewer.baseURL,
|
|
162
|
+
authToken: resolveApiKey(reviewer.apiKey),
|
|
163
|
+
maxTokens: reviewer.maxTokens ?? 32000,
|
|
164
|
+
workDir: cwd,
|
|
165
|
+
tools: await buildReadOnlyExpertTools(cwd),
|
|
166
|
+
permissionMode: 'bypassPermissions',
|
|
167
|
+
maxToolIterations: definition.maxIterations ?? 16,
|
|
168
|
+
systemPrompt,
|
|
457
169
|
});
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
discussionTranscript,
|
|
467
|
-
'',
|
|
468
|
-
'You may override the facilitator\'s recommendation. Decide:',
|
|
469
|
-
'- FINALIZE: synthesize the solution and deliver it',
|
|
470
|
-
'- CONTINUE: specify what the next round should address',
|
|
471
|
-
'',
|
|
472
|
-
'Respond with FINALIZE or CONTINUE followed by your reasoning/output.',
|
|
473
|
-
].join('\n');
|
|
474
|
-
const primaryResult = await singleModelCall(primaryApi, primaryDecisionPrompt, definition.primary?.systemPrompt, memberSignal(signal, definition.timeoutMs));
|
|
475
|
-
const pex = perModelTokens.get(definition.primary.model) ?? { input: 0, output: 0 };
|
|
476
|
-
pex.input += primaryResult.inputTokens;
|
|
477
|
-
pex.output += primaryResult.outputTokens;
|
|
478
|
-
perModelTokens.set(definition.primary.model, pex);
|
|
479
|
-
totalInput += primaryResult.inputTokens;
|
|
480
|
-
totalOutput += primaryResult.outputTokens;
|
|
481
|
-
if (primaryResult.content.trim().startsWith('FINALIZE')) {
|
|
482
|
-
finalAnswer = primaryResult.content.replace(/^FINALIZE\s*/i, '').trim();
|
|
483
|
-
converged = true;
|
|
484
|
-
}
|
|
485
|
-
else {
|
|
486
|
-
const nextTopic = nextTopicMatch?.[1] ?? primaryResult.content.replace(/^CONTINUE\s*/i, '').trim();
|
|
487
|
-
discussionTranscript += `\n### Facilitator (Round ${rounds})\nVerdict: CONTINUE\nNext topic: ${nextTopic}\n`;
|
|
488
|
-
}
|
|
170
|
+
const result = await sdk.run(task, { signal: memberSignal(signal, definition.timeoutMs) });
|
|
171
|
+
report = result.text;
|
|
172
|
+
toolCalls = result.toolCalls.length;
|
|
173
|
+
const input = result.requests.reduce((s, r) => s + (r.usage?.input_tokens ?? 0), 0);
|
|
174
|
+
const output = result.requests.reduce((s, r) => s + (r.usage?.output_tokens ?? 0), 0);
|
|
175
|
+
perModelTokens.set(reviewer.model, { input, output });
|
|
176
|
+
totalInput = input;
|
|
177
|
+
totalOutput = output;
|
|
489
178
|
}
|
|
490
|
-
|
|
491
|
-
|
|
179
|
+
catch (err) {
|
|
180
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
181
|
+
report = `[ERROR: reviewer ${reviewer.model} failed — ${message}]`;
|
|
492
182
|
}
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
rounds,
|
|
498
|
-
facilitatorVerdicts,
|
|
499
|
-
cost,
|
|
500
|
-
durationMs: Date.now() - startedAt,
|
|
501
|
-
};
|
|
502
|
-
}
|
|
503
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
504
|
-
// Executor-Reviewer Mode — advisory critique loop
|
|
505
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
506
|
-
async function runExecutorReviewerMode(prompt, definition, signal) {
|
|
507
|
-
const startedAt = Date.now();
|
|
508
|
-
const executorApi = await createMemberApi(definition.executor);
|
|
509
|
-
const reviewerApi = await createMemberApi(definition.reviewer);
|
|
510
|
-
const decisions = [];
|
|
511
|
-
const reviews = [];
|
|
512
|
-
const modelSet = new Set([definition.executor.model, definition.reviewer.model]);
|
|
513
|
-
const perModelTokens = new Map();
|
|
514
|
-
let totalInput = 0;
|
|
515
|
-
let totalOutput = 0;
|
|
516
|
-
let iterations = 0;
|
|
517
|
-
let converged = false;
|
|
518
|
-
let finalAnswer = '';
|
|
519
|
-
let currentOutput = '';
|
|
520
|
-
const maxIterations = definition.maxIterations ?? 100;
|
|
521
|
-
// Full history visible to both roles each iteration (plan: prevents the
|
|
522
|
-
// reviewer from repeating already-addressed or rejected suggestions).
|
|
523
|
-
const transcript = [];
|
|
524
|
-
// Iteration 1: Executor produces initial output
|
|
525
|
-
iterations++;
|
|
526
|
-
const execResult = await singleModelCall(executorApi, prompt, definition.executor?.systemPrompt, memberSignal(signal, definition.timeoutMs));
|
|
527
|
-
currentOutput = execResult.content;
|
|
528
|
-
transcript.push(`## Iteration 1 — Executor output\n${currentOutput}`);
|
|
529
|
-
perModelTokens.set(definition.executor.model, { input: execResult.inputTokens, output: execResult.outputTokens });
|
|
530
|
-
totalInput += execResult.inputTokens;
|
|
531
|
-
totalOutput += execResult.outputTokens;
|
|
532
|
-
while (!converged && iterations < maxIterations) {
|
|
533
|
-
// Reviewer critiques
|
|
534
|
-
const reviewPrompt = [
|
|
535
|
-
'You are an experienced reviewer providing constructive feedback.',
|
|
536
|
-
'The history below shows prior outputs, your earlier reviews, and the executor\'s decisions (including which suggestions were rejected and why). Do NOT repeat suggestions already addressed or explicitly rejected — focus on new, unaddressed improvements.',
|
|
537
|
-
'',
|
|
538
|
-
'Original request:',
|
|
539
|
-
prompt,
|
|
540
|
-
'',
|
|
541
|
-
'History so far:',
|
|
542
|
-
transcript.join('\n\n---\n\n'),
|
|
543
|
-
'',
|
|
544
|
-
'Latest output to review:',
|
|
545
|
-
currentOutput,
|
|
546
|
-
'',
|
|
547
|
-
'Provide specific, actionable feedback. You are an advisor — the executor makes final decisions.',
|
|
548
|
-
].join('\n');
|
|
549
|
-
const reviewResult = await singleModelCall(reviewerApi, reviewPrompt, definition.reviewer?.systemPrompt, memberSignal(signal, definition.timeoutMs));
|
|
550
|
-
reviews.push({ iteration: iterations, feedback: reviewResult.content });
|
|
551
|
-
transcript.push(`## Iteration ${iterations} — Reviewer feedback\n${reviewResult.content}`);
|
|
552
|
-
const rex = perModelTokens.get(definition.reviewer.model) ?? { input: 0, output: 0 };
|
|
553
|
-
rex.input += reviewResult.inputTokens;
|
|
554
|
-
rex.output += reviewResult.outputTokens;
|
|
555
|
-
perModelTokens.set(definition.reviewer.model, rex);
|
|
556
|
-
totalInput += reviewResult.inputTokens;
|
|
557
|
-
totalOutput += reviewResult.outputTokens;
|
|
558
|
-
// Executor decides: accept/reject/partial/finalize
|
|
559
|
-
const decisionPrompt = [
|
|
560
|
-
'You are the executor with final authority. Review the feedback and decide.',
|
|
561
|
-
'',
|
|
562
|
-
'Original request:',
|
|
563
|
-
prompt,
|
|
564
|
-
'',
|
|
565
|
-
'History so far (prior outputs, reviews, and your past decisions):',
|
|
566
|
-
transcript.join('\n\n---\n\n'),
|
|
567
|
-
'',
|
|
568
|
-
'Your current output:',
|
|
569
|
-
currentOutput,
|
|
570
|
-
'',
|
|
571
|
-
'Latest reviewer feedback:',
|
|
572
|
-
reviewResult.content,
|
|
573
|
-
'',
|
|
574
|
-
'Decide (respond with exactly one action keyword followed by explanation/output):',
|
|
575
|
-
'- ACCEPT: incorporate the feedback and provide revised output',
|
|
576
|
-
'- REJECT: explain why the suggestion is not accepted',
|
|
577
|
-
'- PARTIAL: accept some suggestions, reject others, provide revised output',
|
|
578
|
-
'- FINALIZE: the work is done, deliver final output',
|
|
579
|
-
'',
|
|
580
|
-
'Format: ACTION_KEYWORD',
|
|
581
|
-
'<explanation or revised output>',
|
|
582
|
-
].join('\n');
|
|
583
|
-
const decisionResult = await singleModelCall(executorApi, decisionPrompt, definition.executor?.systemPrompt, memberSignal(signal, definition.timeoutMs));
|
|
584
|
-
const dex = perModelTokens.get(definition.executor.model) ?? { input: 0, output: 0 };
|
|
585
|
-
dex.input += decisionResult.inputTokens;
|
|
586
|
-
dex.output += decisionResult.outputTokens;
|
|
587
|
-
perModelTokens.set(definition.executor.model, dex);
|
|
588
|
-
totalInput += decisionResult.inputTokens;
|
|
589
|
-
totalOutput += decisionResult.outputTokens;
|
|
590
|
-
// Parse decision
|
|
591
|
-
const lines = decisionResult.content.trim().split('\n');
|
|
592
|
-
const actionLine = lines[0]?.trim().toUpperCase() ?? '';
|
|
593
|
-
const explanation = lines.slice(1).join('\n').trim();
|
|
594
|
-
let action;
|
|
595
|
-
if (actionLine.startsWith('FINALIZE')) {
|
|
596
|
-
action = 'finalize';
|
|
597
|
-
finalAnswer = explanation || currentOutput;
|
|
598
|
-
converged = true;
|
|
599
|
-
}
|
|
600
|
-
else if (actionLine.startsWith('ACCEPT')) {
|
|
601
|
-
action = 'accept';
|
|
602
|
-
currentOutput = explanation || decisionResult.content;
|
|
603
|
-
}
|
|
604
|
-
else if (actionLine.startsWith('REJECT')) {
|
|
605
|
-
action = 'reject';
|
|
606
|
-
// Keep current output, note rejection
|
|
607
|
-
}
|
|
608
|
-
else if (actionLine.startsWith('PARTIAL')) {
|
|
609
|
-
action = 'partial';
|
|
610
|
-
currentOutput = explanation || decisionResult.content;
|
|
611
|
-
}
|
|
612
|
-
else {
|
|
613
|
-
// Default: treat as finalize
|
|
614
|
-
action = 'finalize';
|
|
615
|
-
finalAnswer = decisionResult.content;
|
|
616
|
-
converged = true;
|
|
617
|
-
}
|
|
618
|
-
decisions.push({
|
|
619
|
-
iteration: iterations,
|
|
620
|
-
action,
|
|
621
|
-
explanation: explanation || decisionResult.content,
|
|
622
|
-
});
|
|
623
|
-
transcript.push(`## Iteration ${iterations} — Executor decision (${action})\n${explanation || decisionResult.content}`);
|
|
624
|
-
iterations++;
|
|
625
|
-
}
|
|
626
|
-
if (!converged) {
|
|
627
|
-
finalAnswer = currentOutput;
|
|
183
|
+
finally {
|
|
184
|
+
if (sdk)
|
|
185
|
+
await sdk.close();
|
|
186
|
+
slot?.release();
|
|
628
187
|
}
|
|
629
|
-
const cost = computeCost([...
|
|
188
|
+
const cost = computeCost([...perModelTokens.keys()], totalInput, totalOutput, perModelTokens);
|
|
630
189
|
return {
|
|
631
|
-
answer:
|
|
632
|
-
mode: '
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
reviews,
|
|
190
|
+
answer: report,
|
|
191
|
+
mode: 'reviewer',
|
|
192
|
+
report,
|
|
193
|
+
toolCalls,
|
|
636
194
|
cost,
|
|
637
195
|
durationMs: Date.now() - startedAt,
|
|
638
196
|
};
|
|
639
197
|
}
|
|
640
198
|
// ═══════════════════════════════════════════════════════════════════
|
|
641
|
-
// Analysis Mode — read-only ReAct expert panel
|
|
199
|
+
// Panel-Analysis Mode — read-only ReAct expert panel + optional convergence
|
|
642
200
|
// ═══════════════════════════════════════════════════════════════════
|
|
643
201
|
/**
|
|
644
|
-
* Each member is an independent, read-only ReAct agent
|
|
645
|
-
* TavilySearch/WebFetch — no write/edit/bash/delegation) that
|
|
646
|
-
*
|
|
647
|
-
*
|
|
648
|
-
*
|
|
649
|
-
*
|
|
202
|
+
* Unified expert panel. Each member is an independent, read-only ReAct agent
|
|
203
|
+
* (Read/Glob/Grep + TavilySearch/WebFetch — no write/edit/bash/delegation) that
|
|
204
|
+
* investigates the task in parallel and returns a findings report.
|
|
205
|
+
*
|
|
206
|
+
* - No `primary` → single-pass advisory: the caller (the main agent) decides
|
|
207
|
+
* what to do with the concatenated reports (the original `analysis` behavior).
|
|
208
|
+
* - With a `primary` → multi-round convergence: after each round the primary
|
|
209
|
+
* synthesizes the findings and decides FINALIZE or CONTINUE; on CONTINUE the
|
|
210
|
+
* panel re-investigates a refined question with prior findings as context.
|
|
211
|
+
* Harness principle preserved: the primary decides convergence; `maxRounds`
|
|
212
|
+
* (default 100) is only a safety cap.
|
|
213
|
+
*
|
|
214
|
+
* `analysis` and `panel-analysis` both route here; the result `mode` echoes the
|
|
215
|
+
* requested alias.
|
|
650
216
|
*/
|
|
651
|
-
async function
|
|
217
|
+
async function runPanelAnalysisMode(prompt, definition, signal, workDir) {
|
|
652
218
|
const startedAt = Date.now();
|
|
653
219
|
// Lazy imports avoid a circular dependency (agentClient imports modelTeam).
|
|
654
220
|
const { createAgentSdk } = await import('../runtime/agentClient.js');
|
|
655
|
-
const
|
|
656
|
-
const
|
|
657
|
-
const { createTavilySearchTool } = await import('../tools/tavilySearch.js');
|
|
658
|
-
const cwd = process.cwd();
|
|
659
|
-
const READ_ONLY_FILE_TOOLS = new Set(['Read', 'Glob', 'Grep']);
|
|
660
|
-
const buildReadOnlyTools = () => [
|
|
661
|
-
...createActoviqFileTools({ cwd }).filter((t) => READ_ONLY_FILE_TOOLS.has(t.name)),
|
|
662
|
-
...createActoviqWebTools().filter((t) => t.name === 'WebFetch'),
|
|
663
|
-
createTavilySearchTool(),
|
|
664
|
-
];
|
|
221
|
+
const cwd = workDir ?? process.cwd();
|
|
222
|
+
const readOnlyTools = await buildReadOnlyExpertTools(cwd);
|
|
665
223
|
// Bounded ReAct depth per member so a panel can't run away (configurable).
|
|
666
224
|
const memberMaxIterations = definition.maxIterations ?? 16;
|
|
667
225
|
const pool = getGlobalAgentPool();
|
|
@@ -676,69 +234,167 @@ async function runAnalysisMode(prompt, definition, signal) {
|
|
|
676
234
|
'sources), risks, blind spots, concrete recommendations, and anything the main agent should',
|
|
677
235
|
'verify. Be specific and decision-useful.',
|
|
678
236
|
].join(' ');
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
:
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
237
|
+
// One investigation round: every member investigates `question` in parallel as
|
|
238
|
+
// a read-only ReAct agent. `priorContext` (earlier rounds' findings) lets
|
|
239
|
+
// members build on the deliberation instead of starting cold.
|
|
240
|
+
const investigate = async (round, question, priorContext) => {
|
|
241
|
+
const memberPrompt = priorContext
|
|
242
|
+
? `${question}\n\n## Prior panel findings (build on these; don't repeat established points)\n${priorContext}`
|
|
243
|
+
: question;
|
|
244
|
+
return mapWithConcurrency(definition.members, definition.maxParallel ?? definition.members.length, async (member) => {
|
|
245
|
+
const start = Date.now();
|
|
246
|
+
let slot;
|
|
247
|
+
let sdk;
|
|
248
|
+
try {
|
|
249
|
+
slot = await pool.acquire();
|
|
250
|
+
sdk = await createAgentSdk({
|
|
251
|
+
model: member.model,
|
|
252
|
+
provider: member.provider,
|
|
253
|
+
baseURL: member.baseURL,
|
|
254
|
+
authToken: resolveApiKey(member.apiKey),
|
|
255
|
+
maxTokens: member.maxTokens ?? 32000,
|
|
256
|
+
workDir: cwd,
|
|
257
|
+
tools: readOnlyTools,
|
|
258
|
+
permissionMode: 'bypassPermissions',
|
|
259
|
+
maxToolIterations: memberMaxIterations,
|
|
260
|
+
systemPrompt: member.systemPrompt
|
|
261
|
+
? `${analysisFraming}\n\n${member.systemPrompt}`
|
|
262
|
+
: analysisFraming,
|
|
263
|
+
});
|
|
264
|
+
const result = await sdk.run(memberPrompt, {
|
|
265
|
+
signal: memberSignal(signal, definition.timeoutMs),
|
|
266
|
+
});
|
|
267
|
+
const input = result.requests.reduce((s, r) => s + (r.usage?.input_tokens ?? 0), 0);
|
|
268
|
+
const output = result.requests.reduce((s, r) => s + (r.usage?.output_tokens ?? 0), 0);
|
|
269
|
+
const ex = perModelTokens.get(member.model) ?? { input: 0, output: 0 };
|
|
270
|
+
ex.input += input;
|
|
271
|
+
ex.output += output;
|
|
272
|
+
perModelTokens.set(member.model, ex);
|
|
273
|
+
totalInput += input;
|
|
274
|
+
totalOutput += output;
|
|
275
|
+
return {
|
|
276
|
+
model: member.model,
|
|
277
|
+
report: result.text,
|
|
278
|
+
toolCalls: result.toolCalls.length,
|
|
279
|
+
durationMs: Date.now() - start,
|
|
280
|
+
round,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
catch (err) {
|
|
284
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
285
|
+
return {
|
|
286
|
+
model: member.model,
|
|
287
|
+
report: `[ERROR: ${member.model} analysis failed — ${message}]`,
|
|
288
|
+
toolCalls: 0,
|
|
289
|
+
durationMs: Date.now() - start,
|
|
290
|
+
round,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
finally {
|
|
294
|
+
if (sdk)
|
|
295
|
+
await sdk.close();
|
|
296
|
+
slot?.release();
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
};
|
|
300
|
+
const resultMode = definition.mode === 'analysis' ? 'analysis' : 'panel-analysis';
|
|
301
|
+
// With a primary, build the decision callback that synthesizes findings and
|
|
302
|
+
// votes FINALIZE/CONTINUE each round; without one, the panel is single-pass.
|
|
303
|
+
let decide;
|
|
304
|
+
if (definition.primary) {
|
|
305
|
+
const primaryApi = await createMemberApi(definition.primary);
|
|
306
|
+
const primaryModel = definition.primary.model;
|
|
307
|
+
const primarySystem = definition.primary.systemPrompt;
|
|
308
|
+
decide = async (deliberationLog) => {
|
|
309
|
+
const decisionPrompt = [
|
|
310
|
+
'You are the primary synthesizer over a panel of expert analysts; each investigated the task with read-only tools and reported findings.',
|
|
311
|
+
'Review the full panel findings, then decide whether they are sufficient to answer the task.',
|
|
312
|
+
'',
|
|
313
|
+
'Original task:',
|
|
314
|
+
prompt,
|
|
315
|
+
'',
|
|
316
|
+
'Full panel findings so far:',
|
|
317
|
+
deliberationLog,
|
|
318
|
+
'',
|
|
319
|
+
'Respond in one of these formats:',
|
|
320
|
+
'',
|
|
321
|
+
'To FINALIZE (findings are sufficient):',
|
|
322
|
+
'FINALIZE',
|
|
323
|
+
'<your comprehensive synthesized answer, grounded in the findings>',
|
|
324
|
+
'',
|
|
325
|
+
'To CONTINUE (deeper investigation needed):',
|
|
326
|
+
'CONTINUE',
|
|
327
|
+
'<a refined question and the specific aspects the panel should investigate next>',
|
|
328
|
+
].join('\n');
|
|
329
|
+
const decision = await singleModelCall(primaryApi, decisionPrompt, primarySystem, memberSignal(signal, definition.timeoutMs));
|
|
330
|
+
totalInput += decision.inputTokens;
|
|
331
|
+
totalOutput += decision.outputTokens;
|
|
332
|
+
const pex = perModelTokens.get(primaryModel) ?? { input: 0, output: 0 };
|
|
333
|
+
pex.input += decision.inputTokens;
|
|
334
|
+
pex.output += decision.outputTokens;
|
|
335
|
+
perModelTokens.set(primaryModel, pex);
|
|
336
|
+
return decision.content;
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
const { answer, rounds, reports } = await orchestratePanel({
|
|
340
|
+
prompt,
|
|
341
|
+
maxRounds: definition.maxRounds ?? 100,
|
|
342
|
+
investigate,
|
|
343
|
+
decide,
|
|
731
344
|
});
|
|
732
|
-
const answer = reports.map((r) => `### ${r.model}\n${r.report}`).join('\n\n---\n\n');
|
|
733
345
|
const cost = computeCost([...perModelTokens.keys()], totalInput, totalOutput, perModelTokens);
|
|
734
346
|
return {
|
|
735
347
|
answer,
|
|
736
|
-
mode:
|
|
348
|
+
mode: resultMode,
|
|
737
349
|
reports,
|
|
350
|
+
rounds,
|
|
738
351
|
cost,
|
|
739
352
|
durationMs: Date.now() - startedAt,
|
|
740
353
|
};
|
|
741
354
|
}
|
|
355
|
+
/**
|
|
356
|
+
* Orchestrate the panel over an injectable `investigate` (one round of member
|
|
357
|
+
* reports) and optional `decide` (the primary's synthesize-or-continue call).
|
|
358
|
+
*
|
|
359
|
+
* - No `decide` → single-pass advisory: round 1 only; answer = labeled reports.
|
|
360
|
+
* - With `decide` → multi-round convergence: after each round the primary votes
|
|
361
|
+
* FINALIZE (synthesize) or CONTINUE (refined question); loops until FINALIZE
|
|
362
|
+
* or `maxRounds` (safety cap). The keyword match is case-insensitive, and any
|
|
363
|
+
* decision that is not a leading CONTINUE finalizes (graceful default).
|
|
364
|
+
*
|
|
365
|
+
* Exported so the convergence logic is unit-testable without real model calls.
|
|
366
|
+
*/
|
|
367
|
+
export async function orchestratePanel(opts) {
|
|
368
|
+
const label = (rs) => rs.map((r) => `### ${r.model}\n${r.report}`).join('\n\n---\n\n');
|
|
369
|
+
const allReports = [];
|
|
370
|
+
let rounds = 1;
|
|
371
|
+
let currentReports = await opts.investigate(1, opts.prompt);
|
|
372
|
+
allReports.push(...currentReports);
|
|
373
|
+
// No primary → single-pass advisory (original `analysis` behavior).
|
|
374
|
+
if (!opts.decide) {
|
|
375
|
+
return { answer: label(currentReports), rounds, reports: allReports };
|
|
376
|
+
}
|
|
377
|
+
const deliberationLog = [`## Round 1 — Panel findings\n${label(currentReports)}`];
|
|
378
|
+
while (true) {
|
|
379
|
+
const content = await opts.decide(deliberationLog.join('\n\n---\n\n'), rounds);
|
|
380
|
+
const wantsContinue = content.trim().toUpperCase().startsWith('CONTINUE');
|
|
381
|
+
if (wantsContinue && rounds < opts.maxRounds) {
|
|
382
|
+
const refined = content.replace(/^CONTINUE\s*/i, '').trim() || opts.prompt;
|
|
383
|
+
deliberationLog.push(`## Round ${rounds} — Primary decision\nCONTINUE: ${refined}`);
|
|
384
|
+
rounds++;
|
|
385
|
+
currentReports = await opts.investigate(rounds, refined, label(currentReports));
|
|
386
|
+
allReports.push(...currentReports);
|
|
387
|
+
deliberationLog.push(`## Round ${rounds} — Panel findings\n${label(currentReports)}`);
|
|
388
|
+
}
|
|
389
|
+
else {
|
|
390
|
+
// FINALIZE, or the safety cap was reached mid-deliberation.
|
|
391
|
+
const answer = wantsContinue
|
|
392
|
+
? label(currentReports) // cap hit: hand back the findings unsynthesized
|
|
393
|
+
: content.replace(/^FINALIZE\s*/i, '').trim() || label(currentReports);
|
|
394
|
+
return { answer, rounds, reports: allReports };
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
742
398
|
// ═══════════════════════════════════════════════════════════════════
|
|
743
399
|
// ModelTeam class
|
|
744
400
|
// ═══════════════════════════════════════════════════════════════════
|
|
@@ -750,23 +406,19 @@ export class ModelTeam {
|
|
|
750
406
|
this.definition = definition;
|
|
751
407
|
}
|
|
752
408
|
/**
|
|
753
|
-
* Ask the team
|
|
754
|
-
*
|
|
755
|
-
*
|
|
756
|
-
* Executor-Reviewer: executor decides when done.
|
|
409
|
+
* Ask the team. `opts.context` is injected into the reviewer's system prompt
|
|
410
|
+
* (reviewer mode only — what the main agent did and obtained). Returns the
|
|
411
|
+
* mode-specific result.
|
|
757
412
|
*/
|
|
758
|
-
async ask(prompt, signal) {
|
|
413
|
+
async ask(prompt, signal, opts) {
|
|
759
414
|
switch (this.definition.mode) {
|
|
760
|
-
case '
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
case 'discussion':
|
|
765
|
-
return runDiscussionMode(prompt, this.definition, signal);
|
|
766
|
-
case 'executor-reviewer':
|
|
767
|
-
return runExecutorReviewerMode(prompt, this.definition, signal);
|
|
415
|
+
case 'reviewer':
|
|
416
|
+
case 'executor-reviewer': // alias: the retired executor loop now runs the single reviewer
|
|
417
|
+
return runReviewerMode(prompt, this.definition, signal, opts?.context, opts?.workDir);
|
|
418
|
+
case 'panel': // alias: retired pure-text panel now runs the unified engine
|
|
768
419
|
case 'analysis':
|
|
769
|
-
|
|
420
|
+
case 'panel-analysis':
|
|
421
|
+
return runPanelAnalysisMode(prompt, this.definition, signal, opts?.workDir);
|
|
770
422
|
default:
|
|
771
423
|
throw new Error(`Unknown team mode: ${this.definition.mode}`);
|
|
772
424
|
}
|
|
@@ -786,35 +438,18 @@ function validateTeamDefinition(def) {
|
|
|
786
438
|
if (!def.mode)
|
|
787
439
|
throw new Error('Team definition must specify a mode.');
|
|
788
440
|
switch (def.mode) {
|
|
789
|
-
case '
|
|
790
|
-
|
|
791
|
-
throw new Error('Panel mode requires a primary member.');
|
|
792
|
-
if (!def.members || def.members.length === 0)
|
|
793
|
-
throw new Error('Panel mode requires at least one panel member.');
|
|
794
|
-
if (def.members.length > 8)
|
|
795
|
-
throw new Error('Panel mode supports at most 8 members.');
|
|
796
|
-
break;
|
|
797
|
-
case 'router':
|
|
798
|
-
if (!def.router)
|
|
799
|
-
throw new Error('Router mode requires a router member.');
|
|
800
|
-
if (!def.specialists || Object.keys(def.specialists).length === 0)
|
|
801
|
-
throw new Error('Router mode requires at least one specialist.');
|
|
802
|
-
break;
|
|
803
|
-
case 'discussion':
|
|
804
|
-
if (!def.primary)
|
|
805
|
-
throw new Error('Discussion mode requires a primary member.');
|
|
806
|
-
if (!def.members || def.members.length < 2)
|
|
807
|
-
throw new Error('Discussion mode requires at least 2 members.');
|
|
808
|
-
break;
|
|
809
|
-
case 'executor-reviewer':
|
|
810
|
-
if (!def.executor)
|
|
811
|
-
throw new Error('Executor-Reviewer mode requires an executor.');
|
|
441
|
+
case 'reviewer':
|
|
442
|
+
case 'executor-reviewer': // retired alias: needs only a reviewer now
|
|
812
443
|
if (!def.reviewer)
|
|
813
|
-
throw new Error('
|
|
444
|
+
throw new Error('Reviewer mode requires a reviewer member.');
|
|
814
445
|
break;
|
|
446
|
+
case 'panel': // retired alias of panel-analysis (primary now optional)
|
|
815
447
|
case 'analysis':
|
|
448
|
+
case 'panel-analysis':
|
|
816
449
|
if (!def.members || def.members.length === 0)
|
|
817
|
-
throw new Error('
|
|
450
|
+
throw new Error('Panel-analysis mode requires at least one panel member.');
|
|
451
|
+
if (def.members.length > 8)
|
|
452
|
+
throw new Error('Panel-analysis mode supports at most 8 members.');
|
|
818
453
|
break;
|
|
819
454
|
}
|
|
820
455
|
}
|
|
@@ -824,31 +459,55 @@ function validateTeamDefinition(def) {
|
|
|
824
459
|
*/
|
|
825
460
|
export function createTeamTool(definition) {
|
|
826
461
|
const team = createModelTeam(definition);
|
|
462
|
+
const isReviewer = definition.mode === 'reviewer' || definition.mode === 'executor-reviewer';
|
|
463
|
+
const isPanel = definition.mode === 'analysis' || definition.mode === 'panel-analysis';
|
|
827
464
|
return {
|
|
828
465
|
kind: 'local',
|
|
829
466
|
name: definition.name,
|
|
830
467
|
description: definition.description ??
|
|
831
|
-
(
|
|
832
|
-
? '
|
|
833
|
-
:
|
|
468
|
+
(isReviewer
|
|
469
|
+
? 'Reviewer: a single read-only agent inspects the project and reports only genuine, verifiable issues. Pass { task } (what to check) and optional { context } (what you did + the results). It advises; you decide.'
|
|
470
|
+
: isPanel
|
|
471
|
+
? 'Expert panel: independent read-only multi-model analysis (advisory; optional primary-driven convergence). You decide what to do with the findings.'
|
|
472
|
+
: `Multi-model team (${definition.mode} mode)`),
|
|
834
473
|
inputSchema: {
|
|
835
474
|
parse: (input) => input,
|
|
836
475
|
_type: undefined,
|
|
837
476
|
},
|
|
838
|
-
inputJsonSchema:
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
477
|
+
inputJsonSchema: isReviewer
|
|
478
|
+
? {
|
|
479
|
+
type: 'object',
|
|
480
|
+
properties: {
|
|
481
|
+
task: { type: 'string', description: 'What the reviewer should scrutinize' },
|
|
482
|
+
context: { type: 'string', description: 'What you did and the results you obtained (injected into the reviewer system prompt)' },
|
|
483
|
+
},
|
|
484
|
+
required: ['task'],
|
|
485
|
+
additionalProperties: true,
|
|
486
|
+
}
|
|
487
|
+
: {
|
|
488
|
+
type: 'object',
|
|
489
|
+
properties: {
|
|
490
|
+
prompt: { type: 'string', description: 'The question or task for the team' },
|
|
491
|
+
},
|
|
492
|
+
required: ['prompt'],
|
|
493
|
+
// Tolerate extra fields the model may add — a strict schema here caused
|
|
494
|
+
// intermittent "schema error" rejections that silently skipped the panel.
|
|
495
|
+
additionalProperties: true,
|
|
842
496
|
},
|
|
843
|
-
required: ['prompt'],
|
|
844
|
-
// Tolerate extra fields the model may add — a strict schema here caused
|
|
845
|
-
// intermittent "schema error" rejections that silently skipped the panel.
|
|
846
|
-
additionalProperties: true,
|
|
847
|
-
},
|
|
848
497
|
async execute(input) {
|
|
849
|
-
// Accept the
|
|
850
|
-
//
|
|
498
|
+
// Accept the call however the model phrases it (bare string or any of a few
|
|
499
|
+
// common keys) so a formatting quirk never drops the invocation.
|
|
851
500
|
const obj = (input ?? {});
|
|
501
|
+
if (isReviewer) {
|
|
502
|
+
const taskCandidate = typeof input === 'string' ? input : obj.task ?? obj.prompt ?? obj.question ?? obj.input;
|
|
503
|
+
const task = typeof taskCandidate === 'string' ? taskCandidate.trim() : '';
|
|
504
|
+
if (!task) {
|
|
505
|
+
throw new Error(`Reviewer "${definition.name}" requires a non-empty "task" string.`);
|
|
506
|
+
}
|
|
507
|
+
const context = typeof obj.context === 'string' ? obj.context : undefined;
|
|
508
|
+
const result = await team.ask(task, undefined, { context });
|
|
509
|
+
return JSON.stringify({ answer: result.answer, mode: result.mode, cost: result.cost });
|
|
510
|
+
}
|
|
852
511
|
const candidate = typeof input === 'string'
|
|
853
512
|
? input
|
|
854
513
|
: obj.prompt ?? obj.query ?? obj.question ?? obj.task ?? obj.input;
|
|
@@ -869,10 +528,14 @@ export function createTeamTool(definition) {
|
|
|
869
528
|
`## ${definition.name} (Model Team: ${definition.mode})`,
|
|
870
529
|
definition.description ?? '',
|
|
871
530
|
'',
|
|
872
|
-
|
|
873
|
-
? 'Call this tool with
|
|
874
|
-
:
|
|
875
|
-
|
|
531
|
+
isReviewer
|
|
532
|
+
? 'Call this tool with { task } (what to scrutinize) and optional { context } (what you did and the results you got). A single read-only agent inspects the code/web and reports only issues it can verify — no speculation. You keep final authority; re-invoke after changes to re-check.'
|
|
533
|
+
: isPanel
|
|
534
|
+
? 'Call this tool with a { prompt } to have an expert panel of independent read-only agents investigate (read local code + search the web) and each return a findings report. They only analyze and advise — you keep full control and decide what to do with their input. With a configured primary the panel also converges over multiple rounds into a synthesized answer. Use it to assist analysis on large or complex tasks.'
|
|
535
|
+
: 'Call this tool with a { prompt } to get a multi-model synthesized answer.',
|
|
536
|
+
isReviewer
|
|
537
|
+
? `Reviewer: ${definition.reviewer?.model ?? 'configured via definition'}`
|
|
538
|
+
: `Members: ${definition.members?.map((m) => m.model).join(', ') ?? 'configured via definition'}`,
|
|
876
539
|
].join('\n'),
|
|
877
540
|
};
|
|
878
541
|
}
|