Merge branch 'main' into agent/task-requirements-hard-constraints

This commit is contained in:
Jack Chen committed 2026-07-26 14:16:22 +08:00
commit edc33db1a4
8 files changed
+272 -127

No files matched your search

+4 -4
View File
@@ -127,10 +127,10 @@ state. Assignments earlier in one scheduler call are not folded into that same
call; in event-driven execution the next ready-task call can observe tasks
already marked `in_progress`.
Coordinator plans that name an agent outside the roster emit an
`INVALID_ASSIGNEE` warning, clear that assignment, and use the configured
scheduler by default. Set `strictAssignees: true` to stop before task execution
with a structured validation error instead.
Coordinator plans that name an agent outside the roster fail validation before
task execution by default. Set `strictAssignees: false` only to retain the
legacy behavior of emitting an `INVALID_ASSIGNEE` warning, clearing that
assignment, and using the configured scheduler.
## Approval modes
+6 -5
View File
@@ -158,11 +158,12 @@ const orchestrator = new OpenMultiAgent({
Agents may declare `description`, `capabilities`, `costTier`, and
`latencyClass`, and tasks may add hard `requires` constraints. All strategies
fail before worker execution when those constraints cannot be satisfied; set
`strictAssignees: true` to fail fast when a coordinator plan names an agent
outside the roster. Weight semantics, load normalization, `NO_ELIGIBLE_AGENT`
and `INVALID_ASSIGNEE` behavior, approval compatibility, and progress-event
migration are covered in
fail before worker execution when those constraints cannot be satisfied.
Coordinator plans also fail fast by default when they name an agent outside
the roster; set `strictAssignees: false` only to retain legacy reassignment.
Weight semantics, load normalization, `NO_ELIGIBLE_AGENT` and
`INVALID_ASSIGNEE` behavior, approval compatibility, and progress-event migration
are covered in
[Task scheduling and dispatch](https://github.com/open-multi-agent/open-multi-agent/blob/main/docs/task-scheduling.md).
## Capabilities
+5 -4
View File
@@ -153,10 +153,11 @@ const orchestrator = new OpenMultiAgent({
Agent 可声明 `description`、`capabilities`、`costTier` 与 `latencyClass`,
任务可通过 `requires` 声明硬约束。任何调度策略无法满足这些约束时,都会在
worker 执行前失败;设置 `strictAssignees: true` 可在 coordinator 计划引用
roster 之外的 Agent 时提前失败。权重语义、负载归一化、`NO_ELIGIBLE_AGENT`
与 `INVALID_ASSIGNEE` 行为、审批兼容与 progress 事件迁移见
[任务调度与派发](https://github.com/open-multi-agent/open-multi-agent/blob/main/docs/task-scheduling.md)。
worker 执行前失败。Coordinator 计划默认也会在引用
roster 之外的 Agent 时提前失败;仅在需要保留旧的自动重新分配行为时设置
`strictAssignees: false`。权重语义、负载归一化、
`NO_ELIGIBLE_AGENT` 与 `INVALID_ASSIGNEE` 行为、审批兼容与 progress 事件迁移
见[任务调度与派发](https://github.com/open-multi-agent/open-multi-agent/blob/main/docs/task-scheduling.md)。
## 核心能力
+119 -46
View File
@@ -8,6 +8,7 @@
*/
import type { RunOptions } from '../agent/runner.js'
import { extractJSON } from '../agent/structured-output.js'
import type {
AgentConfig,
AgentRunResult,
@@ -35,6 +36,7 @@ import {
withModelRoute,
routeMatches,
} from './agent-config.js'
import { z } from 'zod'
/**
* Partial verify config that the coordinator can emit in task JSON.
@@ -70,6 +72,119 @@ export interface ParsedTaskSpec {
verify?: ConsensusVerifyOptions | CoordinatorVerifySpec | true
}
const coordinatorVerifySchema = z.union([
z.literal(true),
z.object({
mode: z.enum(['refute', 'lens']).optional(),
quorum: z.number().int().positive().optional(),
maxRounds: z.number().int().positive().optional(),
onDissent: z.enum(['revise', 'reject', 'keep']).optional(),
}).strict(),
])
const coordinatorTaskSpecSchema = z.object({
title: z.string().trim().min(1),
description: z.string().trim().min(1),
assignee: z.string().trim().min(1).optional(),
dependsOn: z.array(z.string().trim().min(1)).optional(),
memoryScope: z.enum(['dependencies', 'all']).optional(),
maxRetries: z.number().int().nonnegative().optional(),
retryDelayMs: z.number().finite().nonnegative().optional(),
retryBackoff: z.number().finite().positive().optional(),
role: z.string().trim().min(1).optional(),
priority: z.enum(['low', 'normal', 'high', 'critical']).optional(),
requires: z.object({
requiredTools: z.array(z.string().trim().min(1)).optional(),
requiredCapabilities: z.array(z.string().trim().min(1)).optional(),
requiredBackend: z.enum(['llm', 'process', 'acp']).optional(),
requiredProvider: z.string().trim().min(1).optional(),
}).strict().optional(),
verify: coordinatorVerifySchema.optional(),
}).strict()
const coordinatorTaskSpecsSchema = z.array(coordinatorTaskSpecSchema).min(1)
/**
* Build the coordinator output contract for one team. The roster and task
* titles are runtime facts, so they belong in the same schema that triggers
* the Agent's single structured-output repair attempt.
*/
export function buildCoordinatorTaskSpecsSchema(
agents: readonly AgentConfig[],
strictAssignees: boolean,
) {
const roster = new Set(agents.map((agent) => agent.name))
const normalizeTitle = (title: string) => title.toLowerCase().trim()
return coordinatorTaskSpecsSchema.superRefine((tasks, ctx) => {
const titleIndexes = new Map<string, number[]>()
for (let index = 0; index < tasks.length; index++) {
const key = normalizeTitle(tasks[index]!.title)
const indexes = titleIndexes.get(key) ?? []
indexes.push(index)
titleIndexes.set(key, indexes)
}
for (let index = 0; index < tasks.length; index++) {
const task = tasks[index]!
if (strictAssignees && task.assignee !== undefined && !roster.has(task.assignee)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: [index, 'assignee'],
message: `Unknown assignee "${task.assignee}".`,
})
}
for (const [dependencyIndex, dependency] of (task.dependsOn ?? []).entries()) {
const matches = titleIndexes.get(normalizeTitle(dependency)) ?? []
if (matches.length !== 1) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: [index, 'dependsOn', dependencyIndex],
message: matches.length === 0
? `Unknown dependency "${dependency}".`
: `Ambiguous dependency "${dependency}".`,
})
}
}
}
for (const [title, indexes] of titleIndexes) {
if (indexes.length > 1) {
for (const index of indexes) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: [index, 'title'],
message: `Duplicate task title "${title}".`,
})
}
}
}
const visiting = new Set<number>()
const visited = new Set<number>()
const visit = (index: number): boolean => {
if (visiting.has(index)) return true
if (visited.has(index)) return false
visiting.add(index)
const task = tasks[index]!
for (const dependency of task.dependsOn ?? []) {
const matches = titleIndexes.get(normalizeTitle(dependency)) ?? []
if (matches.length === 1 && visit(matches[0]!)) return true
}
visiting.delete(index)
visited.add(index)
return false
}
if (tasks.some((_, index) => visit(index))) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: [],
message: 'Cyclic task dependency detected.',
})
}
})
}
export interface InvalidAssigneeIssue {
readonly taskTitle: string
readonly assignee: string
@@ -139,50 +254,9 @@ export function parseCoordinatorVerify(raw: unknown): CoordinatorVerifySpec | tr
* or as a bare array. Returns `null` when no valid array can be extracted.
*/
export function parseTaskSpecs(raw: string): ParsedTaskSpec[] | null {
// Strategy 1: look for a fenced JSON block
const fenceMatch = raw.match(/```json\s*([\s\S]*?)```/)
const candidate = fenceMatch ? fenceMatch[1]! : raw
// Strategy 2: find the first '[' and last ']'
const arrayStart = candidate.indexOf('[')
const arrayEnd = candidate.lastIndexOf(']')
if (arrayStart === -1 || arrayEnd === -1 || arrayEnd <= arrayStart) {
return null
}
const jsonSlice = candidate.slice(arrayStart, arrayEnd + 1)
try {
const parsed: unknown = JSON.parse(jsonSlice)
if (!Array.isArray(parsed)) return null
const specs: ParsedTaskSpec[] = []
for (const item of parsed) {
if (typeof item !== 'object' || item === null) continue
const obj = item as Record<string, unknown>
if (typeof obj['title'] !== 'string') continue
if (typeof obj['description'] !== 'string') continue
specs.push({
title: obj['title'],
description: obj['description'],
assignee: typeof obj['assignee'] === 'string' ? obj['assignee'] : undefined,
dependsOn: Array.isArray(obj['dependsOn'])
? (obj['dependsOn'] as unknown[]).filter((x): x is string => typeof x === 'string')
: undefined,
memoryScope: obj['memoryScope'] === 'all' ? 'all' : undefined,
maxRetries: typeof obj['maxRetries'] === 'number' ? obj['maxRetries'] : undefined,
retryDelayMs: typeof obj['retryDelayMs'] === 'number' ? obj['retryDelayMs'] : undefined,
retryBackoff: typeof obj['retryBackoff'] === 'number' ? obj['retryBackoff'] : undefined,
role: typeof obj['role'] === 'string' ? obj['role'] : undefined,
priority: obj['priority'] === 'low' || obj['priority'] === 'normal' || obj['priority'] === 'high' || obj['priority'] === 'critical'
? obj['priority']
: undefined,
requires: parseTaskRequirements(obj['requires']),
verify: parseCoordinatorVerify(obj['verify']),
})
}
return specs.length > 0 ? specs : null
const parsed = coordinatorTaskSpecsSchema.safeParse(extractJSON(raw))
return parsed.success ? parsed.data as ParsedTaskSpec[] : null
} catch {
return null
}
@@ -384,8 +458,7 @@ export function buildCoordinatorOutputFormatSection(hasVerifyJudges?: boolean):
' 3. Avoid adding a dependency just because the information "would be useful" or matches general best practice; if the manifest gives no indication X consumes that input, prefer to leave it out.',
' 4. When uncertain, prefer fewer dependencies over more — extra parents cost parallelism and tokens.',
'',
'Wrap the JSON in a ```json code fence.',
'Do not include any text outside the code fence.',
'Return only the JSON array. Do not use Markdown code fences or extra text.',
)
return lines.join('\n')
}
@@ -408,7 +481,7 @@ export function buildDecompositionPrompt(goal: string, agents: AgentConfig[]): s
`## Goal`,
goal,
'',
'Return ONLY the JSON task array in a ```json code fence.',
'Return ONLY the JSON task array. Do not use a Markdown code fence.',
].join('\n')
}
+62 -16
View File
@@ -80,7 +80,7 @@ import { Team } from '../team/team.js'
import { TaskQueue } from '../task/queue.js'
import { Checkpoint } from '../memory/checkpoint.js'
import { InMemoryStore } from '../memory/store.js'
import { createTask, validateTaskDependencies } from '../task/task.js'
import { validateTaskDependencies } from '../task/task.js'
import { validateTaskMetadata } from '../task/metadata.js'
import { Scheduler } from './scheduler.js'
import {
@@ -172,12 +172,13 @@ import {
} from '../eval/online.js'
import {
parseTaskSpecs,
buildCoordinatorBaseConfig,
buildCoordinatorTaskSpecsSchema,
buildDecompositionPrompt,
runCoordinatorSynthesis,
loadSpecsIntoQueue,
findInvalidAssignees,
type ParsedTaskSpec,
} from './coordinator.js'
// ---------------------------------------------------------------------------
@@ -256,7 +257,7 @@ export class OpenMultiAgent {
* - `maxDelegationDepth`: 3
* - `schedulingStrategy`: `'dependency-first'`
* - `schedulingWeights`: `{ fit: 0.7, load: 0.3 }`
* - `strictAssignees`: `false`
* - `strictAssignees`: `true`
* - `defaultModel`: `'claude-opus-4-6'`
* - `defaultProvider`: `'anthropic'`
*/
@@ -288,7 +289,7 @@ export class OpenMultiAgent {
maxDelegationDepth: config.maxDelegationDepth ?? DEFAULT_MAX_DELEGATION_DEPTH,
schedulingStrategy: config.schedulingStrategy ?? 'dependency-first',
schedulingWeights: config.schedulingWeights ?? {},
strictAssignees: config.strictAssignees ?? false,
strictAssignees: config.strictAssignees ?? true,
executionRouter: config.executionRouter ?? new DeterministicRouter(),
defaultModel: config.defaultModel ?? DEFAULT_MODEL,
defaultProvider: config.defaultProvider ?? 'anthropic',
@@ -893,7 +894,10 @@ export class OpenMultiAgent {
)
const decompositionPrompt = buildDecompositionPrompt(goal, agentConfigs)
const coordinatorAgent = buildAgent(coordinatorConfig)
const coordinatorAgent = buildAgent({
...coordinatorConfig,
outputSchema: buildCoordinatorTaskSpecsSchema(agentConfigs, this.config.strictAssignees),
})
const runId = identity.runId
const coordinatorDecomposeSpanId = this.config.onTrace ? generateSpanId() : undefined
@@ -953,7 +957,32 @@ export class OpenMultiAgent {
// ------------------------------------------------------------------
// Step 2: Parse tasks from coordinator output
// ------------------------------------------------------------------
const taskSpecs = parseTaskSpecs(decompositionResult.output)
if (!decompositionResult.success && decompositionResult.errorInfo?.kind !== 'validation') {
this.config.onProgress?.({
type: 'agent_complete',
agent: 'coordinator',
data: decompositionResult,
})
this.config.onProgress?.({
type: 'error',
data: {
code: 'COORDINATOR_DECOMPOSITION_FAILED',
error: decompositionResult.errorInfo,
},
})
return finish(this.buildTeamRunResult(
agentResults,
identity,
goal,
[],
decompositionResult.status ?? statusOnly('error'),
decompositionResult.errorInfo,
))
}
const taskSpecs = decompositionResult.success && Array.isArray(decompositionResult.structured)
? decompositionResult.structured as ParsedTaskSpec[]
: null
const queue = new TaskQueue()
const scheduler = this.createScheduler(options?.modelRouting, () => queue.list())
@@ -1034,16 +1063,33 @@ export class OpenMultiAgent {
))
}
} else {
// Coordinator failed to produce structured output — fall back to
// one task per agent using the goal as the description.
for (const agentConfig of agentConfigs) {
const task = createTask({
title: `${agentConfig.name}: ${goal.slice(0, 80)}`,
description: goal,
assignee: agentConfig.name,
})
queue.add(task)
}
// A coordinator plan is an execution boundary. Do not turn an invalid
// plan into a different topology: that could duplicate side effects.
const error = Object.assign(
new Error('Coordinator plan failed structured validation after repair.'),
{ code: 'COORDINATOR_PLAN_INVALID' },
)
const classified = classifyRunFailure(error, { kind: 'validation' })
this.config.onProgress?.({
type: 'agent_complete',
agent: 'coordinator',
data: decompositionResult,
})
this.config.onProgress?.({
type: 'error',
data: {
code: 'COORDINATOR_PLAN_INVALID',
error: classified.errorInfo,
},
})
return finish(this.buildTeamRunResult(
agentResults,
identity,
goal,
[],
classified.status,
classified.errorInfo,
))
}
const requirementIssues = validateTaskRequirements(
+3 -4
View File
@@ -1691,10 +1691,9 @@ export interface OrchestratorConfig {
/**
* Reject coordinator plans that name an assignee outside the team roster.
*
* Defaults to `false`: invalid names are cleared, a structured `warning`
* progress event is emitted, and the configured scheduler assigns the task.
* When `true`, the run terminates with a structured `INVALID_ASSIGNEE`
* validation error before any planned task executes.
* Defaults to `true`: coordinator output that names an unknown agent is
* rejected before task execution. Set to `false` only to retain legacy
* behavior that clears the assignment and lets the scheduler choose.
*/
readonly strictAssignees?: boolean
/**
+64 -43
View File
@@ -1091,9 +1091,12 @@ describe('OpenMultiAgent', () => {
expect(result.agentResults.has('coordinator')).toBe(true)
})
it('returns a validation failure for a coordinator-generated cyclic DAG', async () => {
it('repairs a coordinator-generated cyclic DAG before dispatch', async () => {
mockAdapterResponses = [
'```json\n[{"title":"Task A","description":"Do A","assignee":"worker-a","dependsOn":["Task B"]},{"title":"Task B","description":"Do B","assignee":"worker-b","dependsOn":["Task A"]}]\n```',
'```json\n[{"title":"Research","description":"Research the topic","assignee":"worker-a"}]\n```',
'research output',
'final answer',
]
const events: OrchestratorEvent[] = []
const oma = new OpenMultiAgent({
@@ -1107,31 +1110,56 @@ describe('OpenMultiAgent', () => {
'First research the topic, then produce a detailed report with recommendations.',
)
expect(result.success).toBe(false)
expect(result.status?.code).toBe('error')
expect(result.errorInfo).toMatchObject({ kind: 'validation' })
expect(result.tasks).toEqual([])
expect(capturedPrompts).toHaveLength(1)
expect(events).toContainEqual(expect.objectContaining({
type: 'error',
data: expect.objectContaining({ code: 'INVALID_TASK_DEPENDENCIES' }),
}))
expect(result.success).toBe(true)
expect(result.tasks).toHaveLength(1)
expect(capturedPrompts).toHaveLength(4)
expect(events.some((event) => event.type === 'error')).toBe(false)
})
it('falls back to one-task-per-agent when coordinator output is unparseable', async () => {
it('fails closed when coordinator output remains unparseable after repair', async () => {
mockAdapterResponses = [
'I cannot produce JSON output', // invalid coordinator output
'worker-a result',
'worker-b result',
'synthesis',
'Still not JSON', // repair also fails
]
const oma = new OpenMultiAgent({ defaultModel: 'mock-model' })
const events: OrchestratorEvent[] = []
const oma = new OpenMultiAgent({
defaultModel: 'mock-model',
onProgress: (event) => events.push(event),
})
const team = oma.createTeam('t', teamCfg())
const result = await oma.runTeam(team, 'First design the database schema, then implement the REST API endpoints')
expect(result.success).toBe(true)
expect(result.success).toBe(false)
expect(result.status?.code).toBe('error')
expect(result.errorInfo).toMatchObject({ kind: 'validation', code: 'COORDINATOR_PLAN_INVALID' })
expect(result.tasks).toEqual([])
expect(capturedPrompts).toHaveLength(2)
expect(events).toContainEqual(expect.objectContaining({
type: 'error',
data: expect.objectContaining({ code: 'COORDINATOR_PLAN_INVALID' }),
}))
})
it('rejects a partially malformed coordinator plan instead of executing its valid subset', async () => {
const malformedPlan = '```json\n[' +
'{"title":"Research","description":"Research the topic","assignee":"worker-a"},' +
'{"title":"Broken","assignee":"worker-b"}' +
']\n```'
mockAdapterResponses = [malformedPlan, malformedPlan]
const oma = new OpenMultiAgent({ defaultModel: 'mock-model' })
const team = oma.createTeam('t', teamCfg())
const result = await oma.runTeam(
team,
'First research the topic, then produce a detailed report with recommendations.',
)
expect(result.success).toBe(false)
expect(result.tasks).toEqual([])
expect(capturedPrompts).toHaveLength(2)
})
it('uses schedulingStrategy for unassigned coordinator tasks', async () => {
@@ -1263,13 +1291,14 @@ describe('OpenMultiAgent', () => {
expect(capturedPrompts).toHaveLength(1)
})
it('warns and schedules a coordinator task with an invalid assignee by default', async () => {
it('warns and schedules a coordinator task with an invalid assignee only when legacy reassignment is explicit', async () => {
mockAdapterResponses = [
'```json\n[{"title":"Research","description":"Research the topic","assignee":"ghost"}]\n```',
]
const events: OrchestratorEvent[] = []
const oma = new OpenMultiAgent({
defaultModel: 'mock-model',
strictAssignees: false,
onProgress: (event) => events.push(event),
})
const team = oma.createTeam('t', teamCfg([agentConfig('worker-a')]))
@@ -1294,14 +1323,14 @@ describe('OpenMultiAgent', () => {
})
})
it('returns a structured validation error for an invalid assignee in strict mode', async () => {
it('returns a structured validation error for an invalid assignee by default', async () => {
mockAdapterResponses = [
'```json\n[{"title":"Research","description":"Research the topic","assignee":"ghost"}]\n```',
'```json\n[{"title":"Research","description":"Research the topic","assignee":"ghost"}]\n```',
]
const events: OrchestratorEvent[] = []
const oma = new OpenMultiAgent({
defaultModel: 'mock-model',
strictAssignees: true,
onProgress: (event) => events.push(event),
})
const team = oma.createTeam('t', teamCfg([agentConfig('worker-a')]))
@@ -1316,12 +1345,12 @@ describe('OpenMultiAgent', () => {
expect(result.status?.code).toBe('error')
expect(result.errorInfo).toMatchObject({
kind: 'validation',
code: 'INVALID_ASSIGNEE',
code: 'COORDINATOR_PLAN_INVALID',
})
expect(result.tasks).toEqual([])
expect(events).toContainEqual(expect.objectContaining({
type: 'error',
data: expect.objectContaining({ code: 'INVALID_ASSIGNEE' }),
data: expect.objectContaining({ code: 'COORDINATOR_PLAN_INVALID' }),
}))
})
@@ -1621,17 +1650,13 @@ describe('OpenMultiAgent', () => {
expect(result.agentResults.get('worker')?.output).toBe('worker output')
})
it('marks tasks with unknown coordinator dependencies as failed instead of dropping them', async () => {
it('fails closed when coordinator dependencies remain unknown after repair', async () => {
let coordinatorCalls = 0
let workerCalls = 0
let synthesisPrompt = ''
const coordinatorAdapter: LLMAdapter = {
name: 'coordinator-mock',
async chat(messages: LLMMessage[]): Promise<LLMResponse> {
const prompt = extractUserPrompt(messages)
if (prompt.includes('Task Results')) {
synthesisPrompt = prompt
return textResponse('final with gap')
}
async chat(): Promise<LLMResponse> {
coordinatorCalls++
return textResponse('```json\n[{"title": "Use missing", "description": "Use a missing result", "assignee": "worker", "dependsOn": ["Missing research"]}]\n```')
},
async *stream() { yield { type: 'done' as const, data: {} } },
@@ -1654,22 +1679,18 @@ describe('OpenMultiAgent', () => {
coordinator: { adapter: coordinatorAdapter },
})
const task = result.tasks?.find((t) => t.title === 'Use missing')
expect(task?.status).toBe('failed')
expect(synthesisPrompt).toContain('Unresolved dependency reference(s): Missing research')
expect(result.success).toBe(false)
expect(result.tasks).toEqual([])
expect(coordinatorCalls).toBe(2)
expect(workerCalls).toBe(0)
})
it('fails ambiguous title dependencies when coordinator emits duplicate task titles', async () => {
let synthesisPrompt = ''
it('fails closed when duplicate coordinator task titles remain ambiguous after repair', async () => {
let coordinatorCalls = 0
const coordinatorAdapter: LLMAdapter = {
name: 'coordinator-mock',
async chat(messages: LLMMessage[]): Promise<LLMResponse> {
const prompt = extractUserPrompt(messages)
if (prompt.includes('Task Results')) {
synthesisPrompt = prompt
return textResponse('final with ambiguity')
}
async chat(): Promise<LLMResponse> {
coordinatorCalls++
return textResponse([
'```json',
'[',
@@ -1690,9 +1711,9 @@ describe('OpenMultiAgent', () => {
coordinator: { adapter: coordinatorAdapter },
})
const synth = result.tasks?.find((t) => t.title === 'Synthesize')
expect(synth?.status).toBe('failed')
expect(synthesisPrompt).toContain('Research (ambiguous duplicate title)')
expect(result.success).toBe(false)
expect(result.tasks).toEqual([])
expect(coordinatorCalls).toBe(2)
})
it('includes failed and skipped task sections in final synthesis prompt', async () => {
+9 -5
View File
@@ -169,7 +169,7 @@ describe('runTeam verify hook integration', () => {
expect(taskD!.verify).toBeUndefined()
})
it('verify field in coordinator JSON is ignored when invalid (null, number, string)', async () => {
it('rejects an invalid coordinator verify field instead of silently dropping it', async () => {
const worker = agent('worker')
const judge = agent('judge')
@@ -179,6 +179,11 @@ describe('runTeam verify hook integration', () => {
{ title: 'Task E2', description: 'Do E2', assignee: 'worker', dependsOn: [], verify: 42 },
{ title: 'Task E3', description: 'Do E3', assignee: 'worker', dependsOn: [], verify: 'yes' },
]),
coordinatorPlan([
{ title: 'Task E1', description: 'Do E1', assignee: 'worker', dependsOn: [], verify: null },
{ title: 'Task E2', description: 'Do E2', assignee: 'worker', dependsOn: [], verify: 42 },
{ title: 'Task E3', description: 'Do E3', assignee: 'worker', dependsOn: [], verify: 'yes' },
]),
]
const oma = new OpenMultiAgent({ defaultModel: 'mock-model' })
@@ -189,9 +194,8 @@ describe('runTeam verify hook integration', () => {
verifyJudges: [judge],
})
expect(result.success).toBe(true)
for (const task of result.tasks ?? []) {
expect(task.verify).toBeUndefined()
}
expect(result.success).toBe(false)
expect(result.errorInfo).toMatchObject({ kind: 'validation', code: 'COORDINATOR_PLAN_INVALID' })
expect(result.tasks).toEqual([])
})
})