@open-multi-agent/core 1.5.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -32
- package/dist/agent/pool.d.ts +1 -1
- package/dist/agent/pool.d.ts.map +1 -1
- package/dist/agent/pool.js +23 -1
- package/dist/agent/pool.js.map +1 -1
- package/dist/agent/runner.d.ts.map +1 -1
- package/dist/agent/runner.js +37 -7
- package/dist/agent/runner.js.map +1 -1
- package/dist/cli/oma.d.ts.map +1 -1
- package/dist/cli/oma.js +3 -1
- package/dist/cli/oma.js.map +1 -1
- package/dist/errors.d.ts +10 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +13 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/llm/adapter.d.ts +3 -1
- package/dist/llm/adapter.d.ts.map +1 -1
- package/dist/llm/adapter.js +6 -0
- package/dist/llm/adapter.js.map +1 -1
- package/dist/llm/ai-sdk.d.ts.map +1 -1
- package/dist/llm/ai-sdk.js +3 -0
- package/dist/llm/ai-sdk.js.map +1 -1
- package/dist/llm/anthropic.d.ts.map +1 -1
- package/dist/llm/anthropic.js +3 -0
- package/dist/llm/anthropic.js.map +1 -1
- package/dist/llm/azure-openai.d.ts.map +1 -1
- package/dist/llm/azure-openai.js +3 -0
- package/dist/llm/azure-openai.js.map +1 -1
- package/dist/llm/bedrock.d.ts.map +1 -1
- package/dist/llm/bedrock.js +3 -0
- package/dist/llm/bedrock.js.map +1 -1
- package/dist/llm/copilot.d.ts.map +1 -1
- package/dist/llm/copilot.js +3 -0
- package/dist/llm/copilot.js.map +1 -1
- package/dist/llm/gemini.d.ts.map +1 -1
- package/dist/llm/gemini.js +3 -0
- package/dist/llm/gemini.js.map +1 -1
- package/dist/llm/hunyuan.d.ts +40 -0
- package/dist/llm/hunyuan.d.ts.map +1 -0
- package/dist/llm/hunyuan.js +59 -0
- package/dist/llm/hunyuan.js.map +1 -0
- package/dist/llm/minimax.d.ts +2 -2
- package/dist/llm/minimax.js +2 -2
- package/dist/llm/openai-common.d.ts +7 -0
- package/dist/llm/openai-common.d.ts.map +1 -1
- package/dist/llm/openai-common.js +26 -1
- package/dist/llm/openai-common.js.map +1 -1
- package/dist/llm/openai.d.ts.map +1 -1
- package/dist/llm/openai.js +7 -2
- package/dist/llm/openai.js.map +1 -1
- package/dist/llm/validate.d.ts +26 -0
- package/dist/llm/validate.d.ts.map +1 -0
- package/dist/llm/validate.js +55 -0
- package/dist/llm/validate.js.map +1 -0
- package/dist/memory/shared.d.ts +14 -7
- package/dist/memory/shared.d.ts.map +1 -1
- package/dist/memory/shared.js +97 -14
- package/dist/memory/shared.js.map +1 -1
- package/dist/orchestrator/orchestrator.d.ts +35 -4
- package/dist/orchestrator/orchestrator.d.ts.map +1 -1
- package/dist/orchestrator/orchestrator.js +587 -47
- package/dist/orchestrator/orchestrator.js.map +1 -1
- package/dist/task/task.d.ts +4 -1
- package/dist/task/task.d.ts.map +1 -1
- package/dist/task/task.js +3 -0
- package/dist/task/task.js.map +1 -1
- package/dist/types.d.ts +165 -4
- package/dist/types.d.ts.map +1 -1
- package/package.json +7 -3
|
@@ -49,7 +49,8 @@ import { registerBuiltInTools } from '../tool/built-in/index.js';
|
|
|
49
49
|
import { defaultWorkspaceDir } from '../tool/built-in/path-safety.js';
|
|
50
50
|
import { Team } from '../team/team.js';
|
|
51
51
|
import { TaskQueue } from '../task/queue.js';
|
|
52
|
-
import { createTask } from '../task/task.js';
|
|
52
|
+
import { createTask, validateTaskDependencies } from '../task/task.js';
|
|
53
|
+
import { extractJSON, validateOutput } from '../agent/structured-output.js';
|
|
53
54
|
import { Scheduler } from './scheduler.js';
|
|
54
55
|
import { TokenBudgetExceededError } from '../errors.js';
|
|
55
56
|
import { extractKeywords, keywordScore } from '../utils/keywords.js';
|
|
@@ -195,6 +196,23 @@ function buildAgent(config, toolRegistration) {
|
|
|
195
196
|
});
|
|
196
197
|
return new Agent(config, registry, executor);
|
|
197
198
|
}
|
|
199
|
+
/**
|
|
200
|
+
* Apply the orchestrator's {@link OrchestratorConfig.defaultToolPreset} as a
|
|
201
|
+
* fallback grant for an agent that declares neither `tools` nor `toolPreset`.
|
|
202
|
+
*
|
|
203
|
+
* Built-in tools are opt-in (default-deny): an agent with no grant resolves to
|
|
204
|
+
* zero built-in tools. This fills that gap when the orchestrator opts in to a
|
|
205
|
+
* default. Per-agent grants always win — the default never widens an agent that
|
|
206
|
+
* already declares `tools` or `toolPreset`.
|
|
207
|
+
*/
|
|
208
|
+
function applyDefaultToolPreset(config, defaultToolPreset) {
|
|
209
|
+
if (defaultToolPreset === undefined
|
|
210
|
+
|| config.tools !== undefined
|
|
211
|
+
|| config.toolPreset !== undefined) {
|
|
212
|
+
return config;
|
|
213
|
+
}
|
|
214
|
+
return { ...config, toolPreset: defaultToolPreset };
|
|
215
|
+
}
|
|
198
216
|
/** Promise-based delay. */
|
|
199
217
|
function sleep(ms) {
|
|
200
218
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -314,6 +332,10 @@ function parseTaskSpecs(raw) {
|
|
|
314
332
|
maxRetries: typeof obj['maxRetries'] === 'number' ? obj['maxRetries'] : undefined,
|
|
315
333
|
retryDelayMs: typeof obj['retryDelayMs'] === 'number' ? obj['retryDelayMs'] : undefined,
|
|
316
334
|
retryBackoff: typeof obj['retryBackoff'] === 'number' ? obj['retryBackoff'] : undefined,
|
|
335
|
+
role: typeof obj['role'] === 'string' ? obj['role'] : undefined,
|
|
336
|
+
priority: obj['priority'] === 'low' || obj['priority'] === 'normal' || obj['priority'] === 'high' || obj['priority'] === 'critical'
|
|
337
|
+
? obj['priority']
|
|
338
|
+
: undefined,
|
|
317
339
|
});
|
|
318
340
|
}
|
|
319
341
|
return specs.length > 0 ? specs : null;
|
|
@@ -322,6 +344,47 @@ function parseTaskSpecs(raw) {
|
|
|
322
344
|
return null;
|
|
323
345
|
}
|
|
324
346
|
}
|
|
347
|
+
function routeMatches(policy, selection) {
|
|
348
|
+
if (!policy)
|
|
349
|
+
return undefined;
|
|
350
|
+
const task = selection.task;
|
|
351
|
+
for (const rule of policy.rules) {
|
|
352
|
+
const match = rule.match;
|
|
353
|
+
if (match.phase !== undefined && match.phase !== selection.phase)
|
|
354
|
+
continue;
|
|
355
|
+
if (match.agent !== undefined && match.agent !== selection.agent)
|
|
356
|
+
continue;
|
|
357
|
+
if (match.taskRole !== undefined && match.taskRole !== task?.role)
|
|
358
|
+
continue;
|
|
359
|
+
if (match.taskPriority !== undefined && match.taskPriority !== task?.priority)
|
|
360
|
+
continue;
|
|
361
|
+
if (match.leaf !== undefined && match.leaf !== selection.leaf)
|
|
362
|
+
continue;
|
|
363
|
+
if (match.hasDependencies !== undefined && match.hasDependencies !== ((task?.dependsOn?.length ?? 0) > 0))
|
|
364
|
+
continue;
|
|
365
|
+
return rule.route;
|
|
366
|
+
}
|
|
367
|
+
return undefined;
|
|
368
|
+
}
|
|
369
|
+
function withModelRoute(config, route) {
|
|
370
|
+
if (!route)
|
|
371
|
+
return config;
|
|
372
|
+
return {
|
|
373
|
+
...config,
|
|
374
|
+
model: route.model,
|
|
375
|
+
provider: route.provider ?? config.provider,
|
|
376
|
+
baseURL: route.baseURL ?? config.baseURL,
|
|
377
|
+
apiKey: route.apiKey ?? config.apiKey,
|
|
378
|
+
region: route.region ?? config.region,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
function isLeafTask(task, tasks) {
|
|
382
|
+
for (const candidate of tasks) {
|
|
383
|
+
if (candidate.dependsOn?.includes(task.id))
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
386
|
+
return true;
|
|
387
|
+
}
|
|
325
388
|
function buildRevealCoordinatorLines(revealContext, assignee) {
|
|
326
389
|
return [
|
|
327
390
|
'## Team context',
|
|
@@ -376,13 +439,19 @@ function buildTaskAgentTeamInfo(ctx, taskId, traceBase, delegationDepth, delegat
|
|
|
376
439
|
}
|
|
377
440
|
// Apply orchestrator-level defaults just like buildPool, then construct a
|
|
378
441
|
// one-shot Agent for this delegation only.
|
|
379
|
-
const
|
|
442
|
+
const route = routeMatches(ctx.modelRouting, {
|
|
443
|
+
phase: 'delegated',
|
|
444
|
+
agent: targetAgent,
|
|
445
|
+
task: ctx.taskById.get(taskId),
|
|
446
|
+
leaf: ctx.taskLeafById.get(taskId),
|
|
447
|
+
});
|
|
448
|
+
const effective = withModelRoute(applyDefaultToolPreset({
|
|
380
449
|
...targetConfig,
|
|
381
450
|
provider: targetConfig.provider ?? ctx.config.defaultProvider,
|
|
382
451
|
baseURL: targetConfig.baseURL ?? ctx.config.defaultBaseURL,
|
|
383
452
|
apiKey: targetConfig.apiKey ?? ctx.config.defaultApiKey,
|
|
384
453
|
cwd: targetConfig.cwd === undefined ? ctx.config.defaultCwd : targetConfig.cwd,
|
|
385
|
-
};
|
|
454
|
+
}, ctx.config.defaultToolPreset), route);
|
|
386
455
|
const tempAgent = buildAgent(effective, { includeDelegateTool: true });
|
|
387
456
|
const nestedTeam = buildTaskAgentTeamInfo(ctx, taskId, traceBase, delegationDepth + 1, [...delegationChain, targetAgent]);
|
|
388
457
|
const childOpts = {
|
|
@@ -461,6 +530,18 @@ async function executeQueue(queue, ctx) {
|
|
|
461
530
|
});
|
|
462
531
|
return;
|
|
463
532
|
}
|
|
533
|
+
const agentConfig = team.getAgent(assignee);
|
|
534
|
+
if (!agentConfig) {
|
|
535
|
+
const msg = `Agent "${assignee}" not found in team for task "${task.title}".`;
|
|
536
|
+
queue.fail(task.id, msg);
|
|
537
|
+
config.onProgress?.({
|
|
538
|
+
type: 'error',
|
|
539
|
+
task: task.id,
|
|
540
|
+
agent: assignee,
|
|
541
|
+
data: msg,
|
|
542
|
+
});
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
464
545
|
const agent = pool.get(assignee);
|
|
465
546
|
if (!agent) {
|
|
466
547
|
const msg = `Agent "${assignee}" not found in pool for task "${task.title}".`;
|
|
@@ -503,9 +584,22 @@ async function executeQueue(queue, ctx) {
|
|
|
503
584
|
...traceBase,
|
|
504
585
|
team: buildTaskAgentTeamInfo(ctx, task.id, traceBase, 0, [assignee]),
|
|
505
586
|
};
|
|
506
|
-
const
|
|
507
|
-
|
|
508
|
-
|
|
587
|
+
const workerRoute = routeMatches(ctx.modelRouting, {
|
|
588
|
+
phase: 'worker',
|
|
589
|
+
agent: assignee,
|
|
590
|
+
task,
|
|
591
|
+
leaf: ctx.taskLeafById.get(task.id),
|
|
592
|
+
});
|
|
593
|
+
const routedAgent = workerRoute
|
|
594
|
+
? buildAgent(withModelRoute(applyDefaultToolPreset({
|
|
595
|
+
...agentConfig,
|
|
596
|
+
provider: agentConfig.provider ?? config.defaultProvider,
|
|
597
|
+
baseURL: agentConfig.baseURL ?? config.defaultBaseURL,
|
|
598
|
+
apiKey: agentConfig.apiKey ?? config.defaultApiKey,
|
|
599
|
+
cwd: agentConfig.cwd === undefined ? config.defaultCwd : agentConfig.cwd,
|
|
600
|
+
}, config.defaultToolPreset), workerRoute), { includeDelegateTool: true })
|
|
601
|
+
: undefined;
|
|
602
|
+
const streamCallback = config.onAgentStream
|
|
509
603
|
? (event) => {
|
|
510
604
|
if (config.onTrace) {
|
|
511
605
|
const streamMs = Date.now();
|
|
@@ -522,7 +616,12 @@ async function executeQueue(queue, ctx) {
|
|
|
522
616
|
}
|
|
523
617
|
config.onAgentStream(assignee, event);
|
|
524
618
|
}
|
|
525
|
-
: undefined
|
|
619
|
+
: undefined;
|
|
620
|
+
const taskStartMs = Date.now();
|
|
621
|
+
let retryCount = 0;
|
|
622
|
+
const result = await executeWithRetry(() => routedAgent
|
|
623
|
+
? pool.runEphemeral(routedAgent, prompt, runOptions, streamCallback)
|
|
624
|
+
: pool.run(assignee, prompt, runOptions, streamCallback), task, (retryData) => {
|
|
526
625
|
retryCount++;
|
|
527
626
|
config.onProgress?.({
|
|
528
627
|
type: 'task_retry',
|
|
@@ -571,27 +670,37 @@ async function executeQueue(queue, ctx) {
|
|
|
571
670
|
});
|
|
572
671
|
}
|
|
573
672
|
if (result.success) {
|
|
574
|
-
// Persist result into shared memory so other agents can read it
|
|
575
673
|
const sharedMem = team.getSharedMemoryInstance();
|
|
674
|
+
// Opt-in consensus verification runs *before* the task is finalised so the
|
|
675
|
+
// verified outcome (accepted → revised, rejected → original) flows into the
|
|
676
|
+
// queue, shared memory, progress events, and agentResults as one consistent
|
|
677
|
+
// result. Judge usage is charged to the same parent budget as the rest of the run.
|
|
678
|
+
let effective = result;
|
|
679
|
+
if (task.verify && !ctx.budgetExceededTriggered) {
|
|
680
|
+
effective = await runTaskVerify(task, assignee, result, sharedMem, ctx);
|
|
681
|
+
}
|
|
682
|
+
// Reflect the verified result in the per-task record the caller receives.
|
|
683
|
+
ctx.agentResults.set(`${assignee}:${task.id}`, effective);
|
|
684
|
+
// Persist result into shared memory so other agents can read it
|
|
576
685
|
if (sharedMem) {
|
|
577
|
-
await sharedMem.write(assignee, `task:${task.id}:result`,
|
|
686
|
+
await sharedMem.write(assignee, `task:${task.id}:result`, effective.output);
|
|
578
687
|
// Advance the turn counter so any TTL-tagged entries written during
|
|
579
688
|
// this task can be expired by subsequent reads.
|
|
580
689
|
sharedMem.advanceTurn();
|
|
581
690
|
}
|
|
582
|
-
const completedTask = queue.complete(task.id,
|
|
691
|
+
const completedTask = queue.complete(task.id, effective.output);
|
|
583
692
|
completedThisRound.push(completedTask);
|
|
584
693
|
config.onProgress?.({
|
|
585
694
|
type: 'task_complete',
|
|
586
695
|
task: task.id,
|
|
587
696
|
agent: assignee,
|
|
588
|
-
data:
|
|
697
|
+
data: effective,
|
|
589
698
|
});
|
|
590
699
|
config.onProgress?.({
|
|
591
700
|
type: 'agent_complete',
|
|
592
701
|
agent: assignee,
|
|
593
702
|
task: task.id,
|
|
594
|
-
data:
|
|
703
|
+
data: effective,
|
|
595
704
|
});
|
|
596
705
|
}
|
|
597
706
|
else {
|
|
@@ -691,6 +800,251 @@ async function buildTaskPrompt(task, team, queue, revealContext) {
|
|
|
691
800
|
}
|
|
692
801
|
return lines.join('\n');
|
|
693
802
|
}
|
|
803
|
+
/** Skeptic framing applied to every judge (refute mode and lens-mode base). */
|
|
804
|
+
const DEFAULT_VERIFIER_INSTRUCTION = 'You are a rigorous skeptic reviewing a proposed answer to the question shown below. ' +
|
|
805
|
+
'Judge the answer against what that question actually asks: hunt for errors, unsupported ' +
|
|
806
|
+
'claims, gaps, and faulty reasoning, then decide whether it withstands scrutiny.';
|
|
807
|
+
/** Per-judge review angles used in `lens` mode (assigned round-robin by index). */
|
|
808
|
+
const CONSENSUS_LENSES = [
|
|
809
|
+
'factual correctness and logical soundness',
|
|
810
|
+
'completeness and coverage of the question',
|
|
811
|
+
'edge cases, failure modes, and counterexamples',
|
|
812
|
+
'clarity, precision, and freedom from ambiguity',
|
|
813
|
+
'hidden assumptions and unstated premises',
|
|
814
|
+
'evidence, citations, and verifiability',
|
|
815
|
+
];
|
|
816
|
+
/** Verdict contract appended to every judge prompt. */
|
|
817
|
+
const VERDICT_INSTRUCTION = 'Respond ONLY with a JSON object {"accept": <true|false>, "critique": "<concise reason>"}. ' +
|
|
818
|
+
'Set "accept" to true only if the answer withstands scrutiny; otherwise set it false ' +
|
|
819
|
+
'and explain the problem in "critique".';
|
|
820
|
+
/** Apply orchestrator defaults to a consensus agent config, mirroring buildPool. */
|
|
821
|
+
function applyConsensusDefaults(config, defaults) {
|
|
822
|
+
return {
|
|
823
|
+
...config,
|
|
824
|
+
provider: config.provider ?? defaults.defaultProvider,
|
|
825
|
+
baseURL: config.baseURL ?? defaults.defaultBaseURL,
|
|
826
|
+
apiKey: config.apiKey ?? defaults.defaultApiKey,
|
|
827
|
+
cwd: config.cwd === undefined ? defaults.defaultCwd : config.cwd,
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
/** Build the user prompt sent to a single judge, always including the original question. */
|
|
831
|
+
function buildJudgePrompt(p) {
|
|
832
|
+
let instruction;
|
|
833
|
+
if (p.judgePrompt !== undefined) {
|
|
834
|
+
instruction = typeof p.judgePrompt === 'function' ? p.judgePrompt(p.judge) : p.judgePrompt;
|
|
835
|
+
}
|
|
836
|
+
else if (p.mode === 'lens') {
|
|
837
|
+
const lens = CONSENSUS_LENSES[p.judgeIndex % CONSENSUS_LENSES.length];
|
|
838
|
+
instruction = `${DEFAULT_VERIFIER_INSTRUCTION}\nFocus specifically on: ${lens}. ` +
|
|
839
|
+
'If that angle is irrelevant to this question, accept the answer rather than inventing objections.';
|
|
840
|
+
}
|
|
841
|
+
else {
|
|
842
|
+
instruction = DEFAULT_VERIFIER_INSTRUCTION;
|
|
843
|
+
}
|
|
844
|
+
return [
|
|
845
|
+
instruction,
|
|
846
|
+
'',
|
|
847
|
+
'## Question',
|
|
848
|
+
p.prompt,
|
|
849
|
+
'',
|
|
850
|
+
'## Proposed answer',
|
|
851
|
+
p.answer,
|
|
852
|
+
'',
|
|
853
|
+
'## Your verdict',
|
|
854
|
+
VERDICT_INSTRUCTION,
|
|
855
|
+
].join('\n');
|
|
856
|
+
}
|
|
857
|
+
/** Build the proposer prompt for a revision round, feeding back the prior answer and the dissent. */
|
|
858
|
+
function buildRevisePrompt(prompt, answer, dissent) {
|
|
859
|
+
return [
|
|
860
|
+
prompt,
|
|
861
|
+
'',
|
|
862
|
+
'## Your previous answer',
|
|
863
|
+
answer,
|
|
864
|
+
'',
|
|
865
|
+
'## Reviewer critiques to address',
|
|
866
|
+
...dissent.map((d) => `- ${d}`),
|
|
867
|
+
'',
|
|
868
|
+
'Revise the previous answer to address every critique above. Respond with the improved answer only.',
|
|
869
|
+
].join('\n');
|
|
870
|
+
}
|
|
871
|
+
/** Parse a judge's raw output into an accept/critique decision. */
|
|
872
|
+
function parseJudgeVerdict(output, verdictSchema) {
|
|
873
|
+
let parsed;
|
|
874
|
+
try {
|
|
875
|
+
parsed = extractJSON(output);
|
|
876
|
+
}
|
|
877
|
+
catch {
|
|
878
|
+
return { accept: false, critique: 'Judge output was not valid JSON.' };
|
|
879
|
+
}
|
|
880
|
+
if (verdictSchema) {
|
|
881
|
+
try {
|
|
882
|
+
validateOutput(verdictSchema, parsed);
|
|
883
|
+
}
|
|
884
|
+
catch (err) {
|
|
885
|
+
return { accept: false, critique: `Verdict failed schema validation: ${err instanceof Error ? err.message : String(err)}` };
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
const obj = (parsed && typeof parsed === 'object' ? parsed : {});
|
|
889
|
+
const accept = typeof obj['accept'] === 'boolean' ? obj['accept'] : false;
|
|
890
|
+
const critique = typeof obj['critique'] === 'string' && obj['critique']
|
|
891
|
+
? obj['critique']
|
|
892
|
+
: accept ? '' : 'No critique provided.';
|
|
893
|
+
return { accept, critique };
|
|
894
|
+
}
|
|
895
|
+
/**
|
|
896
|
+
* Run the judge/refutation loop over a proposed answer: judges run sequentially
|
|
897
|
+
* (so quorum and budget can stop the rest), dissent is recorded to shared memory
|
|
898
|
+
* and trace, and `onDissent` decides whether to revise, reject, or keep.
|
|
899
|
+
*/
|
|
900
|
+
async function runConsensusCore(params) {
|
|
901
|
+
const { team, prompt, judges, mode, quorum, maxRounds, verdictSchema, onDissent, judgePrompt, budget, budgetBaseTokens, reviseProposer, defaults, onTrace, runId, } = params;
|
|
902
|
+
const pool = params.pool ?? new AgentPool(Math.max(1, defaults.maxConcurrency));
|
|
903
|
+
const sharedMem = team.getSharedMemoryInstance();
|
|
904
|
+
let answer = params.initialAnswer;
|
|
905
|
+
let usage = params.initialUsage;
|
|
906
|
+
const dissent = [];
|
|
907
|
+
let rounds = 0;
|
|
908
|
+
let accepted = false;
|
|
909
|
+
const overBudget = () => budget !== undefined && budgetBaseTokens + usage.input_tokens + usage.output_tokens > budget;
|
|
910
|
+
const runEphemeral = (config, text) => pool.runEphemeral(buildAgent(applyConsensusDefaults(config, defaults)), text);
|
|
911
|
+
// Proposer usage was already accumulated by the caller; bail before judging if it blew the budget.
|
|
912
|
+
if (overBudget()) {
|
|
913
|
+
return { answer, verdict: 'rejected', dissent, rounds, tokenUsage: usage };
|
|
914
|
+
}
|
|
915
|
+
let budgetHit = false;
|
|
916
|
+
for (let round = 1; round <= maxRounds; round++) {
|
|
917
|
+
rounds = round;
|
|
918
|
+
let acceptCount = 0;
|
|
919
|
+
const roundDissent = [];
|
|
920
|
+
for (let j = 0; j < judges.length; j++) {
|
|
921
|
+
const judge = judges[j];
|
|
922
|
+
const judgeText = buildJudgePrompt({ judge: judge.name, answer, prompt, mode, judgeIndex: j, judgePrompt });
|
|
923
|
+
const r = await runEphemeral(judge, judgeText);
|
|
924
|
+
usage = addUsage(usage, r.tokenUsage);
|
|
925
|
+
if (overBudget()) {
|
|
926
|
+
budgetHit = true;
|
|
927
|
+
break;
|
|
928
|
+
}
|
|
929
|
+
const verdict = parseJudgeVerdict(r.output, verdictSchema);
|
|
930
|
+
// Trace every verdict (accept or dissent); shared memory records dissent only.
|
|
931
|
+
if (onTrace) {
|
|
932
|
+
const now = Date.now();
|
|
933
|
+
emitTrace(onTrace, {
|
|
934
|
+
type: 'consensus',
|
|
935
|
+
runId: runId ?? '',
|
|
936
|
+
agent: judge.name,
|
|
937
|
+
round,
|
|
938
|
+
accepted: verdict.accept,
|
|
939
|
+
...(verdict.accept ? {} : { dissent: verdict.critique }),
|
|
940
|
+
startMs: now,
|
|
941
|
+
endMs: now,
|
|
942
|
+
durationMs: 0,
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
if (verdict.accept) {
|
|
946
|
+
acceptCount++;
|
|
947
|
+
if (acceptCount >= quorum) {
|
|
948
|
+
accepted = true;
|
|
949
|
+
break;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
else {
|
|
953
|
+
const labelled = `${judge.name}: ${verdict.critique}`;
|
|
954
|
+
roundDissent.push(labelled);
|
|
955
|
+
dissent.push(labelled);
|
|
956
|
+
if (sharedMem) {
|
|
957
|
+
await sharedMem.write(judge.name, `consensus:round:${round}:dissent`, verdict.critique);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
if (budgetHit || accepted)
|
|
962
|
+
break;
|
|
963
|
+
// Round missed quorum. Revise (if rounds remain) or stop.
|
|
964
|
+
if (onDissent === 'revise' && round < maxRounds && reviseProposer) {
|
|
965
|
+
const r = await runEphemeral(reviseProposer, buildRevisePrompt(prompt, answer, roundDissent));
|
|
966
|
+
usage = addUsage(usage, r.tokenUsage);
|
|
967
|
+
if (r.success && r.output)
|
|
968
|
+
answer = r.output;
|
|
969
|
+
if (overBudget()) {
|
|
970
|
+
budgetHit = true;
|
|
971
|
+
break;
|
|
972
|
+
}
|
|
973
|
+
continue;
|
|
974
|
+
}
|
|
975
|
+
break;
|
|
976
|
+
}
|
|
977
|
+
const verdict = accepted || (!budgetHit && onDissent === 'keep') ? 'accepted' : 'rejected';
|
|
978
|
+
return { answer, verdict, dissent, rounds, tokenUsage: usage };
|
|
979
|
+
}
|
|
980
|
+
/**
|
|
981
|
+
* Run the per-task `verify` hook before a task is finalised: feed the task
|
|
982
|
+
* result into the consensus loop, fold judge usage into the run's cumulative
|
|
983
|
+
* budget, surface the verdict, and return the effective result — the accepted
|
|
984
|
+
* revision when judges revise it, otherwise the original. The caller uses this
|
|
985
|
+
* to finalise the task so the queue, shared memory, events, and agentResults
|
|
986
|
+
* all agree on the verified outcome.
|
|
987
|
+
*/
|
|
988
|
+
async function runTaskVerify(task, assignee, result, sharedMem, ctx) {
|
|
989
|
+
const verify = task.verify;
|
|
990
|
+
const { team, config } = ctx;
|
|
991
|
+
const assigneeConfig = team.getAgents().find((a) => a.name === assignee);
|
|
992
|
+
const consensus = await runConsensusCore({
|
|
993
|
+
team,
|
|
994
|
+
prompt: task.description,
|
|
995
|
+
initialAnswer: result.output,
|
|
996
|
+
initialUsage: ZERO_USAGE,
|
|
997
|
+
budgetBaseTokens: ctx.cumulativeUsage.input_tokens + ctx.cumulativeUsage.output_tokens,
|
|
998
|
+
judges: verify.judges,
|
|
999
|
+
mode: verify.mode ?? 'refute',
|
|
1000
|
+
quorum: Math.min(verify.judges.length, Math.max(1, verify.quorum ?? Math.ceil(verify.judges.length / 2))),
|
|
1001
|
+
maxRounds: Math.max(1, verify.maxRounds ?? 2),
|
|
1002
|
+
verdictSchema: verify.verdictSchema,
|
|
1003
|
+
onDissent: verify.onDissent ?? 'revise',
|
|
1004
|
+
judgePrompt: verify.judgePrompt,
|
|
1005
|
+
budget: ctx.maxTokenBudget,
|
|
1006
|
+
reviseProposer: assigneeConfig,
|
|
1007
|
+
defaults: {
|
|
1008
|
+
defaultProvider: config.defaultProvider,
|
|
1009
|
+
defaultBaseURL: config.defaultBaseURL,
|
|
1010
|
+
defaultApiKey: config.defaultApiKey,
|
|
1011
|
+
defaultCwd: config.defaultCwd,
|
|
1012
|
+
maxConcurrency: config.maxConcurrency ?? DEFAULT_MAX_CONCURRENCY,
|
|
1013
|
+
},
|
|
1014
|
+
onTrace: config.onTrace,
|
|
1015
|
+
...(ctx.runId ? { runId: ctx.runId } : {}),
|
|
1016
|
+
});
|
|
1017
|
+
ctx.cumulativeUsage = addUsage(ctx.cumulativeUsage, consensus.tokenUsage);
|
|
1018
|
+
// Surface the verdict as a task-level outcome so downstream agents and the
|
|
1019
|
+
// final synthesis can see whether the result survived scrutiny.
|
|
1020
|
+
if (sharedMem) {
|
|
1021
|
+
const summary = consensus.verdict === 'accepted'
|
|
1022
|
+
? 'accepted'
|
|
1023
|
+
: `rejected${consensus.dissent.length ? `: ${consensus.dissent.join('; ')}` : ''}`;
|
|
1024
|
+
await sharedMem.write(assignee, `task:${task.id}:verdict`, summary);
|
|
1025
|
+
}
|
|
1026
|
+
const total = ctx.cumulativeUsage.input_tokens + ctx.cumulativeUsage.output_tokens;
|
|
1027
|
+
if (!ctx.budgetExceededTriggered && ctx.maxTokenBudget !== undefined && total > ctx.maxTokenBudget) {
|
|
1028
|
+
ctx.budgetExceededTriggered = true;
|
|
1029
|
+
const err = new TokenBudgetExceededError('orchestrator', total, ctx.maxTokenBudget);
|
|
1030
|
+
ctx.budgetExceededReason = err.message;
|
|
1031
|
+
config.onProgress?.({
|
|
1032
|
+
type: 'budget_exceeded',
|
|
1033
|
+
agent: assignee,
|
|
1034
|
+
task: task.id,
|
|
1035
|
+
data: err,
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
// Only an *accepted* revision supersedes the task result; a rejected revision is
|
|
1039
|
+
// recorded as dissent but the caller finalises with the original output. Judge
|
|
1040
|
+
// usage rolls into the per-task usage (mirrors how delegation usage rolls in).
|
|
1041
|
+
const useRevision = consensus.verdict === 'accepted' && consensus.answer && consensus.answer !== result.output;
|
|
1042
|
+
return {
|
|
1043
|
+
...result,
|
|
1044
|
+
output: useRevision ? consensus.answer : result.output,
|
|
1045
|
+
tokenUsage: addUsage(result.tokenUsage, consensus.tokenUsage),
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
694
1048
|
// ---------------------------------------------------------------------------
|
|
695
1049
|
// OpenMultiAgent
|
|
696
1050
|
// ---------------------------------------------------------------------------
|
|
@@ -726,6 +1080,7 @@ export class OpenMultiAgent {
|
|
|
726
1080
|
// disable the filesystem sandbox; a string sets a custom sandbox root.
|
|
727
1081
|
defaultCwd: config.defaultCwd === undefined ? defaultWorkspaceDir() : config.defaultCwd,
|
|
728
1082
|
maxTokenBudget: config.maxTokenBudget,
|
|
1083
|
+
defaultToolPreset: config.defaultToolPreset,
|
|
729
1084
|
onApproval: config.onApproval,
|
|
730
1085
|
onPlanReady: config.onPlanReady,
|
|
731
1086
|
onAgentStream: config.onAgentStream,
|
|
@@ -770,14 +1125,14 @@ export class OpenMultiAgent {
|
|
|
770
1125
|
*/
|
|
771
1126
|
async runAgent(config, prompt, options) {
|
|
772
1127
|
const effectiveBudget = resolveTokenBudget(config.maxTokenBudget, this.config.maxTokenBudget);
|
|
773
|
-
const effective = {
|
|
1128
|
+
const effective = applyDefaultToolPreset({
|
|
774
1129
|
...config,
|
|
775
1130
|
provider: config.provider ?? this.config.defaultProvider,
|
|
776
1131
|
baseURL: config.baseURL ?? this.config.defaultBaseURL,
|
|
777
1132
|
apiKey: config.apiKey ?? this.config.defaultApiKey,
|
|
778
1133
|
cwd: config.cwd === undefined ? this.config.defaultCwd : config.cwd,
|
|
779
1134
|
maxTokenBudget: effectiveBudget,
|
|
780
|
-
};
|
|
1135
|
+
}, this.config.defaultToolPreset);
|
|
781
1136
|
const agent = buildAgent(effective);
|
|
782
1137
|
this.config.onProgress?.({
|
|
783
1138
|
type: 'agent_start',
|
|
@@ -858,14 +1213,14 @@ export class OpenMultiAgent {
|
|
|
858
1213
|
// to avoid duplicate progress events and double completedTaskCount.
|
|
859
1214
|
// Events are emitted here; counting is handled by buildTeamRunResult().
|
|
860
1215
|
const effectiveBudget = resolveTokenBudget(bestAgent.maxTokenBudget, this.config.maxTokenBudget);
|
|
861
|
-
const effective = {
|
|
1216
|
+
const effective = withModelRoute(applyDefaultToolPreset({
|
|
862
1217
|
...bestAgent,
|
|
863
1218
|
provider: bestAgent.provider ?? this.config.defaultProvider,
|
|
864
1219
|
baseURL: bestAgent.baseURL ?? this.config.defaultBaseURL,
|
|
865
1220
|
apiKey: bestAgent.apiKey ?? this.config.defaultApiKey,
|
|
866
1221
|
cwd: bestAgent.cwd === undefined ? this.config.defaultCwd : bestAgent.cwd,
|
|
867
1222
|
maxTokenBudget: effectiveBudget,
|
|
868
|
-
};
|
|
1223
|
+
}, this.config.defaultToolPreset), routeMatches(options?.modelRouting, { phase: 'short-circuit', agent: bestAgent.name }));
|
|
869
1224
|
const agent = buildAgent(effective);
|
|
870
1225
|
this.config.onProgress?.({
|
|
871
1226
|
type: 'agent_start',
|
|
@@ -915,7 +1270,7 @@ export class OpenMultiAgent {
|
|
|
915
1270
|
// ------------------------------------------------------------------
|
|
916
1271
|
// Step 1: Coordinator decomposes goal into tasks
|
|
917
1272
|
// ------------------------------------------------------------------
|
|
918
|
-
const
|
|
1273
|
+
const coordinatorBaseConfig = {
|
|
919
1274
|
name: 'coordinator',
|
|
920
1275
|
model: coordinatorOverrides?.model ?? this.config.defaultModel,
|
|
921
1276
|
...(coordinatorOverrides?.adapter !== undefined ? { adapter: coordinatorOverrides.adapter } : {}),
|
|
@@ -942,6 +1297,7 @@ export class OpenMultiAgent {
|
|
|
942
1297
|
loopDetection: coordinatorOverrides?.loopDetection,
|
|
943
1298
|
timeoutMs: coordinatorOverrides?.timeoutMs,
|
|
944
1299
|
};
|
|
1300
|
+
const coordinatorConfig = withModelRoute(coordinatorBaseConfig, routeMatches(options?.modelRouting, { phase: 'coordinator', agent: 'coordinator' }));
|
|
945
1301
|
const decompositionPrompt = this.buildDecompositionPrompt(goal, agentConfigs);
|
|
946
1302
|
const coordinatorAgent = buildAgent(coordinatorConfig);
|
|
947
1303
|
const runId = this.config.onTrace ? generateRunId() : undefined;
|
|
@@ -1020,6 +1376,9 @@ export class OpenMultiAgent {
|
|
|
1020
1376
|
},
|
|
1021
1377
|
}
|
|
1022
1378
|
: {}),
|
|
1379
|
+
modelRouting: options?.modelRouting,
|
|
1380
|
+
taskById: new Map(queue.list().map((task) => [task.id, task])),
|
|
1381
|
+
taskLeafById: new Map(queue.list().map((task) => [task.id, isLeafTask(task, queue.list())])),
|
|
1023
1382
|
};
|
|
1024
1383
|
const planTasks = queue.list();
|
|
1025
1384
|
const planReadyStartMs = Date.now();
|
|
@@ -1055,6 +1414,11 @@ export class OpenMultiAgent {
|
|
|
1055
1414
|
assignee: task.assignee,
|
|
1056
1415
|
status: task.status,
|
|
1057
1416
|
dependsOn: task.dependsOn ?? [],
|
|
1417
|
+
description: task.description,
|
|
1418
|
+
memoryScope: task.memoryScope,
|
|
1419
|
+
maxRetries: task.maxRetries,
|
|
1420
|
+
retryDelayMs: task.retryDelayMs,
|
|
1421
|
+
retryBackoff: task.retryBackoff,
|
|
1058
1422
|
metrics: undefined,
|
|
1059
1423
|
}));
|
|
1060
1424
|
this.config.onProgress?.({
|
|
@@ -1075,6 +1439,11 @@ export class OpenMultiAgent {
|
|
|
1075
1439
|
assignee: task.assignee,
|
|
1076
1440
|
status: task.status,
|
|
1077
1441
|
dependsOn: task.dependsOn ?? [],
|
|
1442
|
+
description: task.description,
|
|
1443
|
+
memoryScope: task.memoryScope,
|
|
1444
|
+
maxRetries: task.maxRetries,
|
|
1445
|
+
retryDelayMs: task.retryDelayMs,
|
|
1446
|
+
retryBackoff: task.retryBackoff,
|
|
1078
1447
|
metrics: taskMetrics.get(task.id),
|
|
1079
1448
|
}));
|
|
1080
1449
|
// ------------------------------------------------------------------
|
|
@@ -1088,10 +1457,11 @@ export class OpenMultiAgent {
|
|
|
1088
1457
|
return this.buildTeamRunResult(agentResults, goal, taskRecords);
|
|
1089
1458
|
}
|
|
1090
1459
|
const synthesisPrompt = await this.buildSynthesisPrompt(goal, queue.list(), team);
|
|
1460
|
+
const synthesisAgent = buildAgent(withModelRoute(coordinatorBaseConfig, routeMatches(options?.modelRouting, { phase: 'synthesis', agent: 'coordinator' })));
|
|
1091
1461
|
const synthTraceOptions = this.config.onTrace
|
|
1092
1462
|
? { onTrace: this.config.onTrace, runId: runId ?? '', traceAgent: 'coordinator' }
|
|
1093
1463
|
: undefined;
|
|
1094
|
-
const synthesisResult = await
|
|
1464
|
+
const synthesisResult = await synthesisAgent.run(synthesisPrompt, synthTraceOptions);
|
|
1095
1465
|
agentResults.set('coordinator', synthesisResult);
|
|
1096
1466
|
cumulativeUsage = addUsage(cumulativeUsage, synthesisResult.tokenUsage);
|
|
1097
1467
|
if (maxTokenBudget !== undefined
|
|
@@ -1113,8 +1483,61 @@ export class OpenMultiAgent {
|
|
|
1113
1483
|
return this.buildTeamRunResult(agentResults, goal, taskRecords);
|
|
1114
1484
|
}
|
|
1115
1485
|
// -------------------------------------------------------------------------
|
|
1116
|
-
// Explicit-task team
|
|
1486
|
+
// Explicit-task and plan replay team runs
|
|
1117
1487
|
// -------------------------------------------------------------------------
|
|
1488
|
+
/**
|
|
1489
|
+
* Convert a plan-only {@link TeamRunResult} into a serializable plan artifact.
|
|
1490
|
+
*
|
|
1491
|
+
* The input must come from `runTeam(team, goal, { planOnly: true })` on a
|
|
1492
|
+
* version that records task descriptions. Executed run results are rejected
|
|
1493
|
+
* because their task records are not a replay contract.
|
|
1494
|
+
*/
|
|
1495
|
+
createPlanArtifact(result) {
|
|
1496
|
+
if (result.planOnly !== true || !result.tasks) {
|
|
1497
|
+
throw new Error('createPlanArtifact requires a plan-only TeamRunResult.');
|
|
1498
|
+
}
|
|
1499
|
+
return {
|
|
1500
|
+
version: 1,
|
|
1501
|
+
...(result.goal !== undefined ? { goal: result.goal } : {}),
|
|
1502
|
+
tasks: result.tasks.map((task) => {
|
|
1503
|
+
if (!task.description) {
|
|
1504
|
+
throw new Error(`Plan task "${task.id}" is missing a description and cannot be replayed.`);
|
|
1505
|
+
}
|
|
1506
|
+
return {
|
|
1507
|
+
id: task.id,
|
|
1508
|
+
title: task.title,
|
|
1509
|
+
description: task.description,
|
|
1510
|
+
...(task.assignee !== undefined ? { assignee: task.assignee } : {}),
|
|
1511
|
+
...(task.dependsOn.length > 0 ? { dependsOn: task.dependsOn } : {}),
|
|
1512
|
+
...(task.memoryScope !== undefined ? { memoryScope: task.memoryScope } : {}),
|
|
1513
|
+
...(task.maxRetries !== undefined ? { maxRetries: task.maxRetries } : {}),
|
|
1514
|
+
...(task.retryDelayMs !== undefined ? { retryDelayMs: task.retryDelayMs } : {}),
|
|
1515
|
+
...(task.retryBackoff !== undefined ? { retryBackoff: task.retryBackoff } : {}),
|
|
1516
|
+
};
|
|
1517
|
+
}),
|
|
1518
|
+
};
|
|
1519
|
+
}
|
|
1520
|
+
/**
|
|
1521
|
+
* Replay a persisted plan artifact without invoking the coordinator.
|
|
1522
|
+
*
|
|
1523
|
+
* Task IDs, dependencies, assignees, titles, and descriptions are used exactly
|
|
1524
|
+
* as stored in the artifact. This is intentionally execution-only; it does not
|
|
1525
|
+
* synthesize a coordinator final answer and it does not implement durable
|
|
1526
|
+
* checkpoints.
|
|
1527
|
+
*/
|
|
1528
|
+
async runFromPlan(team, plan, options) {
|
|
1529
|
+
if (plan.version !== 1) {
|
|
1530
|
+
throw new Error(`Unsupported plan artifact version: ${String(plan.version)}`);
|
|
1531
|
+
}
|
|
1532
|
+
const queue = new TaskQueue();
|
|
1533
|
+
const tasks = this.tasksFromPlan(plan);
|
|
1534
|
+
const validation = validateTaskDependencies(tasks);
|
|
1535
|
+
if (!validation.valid) {
|
|
1536
|
+
throw new Error(`Invalid plan artifact: ${validation.errors.join(' ')}`);
|
|
1537
|
+
}
|
|
1538
|
+
queue.addBatch(tasks);
|
|
1539
|
+
return this.executeExplicitTaskQueue(team, queue, options, plan.goal);
|
|
1540
|
+
}
|
|
1118
1541
|
/**
|
|
1119
1542
|
* Run a team with an explicitly provided task list.
|
|
1120
1543
|
*
|
|
@@ -1128,7 +1551,6 @@ export class OpenMultiAgent {
|
|
|
1128
1551
|
async runTasks(team, tasks, options) {
|
|
1129
1552
|
const agentConfigs = team.getAgents();
|
|
1130
1553
|
const queue = new TaskQueue();
|
|
1131
|
-
const scheduler = new Scheduler('dependency-first');
|
|
1132
1554
|
this.loadSpecsIntoQueue(tasks.map((t) => ({
|
|
1133
1555
|
title: t.title,
|
|
1134
1556
|
description: t.description,
|
|
@@ -1138,34 +1560,92 @@ export class OpenMultiAgent {
|
|
|
1138
1560
|
maxRetries: t.maxRetries,
|
|
1139
1561
|
retryDelayMs: t.retryDelayMs,
|
|
1140
1562
|
retryBackoff: t.retryBackoff,
|
|
1563
|
+
role: t.role,
|
|
1564
|
+
priority: t.priority,
|
|
1565
|
+
verify: t.verify,
|
|
1141
1566
|
})), agentConfigs, queue);
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1567
|
+
return this.executeExplicitTaskQueue(team, queue, options);
|
|
1568
|
+
}
|
|
1569
|
+
// -------------------------------------------------------------------------
|
|
1570
|
+
// Consensus
|
|
1571
|
+
// -------------------------------------------------------------------------
|
|
1572
|
+
/**
|
|
1573
|
+
* Run a proposer→judge consensus over a single prompt.
|
|
1574
|
+
*
|
|
1575
|
+
* The proposer emits an answer; judges try to refute it over up to
|
|
1576
|
+
* `maxRounds`, exiting early once `quorum` accept. Proposer and judge token
|
|
1577
|
+
* usage all count against the orchestrator's `maxTokenBudget` — crossing it
|
|
1578
|
+
* stops issuing further judge calls, exactly like delegation and `runTasks`.
|
|
1579
|
+
*/
|
|
1580
|
+
async runConsensus(team, prompt, options) {
|
|
1581
|
+
const proposers = Array.isArray(options.proposer) ? options.proposer : [options.proposer];
|
|
1582
|
+
if (proposers.length === 0) {
|
|
1583
|
+
throw new Error('runConsensus: at least one proposer is required.');
|
|
1584
|
+
}
|
|
1585
|
+
if (options.judges.length === 0) {
|
|
1586
|
+
throw new Error('runConsensus: at least one judge is required.');
|
|
1587
|
+
}
|
|
1588
|
+
const mode = options.mode ?? 'refute';
|
|
1589
|
+
const maxRounds = Math.max(1, options.maxRounds ?? 2);
|
|
1590
|
+
const quorum = Math.min(options.judges.length, Math.max(1, options.quorum ?? Math.ceil(options.judges.length / 2)));
|
|
1591
|
+
const onDissent = options.onDissent ?? 'revise';
|
|
1592
|
+
const budget = this.config.maxTokenBudget;
|
|
1593
|
+
const defaults = {
|
|
1594
|
+
defaultProvider: this.config.defaultProvider,
|
|
1595
|
+
defaultBaseURL: this.config.defaultBaseURL,
|
|
1596
|
+
defaultApiKey: this.config.defaultApiKey,
|
|
1597
|
+
defaultCwd: this.config.defaultCwd,
|
|
1598
|
+
maxConcurrency: this.config.maxConcurrency,
|
|
1599
|
+
};
|
|
1600
|
+
const pool = new AgentPool(Math.max(1, this.config.maxConcurrency));
|
|
1601
|
+
let usage = ZERO_USAGE;
|
|
1602
|
+
// Step 2: run proposer(s); accumulate usage and honour the budget before judging.
|
|
1603
|
+
const candidates = [];
|
|
1604
|
+
for (const proposerConfig of proposers) {
|
|
1605
|
+
const r = await pool.runEphemeral(buildAgent(applyConsensusDefaults(proposerConfig, defaults)), prompt);
|
|
1606
|
+
usage = addUsage(usage, r.tokenUsage);
|
|
1607
|
+
if (r.success && r.output)
|
|
1608
|
+
candidates.push(r.output);
|
|
1609
|
+
if (budget !== undefined && usage.input_tokens + usage.output_tokens > budget) {
|
|
1610
|
+
this.config.onProgress?.({
|
|
1611
|
+
type: 'budget_exceeded',
|
|
1612
|
+
agent: proposerConfig.name,
|
|
1613
|
+
data: new TokenBudgetExceededError(proposerConfig.name, usage.input_tokens + usage.output_tokens, budget),
|
|
1614
|
+
});
|
|
1615
|
+
return {
|
|
1616
|
+
answer: candidates.join('\n\n---\n\n'),
|
|
1617
|
+
verdict: 'rejected',
|
|
1618
|
+
dissent: [],
|
|
1619
|
+
rounds: 0,
|
|
1620
|
+
tokenUsage: usage,
|
|
1621
|
+
};
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
// Every proposer failed or returned empty output: there is nothing to judge.
|
|
1625
|
+
// Bail with a rejected verdict so an empty answer can never come back accepted.
|
|
1626
|
+
if (candidates.length === 0) {
|
|
1627
|
+
return { answer: '', verdict: 'rejected', dissent: [], rounds: 0, tokenUsage: usage };
|
|
1628
|
+
}
|
|
1629
|
+
return runConsensusCore({
|
|
1146
1630
|
team,
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1631
|
+
prompt,
|
|
1632
|
+
initialAnswer: candidates.join('\n\n---\n\n'),
|
|
1633
|
+
initialUsage: usage,
|
|
1634
|
+
budgetBaseTokens: 0,
|
|
1635
|
+
judges: options.judges,
|
|
1636
|
+
mode,
|
|
1637
|
+
quorum,
|
|
1638
|
+
maxRounds,
|
|
1639
|
+
verdictSchema: options.verdictSchema,
|
|
1640
|
+
onDissent,
|
|
1641
|
+
judgePrompt: options.judgePrompt,
|
|
1642
|
+
budget,
|
|
1643
|
+
reviseProposer: proposers[0],
|
|
1644
|
+
defaults,
|
|
1645
|
+
onTrace: this.config.onTrace,
|
|
1151
1646
|
runId: this.config.onTrace ? generateRunId() : undefined,
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
maxTokenBudget: this.config.maxTokenBudget,
|
|
1155
|
-
budgetExceededTriggered: false,
|
|
1156
|
-
budgetExceededReason: undefined,
|
|
1157
|
-
taskMetrics: new Map(),
|
|
1158
|
-
};
|
|
1159
|
-
await executeQueue(queue, ctx);
|
|
1160
|
-
const taskRecords = queue.list().map((task) => ({
|
|
1161
|
-
id: task.id,
|
|
1162
|
-
title: task.title,
|
|
1163
|
-
assignee: task.assignee,
|
|
1164
|
-
status: task.status,
|
|
1165
|
-
dependsOn: task.dependsOn ?? [],
|
|
1166
|
-
metrics: ctx.taskMetrics.get(task.id),
|
|
1167
|
-
}));
|
|
1168
|
-
return this.buildTeamRunResult(agentResults, undefined, taskRecords);
|
|
1647
|
+
pool,
|
|
1648
|
+
});
|
|
1169
1649
|
}
|
|
1170
1650
|
// -------------------------------------------------------------------------
|
|
1171
1651
|
// Observability
|
|
@@ -1325,6 +1805,63 @@ export class OpenMultiAgent {
|
|
|
1325
1805
|
'If some tasks failed or were skipped, note any gaps in the result.',
|
|
1326
1806
|
].join('\n');
|
|
1327
1807
|
}
|
|
1808
|
+
tasksFromPlan(plan) {
|
|
1809
|
+
const now = new Date();
|
|
1810
|
+
return plan.tasks.map((task) => ({
|
|
1811
|
+
id: task.id,
|
|
1812
|
+
title: task.title,
|
|
1813
|
+
description: task.description,
|
|
1814
|
+
status: 'pending',
|
|
1815
|
+
...(task.assignee !== undefined ? { assignee: task.assignee } : {}),
|
|
1816
|
+
...(task.dependsOn && task.dependsOn.length > 0 ? { dependsOn: [...task.dependsOn] } : {}),
|
|
1817
|
+
...(task.memoryScope !== undefined ? { memoryScope: task.memoryScope } : {}),
|
|
1818
|
+
result: undefined,
|
|
1819
|
+
createdAt: now,
|
|
1820
|
+
updatedAt: now,
|
|
1821
|
+
...(task.maxRetries !== undefined ? { maxRetries: task.maxRetries } : {}),
|
|
1822
|
+
...(task.retryDelayMs !== undefined ? { retryDelayMs: task.retryDelayMs } : {}),
|
|
1823
|
+
...(task.retryBackoff !== undefined ? { retryBackoff: task.retryBackoff } : {}),
|
|
1824
|
+
}));
|
|
1825
|
+
}
|
|
1826
|
+
async executeExplicitTaskQueue(team, queue, options, goal) {
|
|
1827
|
+
const agentConfigs = team.getAgents();
|
|
1828
|
+
const scheduler = new Scheduler('dependency-first');
|
|
1829
|
+
scheduler.autoAssign(queue, agentConfigs);
|
|
1830
|
+
const pool = this.buildPool(agentConfigs);
|
|
1831
|
+
const agentResults = new Map();
|
|
1832
|
+
const ctx = {
|
|
1833
|
+
team,
|
|
1834
|
+
pool,
|
|
1835
|
+
scheduler,
|
|
1836
|
+
agentResults,
|
|
1837
|
+
config: this.config,
|
|
1838
|
+
runId: this.config.onTrace ? generateRunId() : undefined,
|
|
1839
|
+
abortSignal: options?.abortSignal,
|
|
1840
|
+
cumulativeUsage: ZERO_USAGE,
|
|
1841
|
+
maxTokenBudget: this.config.maxTokenBudget,
|
|
1842
|
+
budgetExceededTriggered: false,
|
|
1843
|
+
budgetExceededReason: undefined,
|
|
1844
|
+
taskMetrics: new Map(),
|
|
1845
|
+
modelRouting: options?.modelRouting,
|
|
1846
|
+
taskById: new Map(queue.list().map((task) => [task.id, task])),
|
|
1847
|
+
taskLeafById: new Map(queue.list().map((task) => [task.id, isLeafTask(task, queue.list())])),
|
|
1848
|
+
};
|
|
1849
|
+
await executeQueue(queue, ctx);
|
|
1850
|
+
const taskRecords = queue.list().map((task) => ({
|
|
1851
|
+
id: task.id,
|
|
1852
|
+
title: task.title,
|
|
1853
|
+
assignee: task.assignee,
|
|
1854
|
+
status: task.status,
|
|
1855
|
+
dependsOn: task.dependsOn ?? [],
|
|
1856
|
+
description: task.description,
|
|
1857
|
+
memoryScope: task.memoryScope,
|
|
1858
|
+
maxRetries: task.maxRetries,
|
|
1859
|
+
retryDelayMs: task.retryDelayMs,
|
|
1860
|
+
retryBackoff: task.retryBackoff,
|
|
1861
|
+
metrics: ctx.taskMetrics.get(task.id),
|
|
1862
|
+
}));
|
|
1863
|
+
return this.buildTeamRunResult(agentResults, goal, taskRecords);
|
|
1864
|
+
}
|
|
1328
1865
|
/**
|
|
1329
1866
|
* Load a list of task specs into a queue.
|
|
1330
1867
|
*
|
|
@@ -1353,6 +1890,9 @@ export class OpenMultiAgent {
|
|
|
1353
1890
|
maxRetries: spec.maxRetries,
|
|
1354
1891
|
retryDelayMs: spec.retryDelayMs,
|
|
1355
1892
|
retryBackoff: spec.retryBackoff,
|
|
1893
|
+
role: spec.role,
|
|
1894
|
+
priority: spec.priority,
|
|
1895
|
+
verify: spec.verify,
|
|
1356
1896
|
});
|
|
1357
1897
|
const titleKey = normalizeTitle(spec.title);
|
|
1358
1898
|
if ((titleCounts.get(titleKey) ?? 0) === 1) {
|
|
@@ -1398,14 +1938,14 @@ export class OpenMultiAgent {
|
|
|
1398
1938
|
buildPool(agentConfigs) {
|
|
1399
1939
|
const pool = new AgentPool(this.config.maxConcurrency);
|
|
1400
1940
|
for (const config of agentConfigs) {
|
|
1401
|
-
const effective = {
|
|
1941
|
+
const effective = applyDefaultToolPreset({
|
|
1402
1942
|
...config,
|
|
1403
1943
|
model: config.model,
|
|
1404
1944
|
provider: config.provider ?? this.config.defaultProvider,
|
|
1405
1945
|
baseURL: config.baseURL ?? this.config.defaultBaseURL,
|
|
1406
1946
|
apiKey: config.apiKey ?? this.config.defaultApiKey,
|
|
1407
1947
|
cwd: config.cwd === undefined ? this.config.defaultCwd : config.cwd,
|
|
1408
|
-
};
|
|
1948
|
+
}, this.config.defaultToolPreset);
|
|
1409
1949
|
pool.add(buildAgent(effective, { includeDelegateTool: true }));
|
|
1410
1950
|
}
|
|
1411
1951
|
return pool;
|