Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion packages/moss-agent/src/plan-execute/plan-completion-gate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,15 @@ export function evaluatePlanCompletionGate(
return { ok: true }; // 故障 fail-open
}
if (!plan) return { ok: true };
if (plan.status !== 'approved' && plan.status !== 'executing') return { ok: true };
// Only guard plans that are actively executing. An approved-but-not-started
// plan (status==='approved', steps still pending) has NOT begun execution, so
// ending the turn there is not "slacking off mid-run" — the gate must not
// fire. Firing on approved-only plans deadlocks: the correction tells the
// model to plan_step skip the remaining steps, but skipStep rejects plans
// that aren't executing (a step must be in_progress), so the model retries
// until the tool-loop guard halts the run. See plan-completion-gate.spec.mjs
// Case 10.
if (plan.status !== 'executing') return { ok: true };

const total = plan.steps.length;
const done = plan.steps.filter(
Expand Down
22 changes: 22 additions & 0 deletions packages/moss-agent/test/plan-completion-gate.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -116,4 +116,26 @@ function makePlan({ steps, status, completeSteps = [], skipSteps = [] }) {
}
}

// Case 10: approved but NOT started (0/N steps, plan.status==='approved') -> PASS.
// Rationale: the gate's job is "don't false-complete a plan mid-execution".
// An approved-but-not-started plan hasn't begun executing, so ending there is
// not "slacking off mid-run" — it's the model choosing not to execute yet.
// Blocking it forces the model to skip steps to escape, but skipStep rejects
// non-executing plans (step must be in_progress), which deadlocks the loop
// guard -> crashed runs. So the gate must NOT fire on approved-only plans.
{
const c = new PlanExecuteController({ maxReplans: 3, requireApproval: false, autoApproveSimple: false });
const plan = c.createPlan('goal', ['s1', 's2']);
c.approvePlan(plan.id); // approved, 0/2, NOT started (step1 still pending, not in_progress)
assert.equal(plan.status, 'approved');
const prev = process.env.MOSS_PLAN_GATE;
try {
delete process.env.MOSS_PLAN_GATE;
const r = evaluatePlanCompletionGate({ sessionKey: 's', stopReason: 'end_turn' }, { getActivePlanForSession: () => plan });
assert.equal(r.ok, true, 'approved-but-not-started plan -> gate does not fire (no deadlock)');
} finally {
if (prev !== undefined) process.env.MOSS_PLAN_GATE = prev;
}
}

console.log('plan-completion-gate: ok');