mirror of
https://github.com/JackChen-me/open-multi-agent.git
synced 2026-10-19 01:29:57 -05:00
fix(orchestrator): keep parsed consensus output out of the public result (#537)
Follow-up to #536. `runConsensusCore()` tracks the parsed value so an accepted verify revision can replace `output` and `structured` together, but the same loop backs the standalone `runConsensus()`, where several proposer answers may be joined into one string and no parsed value corresponds to the answer. That path was returning a `structured` field the public `ConsensusResult` never declared. Drop it at the public boundary, and cover both halves of the verify contract: an accepted revision replaces the pair, a rejected one preserves it. Credit green3sf in CONTRIBUTORS.md for #536.
This commit is contained in:
1 parent
4d9a5cf3de
commit
399f906f8c
3 files changed
+82
-1
No files matched your search
@@ -25,6 +25,7 @@ Opening your first PR gets you added here. See the [contribution guide](.github/
|
||||
- [@tlysanhuo](https://github.com/tlysanhuo) (trace span parent linkage)
|
||||
- [@LambIessz](https://github.com/LambIessz) (orchestrator cost budget, MessageBus persistence in checkpoints, retryable route fallback)
|
||||
- [@Bobuyoucrypto](https://github.com/Bobuyoucrypto) (Windows bash timeout process-tree kill)
|
||||
- [@green3sf](https://github.com/green3sf) (structured output refresh after an accepted verify revision)
|
||||
|
||||
## Provider integrations
|
||||
|
||||
|
||||
@@ -2691,7 +2691,11 @@ export class OpenMultiAgent {
|
||||
})
|
||||
}
|
||||
|
||||
const result = await runConsensusCore({
|
||||
// `structured` belongs to the per-task verify hook, where one task result
|
||||
// carries one parsed value. Here several proposer answers may be joined into
|
||||
// a single string, so no parsed value corresponds to it; drop it rather than
|
||||
// leak an undeclared field onto the public ConsensusResult.
|
||||
const { structured: _structured, ...result } = await runConsensusCore({
|
||||
team,
|
||||
prompt,
|
||||
initialAnswer: candidates.join('\n\n---\n\n'),
|
||||
|
||||
@@ -253,6 +253,7 @@ describe('runConsensus onDissent', () => {
|
||||
expect(res.verdict).toBe('rejected')
|
||||
})
|
||||
|
||||
|
||||
it('keep stops but keeps the answer (accepted despite dissent)', async () => {
|
||||
const { res, proposerCalls } = await run('keep')
|
||||
expect(proposerCalls).toBe(1)
|
||||
@@ -260,6 +261,33 @@ describe('runConsensus onDissent', () => {
|
||||
expect(res.verdict).toBe('accepted')
|
||||
expect(res.dissent.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('does not surface the revision proposer\'s parsed output on the public result', async () => {
|
||||
// A schema-bearing proposer parses its revision internally, but runConsensus
|
||||
// may join several proposer answers into one string, so no single parsed value
|
||||
// corresponds to `answer`. The public ConsensusResult must not carry one.
|
||||
const proposer: AgentConfig = {
|
||||
...agent('proposer', captureAdapter((p) =>
|
||||
p.includes('previous answer') ? '{"decision":"quarantine"}' : '{"decision":"accept"}',
|
||||
).adapter),
|
||||
outputSchema: z.object({ decision: z.enum(['accept', 'quarantine']) }),
|
||||
}
|
||||
const judge = captureAdapter((p) => (p.includes('quarantine') ? ACCEPT : DISSENT))
|
||||
const orch = new OpenMultiAgent()
|
||||
const team = orch.createTeam('t', { name: 't', agents: [] })
|
||||
|
||||
const res = await orch.runConsensus(team, 'classify the interval', {
|
||||
proposer,
|
||||
judges: [agent('judge', judge.adapter)],
|
||||
quorum: 1,
|
||||
maxRounds: 2,
|
||||
onDissent: 'revise',
|
||||
})
|
||||
|
||||
expect(res.verdict).toBe('accepted')
|
||||
expect(res.answer).toBe('{"decision":"quarantine"}')
|
||||
expect(Object.keys(res)).not.toContain('structured')
|
||||
})
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -536,6 +564,54 @@ describe('per-task verify hook', () => {
|
||||
expect(consumer.prompts[0]).not.toContain('{"decision":"accept"}')
|
||||
})
|
||||
|
||||
it('keeps the original structured output when a revision is rejected', async () => {
|
||||
// Mirror of the accepted-revision case: the worker still produces a revision,
|
||||
// but no judge ever accepts it, so both halves of the original pair must survive.
|
||||
const worker = captureAdapter((p) =>
|
||||
p.includes('previous answer')
|
||||
? '{"decision":"quarantine"}'
|
||||
: '{"decision":"accept"}',
|
||||
)
|
||||
const judge = captureAdapter(DISSENT) // never accepts -> verdict stays rejected
|
||||
const consumer = captureAdapter('done')
|
||||
const orch = new OpenMultiAgent()
|
||||
const team = orch.createTeam('t', {
|
||||
name: 't',
|
||||
agents: [{
|
||||
...agent('worker', worker.adapter),
|
||||
outputSchema: z.object({ decision: z.enum(['accept', 'quarantine']) }),
|
||||
}, agent('consumer', consumer.adapter)],
|
||||
})
|
||||
|
||||
const run = await orch.runTasks(team, [
|
||||
{
|
||||
title: 'verified',
|
||||
description: 'classify the interval',
|
||||
assignee: 'worker',
|
||||
verify: {
|
||||
judges: [agent('judge', judge.adapter)],
|
||||
quorum: 1,
|
||||
maxRounds: 2,
|
||||
onDissent: 'revise',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'consume-verdict',
|
||||
description: 'use the verified structured decision',
|
||||
assignee: 'consumer',
|
||||
dependsOn: ['verified'],
|
||||
dependencyPayload: 'structured',
|
||||
},
|
||||
])
|
||||
const taskId = run.tasks![0]!.id
|
||||
|
||||
// The rejected revision must not leak into either representation.
|
||||
expect(run.taskResults?.get(taskId)?.output).toBe('{"decision":"accept"}')
|
||||
expect(run.taskResults?.get(taskId)?.structured).toEqual({ decision: 'accept' })
|
||||
expect(consumer.prompts[0]).toContain('{"decision":"accept"}')
|
||||
expect(consumer.prompts[0]).not.toContain('{"decision":"quarantine"}')
|
||||
})
|
||||
|
||||
it('feeds the prior answer into the revision prompt', async () => {
|
||||
const worker = captureAdapter((p) => (p.includes('previous answer') ? 'revised' : 'original'))
|
||||
const judge = captureAdapter(DISSENT)
|
||||
|
||||
Reference in new issue
Block a user