actoviq-agent-sdk 0.4.0 → 0.4.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.
Files changed (87) hide show
  1. package/README-zh.md +10 -1
  2. package/README.md +15 -6
  3. package/dist/src/cli/actoviq-react.js +178 -3
  4. package/dist/src/cli/actoviq-react.js.map +1 -1
  5. package/dist/src/cli/actoviq-tui.js +2 -2
  6. package/dist/src/index.d.ts +15 -0
  7. package/dist/src/index.d.ts.map +1 -1
  8. package/dist/src/index.js +15 -0
  9. package/dist/src/index.js.map +1 -1
  10. package/dist/src/runtime/actoviqSkills.d.ts +6 -0
  11. package/dist/src/runtime/actoviqSkills.d.ts.map +1 -1
  12. package/dist/src/runtime/actoviqSkills.js +69 -1
  13. package/dist/src/runtime/actoviqSkills.js.map +1 -1
  14. package/dist/src/runtime/agentClient.d.ts +14 -0
  15. package/dist/src/runtime/agentClient.d.ts.map +1 -1
  16. package/dist/src/runtime/agentClient.js +84 -8
  17. package/dist/src/runtime/agentClient.js.map +1 -1
  18. package/dist/src/runtime/conversationEngine.d.ts +2 -0
  19. package/dist/src/runtime/conversationEngine.d.ts.map +1 -1
  20. package/dist/src/runtime/conversationEngine.js +6 -5
  21. package/dist/src/runtime/conversationEngine.js.map +1 -1
  22. package/dist/src/storage/sessionStore.d.ts.map +1 -1
  23. package/dist/src/storage/sessionStore.js +25 -6
  24. package/dist/src/storage/sessionStore.js.map +1 -1
  25. package/dist/src/team/agentPool.d.ts +26 -0
  26. package/dist/src/team/agentPool.d.ts.map +1 -0
  27. package/dist/src/team/agentPool.js +97 -0
  28. package/dist/src/team/agentPool.js.map +1 -0
  29. package/dist/src/team/modelTeam.d.ts +29 -0
  30. package/dist/src/team/modelTeam.d.ts.map +1 -0
  31. package/dist/src/team/modelTeam.js +879 -0
  32. package/dist/src/team/modelTeam.js.map +1 -0
  33. package/dist/src/team/pricing.d.ts +15 -0
  34. package/dist/src/team/pricing.d.ts.map +1 -0
  35. package/dist/src/team/pricing.js +80 -0
  36. package/dist/src/team/pricing.js.map +1 -0
  37. package/dist/src/team/teamDefinitions.d.ts +28 -0
  38. package/dist/src/team/teamDefinitions.d.ts.map +1 -0
  39. package/dist/src/team/teamDefinitions.js +162 -0
  40. package/dist/src/team/teamDefinitions.js.map +1 -0
  41. package/dist/src/tools/actoviqCoreTools.d.ts.map +1 -1
  42. package/dist/src/tools/actoviqCoreTools.js +6 -1
  43. package/dist/src/tools/actoviqCoreTools.js.map +1 -1
  44. package/dist/src/tools/enterWorktree.d.ts +11 -0
  45. package/dist/src/tools/enterWorktree.d.ts.map +1 -0
  46. package/dist/src/tools/enterWorktree.js +79 -0
  47. package/dist/src/tools/enterWorktree.js.map +1 -0
  48. package/dist/src/tools/exitWorktree.d.ts +4 -0
  49. package/dist/src/tools/exitWorktree.d.ts.map +1 -0
  50. package/dist/src/tools/exitWorktree.js +48 -0
  51. package/dist/src/tools/exitWorktree.js.map +1 -0
  52. package/dist/src/tools/tavilySearch.d.ts +12 -0
  53. package/dist/src/tools/tavilySearch.d.ts.map +1 -0
  54. package/dist/src/tools/tavilySearch.js +139 -0
  55. package/dist/src/tools/tavilySearch.js.map +1 -0
  56. package/dist/src/tui/actoviqTui.d.ts +10 -0
  57. package/dist/src/tui/actoviqTui.d.ts.map +1 -1
  58. package/dist/src/tui/actoviqTui.js +533 -34
  59. package/dist/src/tui/actoviqTui.js.map +1 -1
  60. package/dist/src/tui/editor.d.ts +2 -0
  61. package/dist/src/tui/editor.d.ts.map +1 -1
  62. package/dist/src/tui/editor.js +5 -0
  63. package/dist/src/tui/editor.js.map +1 -1
  64. package/dist/src/types.d.ts +309 -0
  65. package/dist/src/types.d.ts.map +1 -1
  66. package/dist/src/types.js.map +1 -1
  67. package/dist/src/workflow/workflowPersistence.d.ts +34 -0
  68. package/dist/src/workflow/workflowPersistence.d.ts.map +1 -0
  69. package/dist/src/workflow/workflowPersistence.js +144 -0
  70. package/dist/src/workflow/workflowPersistence.js.map +1 -0
  71. package/dist/src/workflow/workflowScriptRuntime.d.ts +22 -0
  72. package/dist/src/workflow/workflowScriptRuntime.d.ts.map +1 -0
  73. package/dist/src/workflow/workflowScriptRuntime.js +655 -0
  74. package/dist/src/workflow/workflowScriptRuntime.js.map +1 -0
  75. package/dist/src/worktree/worktreeHooks.d.ts +38 -0
  76. package/dist/src/worktree/worktreeHooks.d.ts.map +1 -0
  77. package/dist/src/worktree/worktreeHooks.js +82 -0
  78. package/dist/src/worktree/worktreeHooks.js.map +1 -0
  79. package/dist/src/worktree/worktreeInclude.d.ts +11 -0
  80. package/dist/src/worktree/worktreeInclude.d.ts.map +1 -0
  81. package/dist/src/worktree/worktreeInclude.js +65 -0
  82. package/dist/src/worktree/worktreeInclude.js.map +1 -0
  83. package/dist/src/worktree/worktreeService.d.ts +43 -0
  84. package/dist/src/worktree/worktreeService.d.ts.map +1 -0
  85. package/dist/src/worktree/worktreeService.js +356 -0
  86. package/dist/src/worktree/worktreeService.js.map +1 -0
  87. package/package.json +2 -24
@@ -0,0 +1,879 @@
1
+ import { getGlobalAgentPool } from './agentPool.js';
2
+ import { estimateCost, hasFullPricing } from './pricing.js';
3
+ import { resolveRuntimeConfig } from '../config/resolveRuntimeConfig.js';
4
+ import { createActoviqModelApi } from '../runtime/actoviqModelApi.js';
5
+ import { createOpenaiModelApi } from '../provider/openai-model-api.js';
6
+ // ═══════════════════════════════════════════════════════════════════
7
+ // Per-member ModelApi instantiation
8
+ // ═══════════════════════════════════════════════════════════════════
9
+ function resolveApiKey(apiKey) {
10
+ if (!apiKey)
11
+ return undefined;
12
+ if (apiKey.startsWith('$')) {
13
+ const varName = apiKey.slice(1);
14
+ return process.env[varName];
15
+ }
16
+ return apiKey;
17
+ }
18
+ async function createMemberApi(member) {
19
+ const resolved = await resolveRuntimeConfig({
20
+ model: member.model,
21
+ provider: member.provider,
22
+ baseURL: member.baseURL,
23
+ authToken: resolveApiKey(member.apiKey),
24
+ maxTokens: member.maxTokens ?? 32000,
25
+ workDir: process.cwd(),
26
+ });
27
+ const api = resolved.provider === 'openai'
28
+ ? createOpenaiModelApi(resolved)
29
+ : createActoviqModelApi(resolved);
30
+ return { api, model: resolved.model, maxTokens: member.maxTokens ?? 32000 };
31
+ }
32
+ // ═══════════════════════════════════════════════════════════════════
33
+ // Helpers
34
+ // ═══════════════════════════════════════════════════════════════════
35
+ async function singleModelCall(memberApi, prompt, systemPrompt, signal) {
36
+ const startedAt = Date.now();
37
+ const messages = [{ role: 'user', content: prompt }];
38
+ const response = await memberApi.api.createMessage({
39
+ model: memberApi.model,
40
+ messages,
41
+ max_tokens: memberApi.maxTokens,
42
+ system: systemPrompt,
43
+ signal,
44
+ });
45
+ const text = response.content
46
+ .filter((b) => b.type === 'text')
47
+ .map((b) => b.text)
48
+ .join('');
49
+ return {
50
+ content: text,
51
+ inputTokens: response.usage?.input_tokens ?? 0,
52
+ outputTokens: response.usage?.output_tokens ?? 0,
53
+ durationMs: Date.now() - startedAt,
54
+ };
55
+ }
56
+ function computeCost(models, inputTokens, outputTokens, perModelTokens, homeDir) {
57
+ const costWarning = !hasFullPricing(models, homeDir);
58
+ let totalEstimatedCost = 0;
59
+ const breakdown = [];
60
+ for (const [model, tokens] of perModelTokens) {
61
+ const cost = estimateCost(model, tokens.input, tokens.output, homeDir);
62
+ if (cost !== null) {
63
+ breakdown.push({ model, cost });
64
+ totalEstimatedCost = (totalEstimatedCost ?? 0) + cost;
65
+ }
66
+ else {
67
+ totalEstimatedCost = null; // null if any model lacks pricing
68
+ }
69
+ }
70
+ return {
71
+ totalInputTokens: inputTokens,
72
+ totalOutputTokens: outputTokens,
73
+ estimatedCost: totalEstimatedCost,
74
+ breakdown,
75
+ costWarning,
76
+ };
77
+ }
78
+ // ═══════════════════════════════════════════════════════════════════
79
+ // Concurrency + timeout helpers
80
+ // ═══════════════════════════════════════════════════════════════════
81
+ /** Combine the caller's abort signal with a per-call timeout (if set). */
82
+ function memberSignal(signal, timeoutMs) {
83
+ if (!timeoutMs || timeoutMs <= 0)
84
+ return signal;
85
+ const signals = [signal, AbortSignal.timeout(timeoutMs)].filter((s) => s != null);
86
+ return AbortSignal.any(signals);
87
+ }
88
+ /** Run fn over items with at most `limit` in flight; preserves input order. */
89
+ async function mapWithConcurrency(items, limit, fn) {
90
+ const results = new Array(items.length);
91
+ let next = 0;
92
+ const workers = Array.from({ length: Math.max(1, Math.min(limit, items.length)) }, async () => {
93
+ while (true) {
94
+ const i = next++;
95
+ if (i >= items.length)
96
+ return;
97
+ results[i] = await fn(items[i], i);
98
+ }
99
+ });
100
+ await Promise.all(workers);
101
+ return results;
102
+ }
103
+ // ═══════════════════════════════════════════════════════════════════
104
+ // Panel Mode — multi-round autonomous deliberation
105
+ // ═══════════════════════════════════════════════════════════════════
106
+ async function runPanelMode(prompt, definition, signal) {
107
+ const startedAt = Date.now();
108
+ const primaryApi = await createMemberApi(definition.primary);
109
+ const memberApis = await Promise.all(definition.members.map((m) => createMemberApi(m)));
110
+ let rounds = 0;
111
+ const allResponses = [];
112
+ const modelSet = new Set(definition.members.map((m) => m.model));
113
+ modelSet.add(definition.primary.model);
114
+ const perModelTokens = new Map();
115
+ let totalInput = 0;
116
+ let totalOutput = 0;
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
+ };
303
+ }
304
+ // ═══════════════════════════════════════════════════════════════════
305
+ // Router Mode — user-configured dispatch
306
+ // ═══════════════════════════════════════════════════════════════════
307
+ async function runRouterMode(prompt, definition, signal) {
308
+ const startedAt = Date.now();
309
+ const routerApi = await createMemberApi(definition.router);
310
+ const specialistApis = new Map();
311
+ for (const [key, spec] of Object.entries(definition.specialists ?? {})) {
312
+ specialistApis.set(key, await createMemberApi(spec));
313
+ }
314
+ const fallbackApi = definition.fallback ? await createMemberApi(definition.fallback) : null;
315
+ // Build classification prompt
316
+ const specialistDescriptions = Object.entries(definition.specialists ?? {})
317
+ .map(([key, spec]) => `- ${key}: ${spec.description ?? 'General tasks'}`)
318
+ .join('\n');
319
+ const classificationPrompt = definition.classificationPrompt ?? [
320
+ 'You are a task classifier. Based on the user\'s request, determine which specialist should handle it.',
321
+ '',
322
+ 'Available specialists:',
323
+ specialistDescriptions,
324
+ definition.fallback ? `- fallback: ${definition.fallback.description ?? 'General assistance'}` : null,
325
+ '',
326
+ 'User request:',
327
+ prompt,
328
+ '',
329
+ 'Return ONLY the specialist name (one word, lowercase).',
330
+ ].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
+ const perModelTokens = new Map();
390
+ let totalInput = 0;
391
+ let totalOutput = 0;
392
+ let discussionTranscript = `# Discussion Topic\n${prompt}\n\n`;
393
+ const maxRounds = definition.maxRounds ?? 100;
394
+ while (!converged && rounds < maxRounds) {
395
+ rounds++;
396
+ // Sequential speaking: each member sees prior speakers
397
+ for (let i = 0; i < definition.members.length; i++) {
398
+ const member = definition.members[i];
399
+ const speakPrompt = [
400
+ `## Discussion Round ${rounds}`,
401
+ '',
402
+ `Topic: ${prompt}`,
403
+ '',
404
+ 'Previous discussion:',
405
+ discussionTranscript,
406
+ '',
407
+ `You are ${member.model}. Please contribute your perspective. ` +
408
+ 'Consider what previous speakers have said. Build on agreements, address disagreements.',
409
+ ].join('\n');
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,
457
+ });
458
+ // Primary model makes final decision
459
+ const primaryDecisionPrompt = [
460
+ 'As the primary decision-maker, review the facilitator\'s report and the full discussion.',
461
+ '',
462
+ 'Facilitator recommendation:',
463
+ facilitatorResult.content,
464
+ '',
465
+ 'Full discussion transcript:',
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
+ }
489
+ }
490
+ if (!converged) {
491
+ finalAnswer = discussionTranscript;
492
+ }
493
+ const cost = computeCost([...modelSet], totalInput, totalOutput, perModelTokens);
494
+ return {
495
+ answer: finalAnswer,
496
+ mode: 'discussion',
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;
628
+ }
629
+ const cost = computeCost([...modelSet], totalInput, totalOutput, perModelTokens);
630
+ return {
631
+ answer: finalAnswer,
632
+ mode: 'executor-reviewer',
633
+ iterations,
634
+ decisions,
635
+ reviews,
636
+ cost,
637
+ durationMs: Date.now() - startedAt,
638
+ };
639
+ }
640
+ // ═══════════════════════════════════════════════════════════════════
641
+ // Analysis Mode — read-only ReAct expert panel (advisory)
642
+ // ═══════════════════════════════════════════════════════════════════
643
+ /**
644
+ * Each member is an independent, read-only ReAct agent (Read/Glob/Grep +
645
+ * TavilySearch/WebFetch — no write/edit/bash/delegation) that investigates the
646
+ * task and the project in parallel and returns a findings report. No synthesis,
647
+ * no convergence: the caller (the main agent) decides what to do with the
648
+ * reports. This is the autonomous "expert panel" the main model invokes as a
649
+ * tool to assist analysis on large/complex tasks; it only analyzes and reports.
650
+ */
651
+ async function runAnalysisMode(prompt, definition, signal) {
652
+ const startedAt = Date.now();
653
+ // Lazy imports avoid a circular dependency (agentClient imports modelTeam).
654
+ const { createAgentSdk } = await import('../runtime/agentClient.js');
655
+ const { createActoviqFileTools } = await import('../tools/actoviqFileTools.js');
656
+ const { createActoviqWebTools } = await import('../tools/actoviqWebTools.js');
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
+ ];
665
+ // Bounded ReAct depth per member so a panel can't run away (configurable).
666
+ const memberMaxIterations = definition.maxIterations ?? 16;
667
+ const pool = getGlobalAgentPool();
668
+ const perModelTokens = new Map();
669
+ let totalInput = 0;
670
+ let totalOutput = 0;
671
+ const analysisFraming = [
672
+ 'You are an analyst on a multi-model expert panel.',
673
+ 'Investigate the task using ONLY your read-only tools: read local files (Read/Glob/Grep)',
674
+ 'and research the web (TavilySearch/WebFetch). You cannot modify files, write code, or run',
675
+ 'commands — you only analyze and advise. Produce a focused findings report: key facts (with',
676
+ 'sources), risks, blind spots, concrete recommendations, and anything the main agent should',
677
+ 'verify. Be specific and decision-useful.',
678
+ ].join(' ');
679
+ const reports = await mapWithConcurrency(definition.members, definition.maxParallel ?? definition.members.length, async (member) => {
680
+ const start = Date.now();
681
+ let slot;
682
+ let sdk;
683
+ try {
684
+ slot = await pool.acquire();
685
+ sdk = await createAgentSdk({
686
+ model: member.model,
687
+ provider: member.provider,
688
+ baseURL: member.baseURL,
689
+ authToken: resolveApiKey(member.apiKey),
690
+ maxTokens: member.maxTokens ?? 32000,
691
+ workDir: cwd,
692
+ tools: buildReadOnlyTools(),
693
+ permissionMode: 'bypassPermissions',
694
+ maxToolIterations: memberMaxIterations,
695
+ systemPrompt: member.systemPrompt
696
+ ? `${analysisFraming}\n\n${member.systemPrompt}`
697
+ : analysisFraming,
698
+ });
699
+ const result = await sdk.run(prompt, {
700
+ signal: memberSignal(signal, definition.timeoutMs),
701
+ });
702
+ const input = result.requests.reduce((s, r) => s + (r.usage?.input_tokens ?? 0), 0);
703
+ const output = result.requests.reduce((s, r) => s + (r.usage?.output_tokens ?? 0), 0);
704
+ const ex = perModelTokens.get(member.model) ?? { input: 0, output: 0 };
705
+ ex.input += input;
706
+ ex.output += output;
707
+ perModelTokens.set(member.model, ex);
708
+ totalInput += input;
709
+ totalOutput += output;
710
+ return {
711
+ model: member.model,
712
+ report: result.text,
713
+ toolCalls: result.toolCalls.length,
714
+ durationMs: Date.now() - start,
715
+ };
716
+ }
717
+ catch (err) {
718
+ const message = err instanceof Error ? err.message : String(err);
719
+ return {
720
+ model: member.model,
721
+ report: `[ERROR: ${member.model} analysis failed — ${message}]`,
722
+ toolCalls: 0,
723
+ durationMs: Date.now() - start,
724
+ };
725
+ }
726
+ finally {
727
+ if (sdk)
728
+ await sdk.close();
729
+ slot?.release();
730
+ }
731
+ });
732
+ const answer = reports.map((r) => `### ${r.model}\n${r.report}`).join('\n\n---\n\n');
733
+ const cost = computeCost([...perModelTokens.keys()], totalInput, totalOutput, perModelTokens);
734
+ return {
735
+ answer,
736
+ mode: 'analysis',
737
+ reports,
738
+ cost,
739
+ durationMs: Date.now() - startedAt,
740
+ };
741
+ }
742
+ // ═══════════════════════════════════════════════════════════════════
743
+ // ModelTeam class
744
+ // ═══════════════════════════════════════════════════════════════════
745
+ export class ModelTeam {
746
+ name;
747
+ definition;
748
+ constructor(definition) {
749
+ this.name = definition.name;
750
+ this.definition = definition;
751
+ }
752
+ /**
753
+ * Ask the team a question. Returns the synthesized answer.
754
+ * Panel/Discussion: primary model decides when to converge.
755
+ * Router: classifies and dispatches.
756
+ * Executor-Reviewer: executor decides when done.
757
+ */
758
+ async ask(prompt, signal) {
759
+ switch (this.definition.mode) {
760
+ case 'panel':
761
+ return runPanelMode(prompt, this.definition, signal);
762
+ case 'router':
763
+ return runRouterMode(prompt, this.definition, signal);
764
+ case 'discussion':
765
+ return runDiscussionMode(prompt, this.definition, signal);
766
+ case 'executor-reviewer':
767
+ return runExecutorReviewerMode(prompt, this.definition, signal);
768
+ case 'analysis':
769
+ return runAnalysisMode(prompt, this.definition, signal);
770
+ default:
771
+ throw new Error(`Unknown team mode: ${this.definition.mode}`);
772
+ }
773
+ }
774
+ }
775
+ /**
776
+ * Create a ModelTeam from a definition or from disk.
777
+ */
778
+ export function createModelTeam(definition) {
779
+ // Validate
780
+ validateTeamDefinition(definition);
781
+ return new ModelTeam(definition);
782
+ }
783
+ function validateTeamDefinition(def) {
784
+ if (!def.name)
785
+ throw new Error('Team definition must have a name.');
786
+ if (!def.mode)
787
+ throw new Error('Team definition must specify a mode.');
788
+ switch (def.mode) {
789
+ case 'panel':
790
+ if (!def.primary)
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.');
812
+ if (!def.reviewer)
813
+ throw new Error('Executor-Reviewer mode requires a reviewer.');
814
+ break;
815
+ case 'analysis':
816
+ if (!def.members || def.members.length === 0)
817
+ throw new Error('Analysis mode requires at least one panel member.');
818
+ break;
819
+ }
820
+ }
821
+ /**
822
+ * Create a team tool that agents can invoke.
823
+ * Returns an AgentToolDefinition with interruptBehavior: 'block'.
824
+ */
825
+ export function createTeamTool(definition) {
826
+ const team = createModelTeam(definition);
827
+ return {
828
+ kind: 'local',
829
+ name: definition.name,
830
+ description: definition.description ??
831
+ (definition.mode === 'analysis'
832
+ ? 'Expert panel: independent read-only multi-model analysis (advisory). You decide what to do with the findings.'
833
+ : `Multi-model team (${definition.mode} mode)`),
834
+ inputSchema: {
835
+ parse: (input) => input,
836
+ _type: undefined,
837
+ },
838
+ inputJsonSchema: {
839
+ type: 'object',
840
+ properties: {
841
+ prompt: { type: 'string', description: 'The question or task for the team' },
842
+ },
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
+ async execute(input) {
849
+ // Accept the prompt however the model phrases the call (bare string or any
850
+ // of a few common keys) so a formatting quirk never drops the panel.
851
+ const obj = (input ?? {});
852
+ const candidate = typeof input === 'string'
853
+ ? input
854
+ : obj.prompt ?? obj.query ?? obj.question ?? obj.task ?? obj.input;
855
+ const prompt = typeof candidate === 'string' ? candidate.trim() : '';
856
+ if (!prompt) {
857
+ throw new Error(`Team "${definition.name}" requires a non-empty "prompt" string.`);
858
+ }
859
+ const result = await team.ask(prompt);
860
+ return JSON.stringify({
861
+ answer: result.answer,
862
+ mode: result.mode,
863
+ cost: result.cost,
864
+ });
865
+ },
866
+ interruptBehavior: 'block',
867
+ isConcurrencySafe: () => false,
868
+ prompt: async () => [
869
+ `## ${definition.name} (Model Team: ${definition.mode})`,
870
+ definition.description ?? '',
871
+ '',
872
+ definition.mode === 'analysis'
873
+ ? '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. Use it to assist analysis on large or complex tasks.'
874
+ : 'Call this tool with a { prompt } to get a multi-model synthesized answer.',
875
+ `Members: ${definition.members?.map((m) => m.model).join(', ') ?? 'configured via definition'}`,
876
+ ].join('\n'),
877
+ };
878
+ }
879
+ //# sourceMappingURL=modelTeam.js.map