@open-multi-agent/core 1.6.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 +11 -2
- 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.js +1 -1
- package/dist/cli/oma.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/llm/minimax.d.ts +2 -2
- package/dist/llm/minimax.js +2 -2
- package/dist/orchestrator/orchestrator.d.ts +14 -4
- package/dist/orchestrator/orchestrator.d.ts.map +1 -1
- package/dist/orchestrator/orchestrator.js +469 -19
- 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 +122 -4
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -50,6 +50,7 @@ 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
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();
|
|
@@ -1098,10 +1457,11 @@ export class OpenMultiAgent {
|
|
|
1098
1457
|
return this.buildTeamRunResult(agentResults, goal, taskRecords);
|
|
1099
1458
|
}
|
|
1100
1459
|
const synthesisPrompt = await this.buildSynthesisPrompt(goal, queue.list(), team);
|
|
1460
|
+
const synthesisAgent = buildAgent(withModelRoute(coordinatorBaseConfig, routeMatches(options?.modelRouting, { phase: 'synthesis', agent: 'coordinator' })));
|
|
1101
1461
|
const synthTraceOptions = this.config.onTrace
|
|
1102
1462
|
? { onTrace: this.config.onTrace, runId: runId ?? '', traceAgent: 'coordinator' }
|
|
1103
1463
|
: undefined;
|
|
1104
|
-
const synthesisResult = await
|
|
1464
|
+
const synthesisResult = await synthesisAgent.run(synthesisPrompt, synthTraceOptions);
|
|
1105
1465
|
agentResults.set('coordinator', synthesisResult);
|
|
1106
1466
|
cumulativeUsage = addUsage(cumulativeUsage, synthesisResult.tokenUsage);
|
|
1107
1467
|
if (maxTokenBudget !== undefined
|
|
@@ -1200,10 +1560,94 @@ export class OpenMultiAgent {
|
|
|
1200
1560
|
maxRetries: t.maxRetries,
|
|
1201
1561
|
retryDelayMs: t.retryDelayMs,
|
|
1202
1562
|
retryBackoff: t.retryBackoff,
|
|
1563
|
+
role: t.role,
|
|
1564
|
+
priority: t.priority,
|
|
1565
|
+
verify: t.verify,
|
|
1203
1566
|
})), agentConfigs, queue);
|
|
1204
1567
|
return this.executeExplicitTaskQueue(team, queue, options);
|
|
1205
1568
|
}
|
|
1206
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({
|
|
1630
|
+
team,
|
|
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,
|
|
1646
|
+
runId: this.config.onTrace ? generateRunId() : undefined,
|
|
1647
|
+
pool,
|
|
1648
|
+
});
|
|
1649
|
+
}
|
|
1650
|
+
// -------------------------------------------------------------------------
|
|
1207
1651
|
// Observability
|
|
1208
1652
|
// -------------------------------------------------------------------------
|
|
1209
1653
|
/**
|
|
@@ -1398,6 +1842,9 @@ export class OpenMultiAgent {
|
|
|
1398
1842
|
budgetExceededTriggered: false,
|
|
1399
1843
|
budgetExceededReason: undefined,
|
|
1400
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())])),
|
|
1401
1848
|
};
|
|
1402
1849
|
await executeQueue(queue, ctx);
|
|
1403
1850
|
const taskRecords = queue.list().map((task) => ({
|
|
@@ -1443,6 +1890,9 @@ export class OpenMultiAgent {
|
|
|
1443
1890
|
maxRetries: spec.maxRetries,
|
|
1444
1891
|
retryDelayMs: spec.retryDelayMs,
|
|
1445
1892
|
retryBackoff: spec.retryBackoff,
|
|
1893
|
+
role: spec.role,
|
|
1894
|
+
priority: spec.priority,
|
|
1895
|
+
verify: spec.verify,
|
|
1446
1896
|
});
|
|
1447
1897
|
const titleKey = normalizeTitle(spec.title);
|
|
1448
1898
|
if ((titleCounts.get(titleKey) ?? 0) === 1) {
|
|
@@ -1488,14 +1938,14 @@ export class OpenMultiAgent {
|
|
|
1488
1938
|
buildPool(agentConfigs) {
|
|
1489
1939
|
const pool = new AgentPool(this.config.maxConcurrency);
|
|
1490
1940
|
for (const config of agentConfigs) {
|
|
1491
|
-
const effective = {
|
|
1941
|
+
const effective = applyDefaultToolPreset({
|
|
1492
1942
|
...config,
|
|
1493
1943
|
model: config.model,
|
|
1494
1944
|
provider: config.provider ?? this.config.defaultProvider,
|
|
1495
1945
|
baseURL: config.baseURL ?? this.config.defaultBaseURL,
|
|
1496
1946
|
apiKey: config.apiKey ?? this.config.defaultApiKey,
|
|
1497
1947
|
cwd: config.cwd === undefined ? this.config.defaultCwd : config.cwd,
|
|
1498
|
-
};
|
|
1948
|
+
}, this.config.defaultToolPreset);
|
|
1499
1949
|
pool.add(buildAgent(effective, { includeDelegateTool: true }));
|
|
1500
1950
|
}
|
|
1501
1951
|
return pool;
|