fix(debate): route advocate + critic through the same persona→backend resolver as council - #19
Open
KSiig wants to merge 1 commit into
Open
fix(debate): route advocate + critic through the same persona→backend resolver as council#19KSiig wants to merge 1 commit into
KSiig wants to merge 1 commit into
Conversation
… resolver as council Debate mode resolved every persona through `resolveAdvisor`, which is inline-only — a CLI-backed persona (the default route for the bundled architect/critic, which carry no `defaultModel` and fall through to `config.modes.solo.model`) silently broke with "no api key resolved" from `getAuth` (advisor.ts:50) before round 1 ran. Council mode handled the same scenario correctly via `resolveCouncilMembers`; debate never got the equivalent routing. Extracts the per-persona resolver into `src/resolve-side.ts` so both modes share the same inline-vs-CLI decision (council §1: persona-scoped backend takes precedence over the legacy `backends[modelKey]` map). Council wraps it with its pre-failure convention (one bad persona mustn't kill the council); debate calls it directly and bails on any failure (sequential rounds can't tolerate a missing seat). The synthesizer stays inline-only to match council mode's existing behavior — if a CLI-backed synthesizer is the intended route, that's a pre-existing shared constraint, not a regression. - src/resolve-side.ts — new shared kernel (resolveSide, ResolvedSide). - src/council.ts — resolveCouncilMembers becomes a thin loop over resolveSide; ResolvedMember aliased to ResolvedSide. - src/debate.ts — advocate/critic resolve through resolveSide; callStep dispatches on kind (inline → callAdvisor, CLI → callCliAdvisor). - tests/debate-cli.test.ts — 9 new cases mirroring council-cli.test.ts. - SPEC.md + README.md — note CLI support in the debate section. Tests: 268 → 277 (all green). Typecheck clean.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this PR fixes
Debate mode (
consult({ mode: "debate" })) silently fails withRound 1 advocate failed: no api key resolvedwhenever a persona is configured to route through a CLI backend (the default for the bundledarchitect/critic, which carry nodefaultModeland fall through toconfig.modes.solo.model). Council mode handles the same scenario correctly viaresolveCouncilMembers; debate never got the equivalent routing.The root cause:
debate.ts:executeDebateresolved every persona throughresolveAdvisor, which is inline-only. When a persona's intended route is a CLI backend, the registry lookup misses andgetAuth(advisor.ts:50) returns the"no api key resolved"error thatconsultsurfaces verbatim.How
Extracts the per-persona resolver into a shared kernel (
src/resolve-side.ts) so council and debate use the same inline-vs-CLI decision. Council wraps it with its pre-failure convention (one bad persona doesn't kill the council); debate calls it directly and bails on any failure (sequential rounds can't tolerate a missing seat).src/resolve-side.ts— newresolveSide(persona, rawPersona, config, resolveAdvisorFn), returns a discriminatedResolvedSide(kind: "inline" | "cli"). Pure, registry-injected for testability.src/council.ts—resolveCouncilMembersbecomes a thin loop overresolveSide(behavior identical, ~30 lines deleted).ResolvedMemberis now an alias ofResolvedSide.src/debate.ts— advocate + critic resolve throughresolveSide.callStepdispatches onkind: inline →callAdvisor(existing path), CLI →callCliAdvisor(mirrorscouncil.ts:runMember). Synthesizer stays inline-only to match council mode's existing behavior.tests/debate-cli.test.ts— 9 new cases mirroringcouncil-cli.test.ts: inline fallback for personas withoutdefaultModel, preset CLI routing, custom CLI with declared/unknown window, persona-scoped backend precedence, same-model-different-routes.SPEC.md(debate section + §B),README.md(debate row) — note that advocate/critic honour persona-scoped CLI routing identically to council seats.Why this and not something simpler
resolveAdvisorto handle CLI? It doesn't know about persona-scoped backends or the legacybackends[modelKey]map — that'sresolvePersonaBackend's job inconfig.ts. Lifting that intoadvisor.tswould cross a module boundary and pull CLI types into the registry abstraction.council.ts:104, never viaresolveCouncilMembers). Adding CLI support to the synthesizer is a separate, larger change that affects both modes — out of scope here. Noted indebate.tsas a pre-existing shared constraint.Test plan
cd packages/bpx-consult && npx tsc --noEmit— clean.cd packages/bpx-consult && npx vitest run— 277/277 pass (was 268 + 9 new indebate-cli.test.ts).cd packages/bpx-consult && npm pack --dry-run—src/resolve-side.ts(3.5kB) included in tarball.Risk assessment
Low. The change is additive on the council side (no behavior change — the refactor preserves
resolveCouncilMembers's return shape and the 11 existingcouncil-cli.test.tscases still pass). On the debate side, the persona→backend routing is brand-new but theresolveSidehelper itself is exercised by council's existing 11 test cases, so the kernel is well-covered; the 9 newdebate-cli.test.tscases guard the debate-specific wiring.Backwards compatibility. The public API of
executeDebate,DebateDetails,formatDebatePartialis unchanged.resolveCouncilMemberskeeps its existing signature; only the implementation was simplified. Any consumer importing these from@booplex/bpx-consultis unaffected.Open question (not blocking). The synthesizer's inline-only constraint is shared with council. If
bpx-consultusers have configured a CLI-backed synthesizer undermodes.council.synthesizer, both council and debate silently fail today. Happy to follow up with a separate PR if the maintainers agree the constraint should be lifted.🤖 Generated with pi. Co-authored-by: pi pi@anthropic.com