merge_plans
Combine multiple sub-agent plans and identify hard conflicts prior to applying changes.
Instructions
Deterministically merge multiple sub-agent plans and detect hard conflicts before apply.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| plans | Yes | ||
| fail_on_conflict | No | ||
| require_shared_base_revision | No |
Implementation Reference
- Main handler function `mergePlans` that takes an object with `plans`, `fail_on_conflict`, and `require_shared_base_revision`. Normalizes plans, detects conflicts (base revision mismatches, duplicate step IDs, overlapping replace ranges, insert slot collisions), and returns a merged plan or conflict diagnostics.
export async function mergePlans( params: { plans: unknown; fail_on_conflict?: boolean; require_shared_base_revision?: boolean; }, ): Promise<ToolResponse> { try { const failOnConflict = asBoolean(params.fail_on_conflict, true); const requireSharedBaseRevision = asBoolean(params.require_shared_base_revision, true); const normalized = normalizePlans(params.plans); const normalizedPlans = normalized.plans; const flattenedSteps = normalizedPlans.flatMap((plan) => plan.steps); const revisionCheck = detectBaseRevisionConflicts(normalizedPlans, requireSharedBaseRevision); const conflicts: Conflict[] = [ ...normalized.conflicts, ...revisionCheck.conflicts, ...detectDuplicateStepIdConflicts(flattenedSteps), ...detectReplaceConflicts(flattenedSteps), ...detectInsertSlotCollisions(flattenedSteps), ]; const hasConflicts = conflicts.length > 0; const mergedPlan = { format: 'safe_docx_merged_plan_v1', generated_at: new Date().toISOString(), base_revision: revisionCheck.mergedBaseRevision, plan_count: normalized.plan_count, step_count: flattenedSteps.length, steps: flattenedSteps, }; if (hasConflicts && failOnConflict) { return { success: false, error: { code: 'PLAN_CONFLICT', message: `Detected ${conflicts.length} hard conflict(s) while merging plans.`, hint: 'Resolve reported conflicts, or set fail_on_conflict=false to inspect diagnostics without hard failure.', }, has_conflicts: true, conflict_count: conflicts.length, conflicts, merged_plan: mergedPlan, }; } return ok({ has_conflicts: hasConflicts, conflict_count: conflicts.length, conflicts, merged_plan: mergedPlan, conflict_policy: { fail_on_conflict: failOnConflict, require_shared_base_revision: requireSharedBaseRevision, }, }); } catch (e: unknown) { const msg = errorMessage(e); return err('MERGE_PLAN_ERROR', `Failed to merge plans: ${msg}`); } } - packages/docx-mcp/src/server.ts:99-100 (registration)Case branch in the server's tool dispatcher that routes 'merge_plans' to the mergePlans function (imported from './tools/merge_plans.js').
case 'merge_plans': return await mergePlans(args as Parameters<typeof mergePlans>[0]); - Tool catalog entry defining the 'merge_plans' tool: Zod input schema with `plans: array`, `fail_on_conflict?: boolean`, `require_shared_base_revision?: boolean`, description, and readOnlyHint annotation.
name: 'merge_plans', description: 'Deterministically merge multiple sub-agent plans and detect hard conflicts before apply.', input: z.object({ plans: z.array(PLAN_OBJECT_SCHEMA), fail_on_conflict: z.boolean().optional(), require_shared_base_revision: z.boolean().optional(), }), annotations: { readOnlyHint: true, destructiveHint: false }, }, - `normalizePlans` helper function that validates and normalizes raw plan input into structured NormalizedPlan objects, collecting hard conflicts for invalid input.
function normalizePlans( plansRaw: unknown, ): { plans: NormalizedPlan[]; conflicts: Conflict[]; plan_count: number } { if (!Array.isArray(plansRaw)) { return { plans: [], plan_count: 0, conflicts: [ { code: 'INVALID_INPUT', severity: 'hard', message: "'plans' must be an array.", step_refs: [], }, ], }; } const plans: NormalizedPlan[] = []; const conflicts: Conflict[] = []; for (let pIdx = 0; pIdx < plansRaw.length; pIdx += 1) { const planObj = asRecord(plansRaw[pIdx]); const planId = asTrimmedString(planObj?.plan_id) ?? `plan_${pIdx + 1}`; if (!planObj) { conflicts.push({ code: 'INVALID_PLAN', severity: 'hard', message: `Plan at index ${pIdx} is not an object.`, step_refs: [], }); continue; } const baseRevisionRaw = planObj.base_revision; const baseRevision = baseRevisionRaw == null ? null : asInteger(baseRevisionRaw); if (baseRevisionRaw != null && baseRevision == null) { conflicts.push({ code: 'INVALID_BASE_REVISION', severity: 'hard', message: `Plan '${planId}' has invalid base_revision. Expected a non-negative integer.`, step_refs: [], }); } const stepsRaw = planObj.steps; if (!Array.isArray(stepsRaw)) { conflicts.push({ code: 'INVALID_PLAN_STEPS', severity: 'hard', message: `Plan '${planId}' must include a 'steps' array.`, step_refs: [], }); continue; } const normalizedSteps: NormalizedStep[] = []; for (let sIdx = 0; sIdx < stepsRaw.length; sIdx += 1) { const stepObj = asRecord(stepsRaw[sIdx]); const generatedStepId = buildAutoStepId(pIdx, sIdx); const providedStepId = asTrimmedString(stepObj?.step_id); const finalStepId = providedStepId ?? generatedStepId; if (!stepObj) { conflicts.push({ code: 'INVALID_STEP', severity: 'hard', message: `Step ${sIdx} in plan '${planId}' is not an object.`, step_refs: [{ plan_id: planId, plan_index: pIdx, step_index: sIdx, step_id: finalStepId }], }); continue; } const operation = normalizeOperation(stepObj.operation ?? stepObj.op); if (!operation) { conflicts.push({ code: 'INVALID_STEP_OPERATION', severity: 'hard', message: `Step '${finalStepId}' in plan '${planId}' has unsupported operation.`, step_refs: [{ plan_id: planId, plan_index: pIdx, step_index: sIdx, step_id: finalStepId }], }); continue; } const baseStep: Omit<NormalizedStep, 'operation'> = { step_id: finalStepId, source_plan_id: planId, source_plan_index: pIdx, source_step_index: sIdx, note: asTrimmedString(stepObj.note), arguments: cloneArguments(stepObj), }; if (operation === 'replace_text') { const targetParagraphId = asTrimmedString(stepObj.target_paragraph_id); if (!targetParagraphId) { conflicts.push({ code: 'INVALID_STEP_TARGET', severity: 'hard', message: `replace_text step '${finalStepId}' in plan '${planId}' requires target_paragraph_id.`, step_refs: [{ plan_id: planId, plan_index: pIdx, step_index: sIdx, step_id: finalStepId }], }); continue; } const range = extractRange(stepObj); if (range === 'invalid') { conflicts.push({ code: 'INVALID_STEP_RANGE', severity: 'hard', message: `replace_text step '${finalStepId}' in plan '${planId}' has invalid range/span metadata.`, paragraph_id: targetParagraphId, step_refs: [{ plan_id: planId, plan_index: pIdx, step_index: sIdx, step_id: finalStepId }], }); continue; } normalizedSteps.push({ ...baseStep, operation, target_paragraph_id: targetParagraphId, range: range ?? undefined, }); continue; } const anchorParagraphId = asTrimmedString(stepObj.positional_anchor_node_id) ?? asTrimmedString(stepObj.anchor_paragraph_id); if (!anchorParagraphId) { conflicts.push({ code: 'INVALID_STEP_TARGET', severity: 'hard', message: `insert_paragraph step '${finalStepId}' in plan '${planId}' requires positional_anchor_node_id.`, step_refs: [{ plan_id: planId, plan_index: pIdx, step_index: sIdx, step_id: finalStepId }], }); continue; } const position = (asTrimmedString(stepObj.position) ?? 'AFTER').toUpperCase(); if (position !== 'BEFORE' && position !== 'AFTER') { conflicts.push({ code: 'INVALID_STEP_POSITION', severity: 'hard', message: `insert_paragraph step '${finalStepId}' in plan '${planId}' has invalid position '${String(stepObj.position)}'.`, paragraph_id: anchorParagraphId, step_refs: [{ plan_id: planId, plan_index: pIdx, step_index: sIdx, step_id: finalStepId }], }); continue; } normalizedSteps.push({ ...baseStep, operation, positional_anchor_node_id: anchorParagraphId, position: position as 'BEFORE' | 'AFTER', }); } plans.push({ plan_id: planId, base_revision: baseRevision, source_plan_index: pIdx, steps: normalizedSteps, }); } return { plans, conflicts, plan_count: plansRaw.length, }; } - `detectBaseRevisionConflicts` helper that checks whether all plans share the same base_revision and reports mismatches when requireSharedBaseRevision is true.
function detectBaseRevisionConflicts( plans: NormalizedPlan[], requireSharedBaseRevision: boolean, ): { conflicts: Conflict[]; mergedBaseRevision: number | null } { const revisions = new Set<number>(); for (const plan of plans) { if (plan.base_revision == null) continue; revisions.add(plan.base_revision); } if (revisions.size === 0) { return { conflicts: [], mergedBaseRevision: null }; } const mergedBaseRevision = revisions.size === 1 ? [...revisions][0]! : null; if (!requireSharedBaseRevision || revisions.size <= 1) { return { conflicts: [], mergedBaseRevision }; } return { mergedBaseRevision, conflicts: [ { code: 'BASE_REVISION_CONFLICT', severity: 'hard', message: `Submitted plans have mismatched base_revision values: ${[...revisions].sort((a, b) => a - b).join(', ')}.`, step_refs: [], details: { base_revisions: [...revisions].sort((a, b) => a - b), }, }, ], }; }