Skip to content

01 β€” Selection surface: config map + insertion points (each flag its own middleware)Β #521

Description

@gewenyu99

πŸ”„ Re-spec β€” 2026-06-26 (config map + insertion points; each flag its own middleware)

Supersedes the earlier AgentBackend + selectBackend framing. The selection surface becomes a central
config map (the base) + named insertion points (middleware)
: two registries (ROUTERS, RUNNERS) and a
ROUTES map define per-program defaults ({ router, runner, model }); each decision then flows through an
ordered middleware chain at a named point (resolveRouter, resolvePair) that terminates in the map β€”
no default-resolver functions. Each existing flag is its own middleware, named for it
(wizard-orchestrator β†’ wizardOrchestrator, wizard-runner β†’ wizardRunner) β€” nothing renamed, replaced,
or created. Two stacked PRs: the map + empty points first, then the per-flag middleware.

Epic: #520 Β· Behavior change: none (defaults reproduce today exactly) Β· Depends on: #688

Summary

Replace the two switch points and the 4 hardcoded model constants with a config map + insertion points.
The base is just the map; control is asserted by middleware, one per flag. New capability: per-program
model selection
(today model is hardcoded sonnet). Existing flags keep exact behavior, now as middleware.

Scope β€” PR #692a (config map + empty insertion points, NO flags)

  • runner-plan.ts:
    • RUNNERS: Record<RunnerName, AgentRunner> β€” leaf engines (anthropic now; pi in 04 β€” pi.dev runner (pi)Β #524).
      AgentRunner.run(inputs & { model: string }) β€” drop-in for today's linear body, consuming
      BootstrapResult.
    • MODELS aliasβ†’gateway-id map (sonnet/opus). Replaces the hardcoded model in agent-interface.ts,
      agent-prompt-loader.ts, mcp-prompt-streaming.ts (+ 04 β€” pi.dev runner (pi)Β #524 pi).
    • ROUTERS β€” linearRouter now; orchestratorRouter is a separate stack.
    • ROUTES: Partial<Record<ProgramId, Route>> where Route = { router, runner, model, roles? };
      DEFAULT_ROUTE = { linear, anthropic, sonnet }. This map is the base β€” no defaultRouter/defaultPair
      functions.
    • Insertion-point plumbing: Mw<D> = (ctx, next) => D, run(chain, ctx, base), two chains
      ROUTER_MIDDLEWARE / PAIR_MIDDLEWARE initialized empty, exposed as resolveRouter / resolvePair
      whose terminal reads ROUTES[program] ?? DEFAULT_ROUTE (pair = { runner, model, ...roles?.[role] }).
  • Rewire runner/index.ts to ROUTERS[resolveRouter(ctx)].run(...); routers call resolvePair(ctx, role)
    per leaf. Empty chains β†’ every program resolves to linear / anthropic / sonnet β†’ byte-identical to today.
    Extract today's linear body into RUNNERS.anthropic verbatim.
  • Log the resolved router + runner + model every run.

Scope β€” PR #692b (per-flag middleware for the existing flags β€” adds no flags)

  • Add one middleware per existing flag, named for the flag:
    • wizardOrchestrator (ROUTER_MIDDLEWARE): wizard-orchestrator === 'true' β†’ assert orchestrator,
      else next().
    • wizardRunner (PAIR_MIDDLEWARE): defers-then-modifies β€” ALWAYS calls next() first to get the
      base pair, then overlays only the runner field iff wizard-runner ∈ {anthropic, pi}. The model
      always comes from the config map, never the flag. (Note: this is not the assert-or-defer shape of
      wizardOrchestrator β€” it never short-circuits; pattern: const p = next(); const r = flags['wizard-runner']; return (r==='anthropic'||r==='pi') ? {...p, runner:r} : p.)
  • Construct ResolveCtx (flags + env) once after bootstrap; hand resolvePair into the RouterContext so
    every leaf (linear's one, orchestrator's many) flows through the same point.
  • No PostHog flag changes. wizard-orchestrator / wizard-runner keep their current definitions.
  • (No standalone guard middleware β€” dropped.)

Acceptance criteria

  • pnpm build && pnpm test && pnpm fix pass; existing e2e unchanged; with the flag middleware and no flag
    set, every program resolves to linear / anthropic / sonnet (behavior identical).
  • Chains unit-tested: empty chains β†’ map defaults; wizardOrchestrator / wizardRunner each in
    isolation (assert-vs-defer).
  • wizard-orchestrator / wizard-runner behave exactly as before β€” asserted by test.
  • Resolved router + runner + model log line present.

Files

  • src/lib/agent/runner/runner-plan.ts (registries, MODELS, ROUTES/DEFAULT_ROUTE, run/chains,
    resolveRouter/resolvePair, linearRouter, the per-flag middleware), runner/index.ts,
    runner/backends/anthropic.ts, agent-interface.ts, agent-prompt-loader.ts, mcp-prompt-streaming.ts.
    (src/lib/constants.ts flag keys unchanged.)

Notes

The orchestrator is not a runner β€” it's a Router (separate stack on this core). #692a is the config map

  • empty points (the "clean base"); #692b adds one named middleware per existing flag. Future controls (cost
    caps, per-org policy, A/B, a model-override flag) are each their own new middleware at these same points β€” no
    rewrite.

Landed as (2026-06-27)

Implemented across #692 (pi/01-seam-flag) and #693 (pi/02-pi-backend), not a separate #692a/#692b:

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions