Skip to content

feat(task): auto-generate autorouting tiers from declared providers - #3764

Open
Yeachan-Heo wants to merge 2 commits into
devfrom
feat/autorouting
Open

feat(task): auto-generate autorouting tiers from declared providers#3764
Yeachan-Heo wants to merge 2 commits into
devfrom
feat/autorouting

Conversation

@Yeachan-Heo

Copy link
Copy Markdown
Owner

Turns sub-agent autorouting from a hand-authored tier map into a declare-your-providers flow: pick the providers you have, in priority order, and gjc materializes deterministic fast/balanced/strong cross-provider fallback chains for you.

Builds on the opt-in autorouting feature in 982e6b9.

The UX

/model  →  presets landing  →  [Smart routing]  →  panel

In the panel you set provider order (the only priority channel) and an optional model allowlist (filters eligibility, never reorders), see a live preview of the generated chains, and hit Apply. That commits tiers + setup + provenance in one atomic batch — what you previewed is exactly what is written.

Then a Task spawn only needs tier: fast|balanced|strong. Tier-by-tier model selection is gone.

What's in it

Curation & generation

  • Curated per-model tier-label map in packages/coding-agent, seeded 1:1 from the shipped presets, with a skip list and version fingerprint. Only current models carry labels, so "newest only" is a curation discipline rather than recency-filter code.
  • Pure, clock-free generator over the full catalog. Identical inputs produce byte-identical chains: declaration order → curation rank → stable tie-break, deduped. Credentials and disabled providers are routing-time facts, never generation inputs — adding an API key cannot silently change routing.
  • A provider lacking a label for a tier contributes nothing to it. Unfillable tiers stay absent and fall through to the existing manual fallback rather than being padded with a model never labeled for that tier.
  • CI forcing gate wired into both required root checks: fails when a new current catalog model is neither labeled nor skip-listed.

Settings & TUI

  • Typed task.autorouting.setup / .provenance behind a narrow optional-object setting kind, wired through schema, validation, reconciliation and JSON-schema generation. Both default to absent, so untouched configs round-trip byte-identically and the disabled path is unchanged.
  • Smart-routing panel off the /model presets landing. Refresh regenerates from the recorded declaration; Clear removes the generated keys while preserving preset and enabled; the toggle writes only enabled. Hand-edited tiers are detected by fingerprint and never overwritten without explicit confirmation.

Runtime

  • Autorouting preflight for initial routed tasks: a bounded ledger of at most three unique candidates spanning throwaway probes and the durable attempt. Only typed transient pre-start failures advance; anything unclassified is terminal; nothing switches once the request fence is crossed — still no mid-run failover.
  • Failed attempts publish nowhere. Staged sessions suppress breadcrumbs and session-list registration until a single commit point, staged paths are excluded from every discovery reader, and artifacts are attempt-scoped with a serialized reservation and a frozen id map applied before the transcript becomes visible.
  • Routing evidence records bounded skips that distinguish disabled providers from missing snapshot entries and missing credentials, a phase-tagged attempt ledger, and terminal outcomes that survive even when no candidate executes.

Non-goals (unchanged)

No dynamic runtime tier derivation, no credential-based auto-activation, no mid-run failover, no cross-provider scalar quality ranking, no CLI/wizard as the primary surface, no general model-selection presets. Manual tiers and preset remain the power-user escape hatch.

Verification

bun test <11 autorouting/staging/boundary suites>   # 130 pass / 0 fail / 616 expects
cd packages/coding-agent && bun run check:types      # 0
cd packages/coding-agent && bun run check:autorouting-map  # 0 — 3930 in-scope keys, 3915 baseline skips
bun run check:schemas                                # 0
git diff --exit-code -- packages/ai schemas/models.schema.json  # clean
bunx biome check packages/coding-agent/src           # 0

packages/ai and schemas/models.schema.json are untouched by design.

Pre-existing and unrelated (reproduced on a clean worktree at the base commit): 4 FileSessionStorageWriter path-security failures in session-storage.test.ts, and root check:sdk-closure.

Review

Eight review generations across an ai-slop-cleaner lane, an architect lane and an executor QA/red-team lane closed 25 findings, then a terminal critic signed off. Four were genuine defects that would otherwise have shipped:

  • a managed-acceptance not_directory bug that made managed autorouted publication impossible;
  • a ledger advance/terminal conflation that let terminal classifications advance to the next candidate;
  • a diagnostic-egress leak of raw stacks including secrets and absolute paths;
  • a hand-edit confirmation that silently downgraded a guarded Apply into a Refresh, discarding the user's edited draft.

The adversarial suite (autorouting-boundary-redteam.test.ts, 39 cases) covers forged previews, catalog races, attempt-id traversal, cyclic/substring/nested URI re-keying, bounded skip projection, breadcrumb interleaving, cleanup-failure surfacing, and leaked-id retirement.

Accepted limitations

  • v1 labels are preset-seeded; a broader benchmark-informed re-curation is a follow-up.
  • Concurrent preflight attempts and mid-commit crash recovery are out of scope.
  • The catalog fingerprint covers provider/id identity only.

Yeachan-Heo added 2 commits August 1, 2026 00:07
Adds a fixed fast/balanced/strong tier vocabulary for Task-tool spawns,
gated behind `task.autorouting.enabled` (default false). Tiers come from
`task.autorouting.tiers` or one of the locked presets (anthropic,
openai-codex, google, xai); explicit tiers win over a preset and an
omitted item tier routes as balanced. An autorouting pin overrides the
manual model chain, unmatched tiers fall back to manual resolution per
item with a bounded reason, and routing evidence propagates to the task
result, receipt, renderer, and task-summary prompt.

Selectors must be exact provider-qualified `provider/modelId` strings
with an optional thinking suffix; globs, bare ids, and `pi/<role>`
aliases are rejected by the generated config schema. With autorouting
disabled the model-resolution path is unchanged.
Turn sub-agent autorouting from a hand-authored tier map into a
declare-your-providers flow: pick the providers you have, in priority
order, and gjc materializes deterministic fast/balanced/strong
cross-provider fallback chains for you.

Curation and generation:
- Add a curated per-model tier-label map in packages/coding-agent,
  seeded 1:1 from the shipped presets, with a skip list and a version
  fingerprint. Only current models carry labels, so "newest only" is a
  curation discipline rather than recency-filter code.
- Add a pure, clock-free generator over the full model catalog. Given a
  declaration it emits byte-identical chains: declaration order, then
  curation rank, then a stable tie-break, deduped. Credentials and
  disabled providers are routing-time facts and never generation
  inputs, so adding an API key cannot silently change routing.
- A provider that lacks a label for a tier contributes nothing to it.
  Unfillable tiers stay absent and fall through to the existing manual
  fallback rather than being padded with a model that was never
  labeled for that tier.
- Add a CI forcing gate, wired into both required root checks, that
  fails when a new current catalog model is neither labeled nor
  skip-listed.

Settings and TUI:
- Add typed task.autorouting.setup and .provenance behind a narrow
  optional-object setting kind, wired through the schema, validation,
  reconciliation and JSON-schema generation. Both default to absent, so
  untouched configs round-trip byte-identically and the disabled path
  is unchanged.
- Add a smart-routing panel reachable from the /model presets landing.
  It edits provider order and an optional model allowlist, previews the
  generated chains, and commits tiers + setup + provenance in one
  atomic batch, so what you previewed is exactly what is written.
  Refresh regenerates from the recorded declaration, Clear removes the
  generated keys while preserving preset and enabled, and the toggle
  writes only enabled. Hand-edited tiers are detected by fingerprint
  and never overwritten without explicit confirmation.

Runtime:
- Add an autorouting preflight mode for initial routed tasks: a bounded
  ledger of at most three unique candidates spanning throwaway probes
  and the durable attempt. Only typed transient pre-start failures
  advance; anything unclassified is terminal, and nothing switches once
  the request fence is crossed, so there is still no mid-run failover.
- Publish failed attempts nowhere. Staged sessions suppress breadcrumbs
  and session-list registration until a single commit point, staged
  paths are excluded from every discovery reader, and artifacts are
  attempt-scoped with a serialized reservation and a frozen id map that
  is applied before the transcript becomes visible.
- Record why routing did what it did: bounded skip evidence that
  distinguishes disabled providers from missing snapshot entries and
  missing credentials, a phase-tagged attempt ledger, and terminal
  outcomes that survive even when no candidate ever executes.

The declaration is the only priority channel; the optional model list
filters eligibility and never reorders. Manual tiers and presets remain
as the power-user escape hatch.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f0462a8ba2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1882 to +1888
await this.ctx.settings.commitAtomicBatchWithCurrent(() =>
buildAutoroutingSettingsBatch({
tiers: preview.tiers,
setup: preview.setup,
provenance: preview.provenance,
}),
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Recheck hand edits inside the atomic callback

When another process edits the generated tiers after the settings snapshot was loaded—or between #assertSmartRoutingNotHandEdited and this write—the guard examines stale in-memory data, while this callback ignores the current on-disk configuration supplied by commitAtomicBatchWithCurrent. Apply therefore silently overwrites the concurrent hand edit without the required confirmation; Refresh repeats the same pattern. Re-evaluate the provenance/tier fingerprint from current inside the locked callback before returning the patches.

Useful? React with 👍 / 👎.

const attempts: AutoroutingAttempt[] = [];
const consumed = new Set<string>();
const skips = buildBoundedRoutingSkips(options.autoroutingSkips);
const candidates = options.autoroutingCandidates.map(boundedSelector).filter(selector => selector.length > 0);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve live selectors before preflight

When a valid custom provider/model selector exceeds 256 characters or contains NFKC-sensitive characters, this applies evidence sanitization to the selector before it is used as modelOverride. Although the original selector already matched the registry snapshot, the truncated or normalized value no longer identifies that model, so preflight can incorrectly exhaust the route. Keep the original candidates for execution and call boundedSelector only when recording routing evidence.

Useful? React with 👍 / 👎.

Comment on lines +916 to +918
if (result.routing) {
const model = result.routing.effectiveModel ?? "not-executed";
lines.push(`${continuePrefix}${theme.fg("dim", `Routing: ${model} ${result.routing.note ?? ""}`)}`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Sanitize routing text before rendering

When a custom provider reports a model identifier containing tabs or ANSI/control sequences, effectiveModel is interpolated directly into the Task tool renderer, allowing malformed alignment or terminal escape injection; the new routing note is also rendered without the shared width bound. Apply replaceTabs() and truncateToWidth()/ui.truncate() to the complete routing line before passing it to the theme renderer.

AGENTS.md reference: AGENTS.md:L115-L121

Useful? React with 👍 / 👎.

@Yeachan-Heo

Copy link
Copy Markdown
Owner Author

Computer/task backlog ownership routing

Exact head f0462a8ba248632881a70056aebd37c2d73686b1 is not review-eligible: GitHub reports mergeable_state: dirty against dev, and no CI checks exist for this head. The existing mutation owner should resume this branch; no duplicate mutation or CI action was started.

Signature: GJC backlog census | PR #3764 | exact-head f0462a8ba248632881a70056aebd37c2d73686b1 | OWNER_RESUME_REQUIRED

@Yeachan-Heo

Copy link
Copy Markdown
Owner Author

Signed task-autorouting conflict handoff

Owner: existing branch/PR mutation owner @Yeachan-Heo
Exact PR head: f0462a8ba248632881a70056aebd37c2d73686b1
Rebase target: current dev at aebb0f6d119acf178a9c710f460f83e9735bb8e6
Merge base: 087a01c268d1a9447c896d3e362d90391b8fd3ba

A three-way git merge-tree census found four textual conflict files:

  1. packages/coding-agent/src/config/settings-schema.ts — three hunks. Preserve current-dev array/record validation behavior while integrating the PR's constrained-record and optional-object autorouting schema types and their specialized validation.
  2. packages/coding-agent/src/session/session-manager.ts — one hunk. Preserve current-dev cwdChanged rewrite/flush bookkeeping, then use the PR's instance #writeTerminalBreadcrumb(resolved) path.
  3. packages/coding-agent/src/task/executor.ts — one hunk. Reconcile current-dev managed-session opening with the PR's probe/durable staged-session preflight flow; preserve parent artifact adoption only for the non-preflight path and pass the current worktree/cwd contract where required.
  4. packages/coding-agent/src/task/types.ts — one hunk. Retain current-dev TaskRecoveryArtifactRef/recovery receipt declarations and append the PR's autorouting evidence, attempt, skip, and preflight failure types rather than choosing either side.

Scope boundary: resolve only this rebase and directly required generated/schema/test fallout for the task-autorouting change. Do not absorb unrelated source cleanup. Reconfirm the branch head before pushing because this handoff is signed only for the SHA pair above.

No duplicate mutation owner was started, and no CI workflow was rerun or cancelled.

Signature: GJC task-autorouting batch lane | owner Yeachan-Heo | PR #3764@f0462a8ba248632881a70056aebd37c2d73686b1 | dev aebb0f6d119acf178a9c710f460f83e9735bb8e6 | SCOPE_OWNER_HANDOFF

@Yeachan-Heo

Copy link
Copy Markdown
Owner Author

Owner hostile review — REQUEST_CHANGES

Exact revision and CI ledger

  • PR head: f0462a8ba248632881a70056aebd37c2d73686b1 (feat/autorouting)
  • GitHub PR base snapshot: ffe2ca4fe9ef02f4321a13035e74ffb31a9144b9
  • Live target: dev@732856b3ccb3fade6e9fbc17908a4fbca5a7682f
  • Merge base: 087a01c268d1a9447c896d3e362d90391b8fd3ba; head is 2 commits ahead and 241 behind live dev
  • GitHub: mergeable=false, mergeable_state=dirty, rebaseable=false
  • Exact head CI: 0 check runs / 0 status contexts
  • PR base snapshot Dev CI run 30771702761: terminal red in coding-agent shard 8, evidence producer, and aggregate
  • Live-dev CI run 30935832940: terminal red in the same three jobs

No workflow was rerun, cancelled, or otherwise controlled.

Contributor ledger

  • Branch and both commits (982e6b920d4ff6e6cc49397bb121214b98274e61, f0462a8ba248632881a70056aebd37c2d73686b1) are authored by @Yeachan-Heo.
  • The exact-head Codex review contributed three still-present P2 findings; this GPT-heavy owner pass revalidated them rather than dropping that review signal.

Blocking findings

  1. P1 — the branch is not integrable against the current target. A current three-way census has textual conflicts in five files: config/settings-schema.ts, session/artifacts.ts, session/session-manager.ts, task/executor.ts, and task/types.ts. Rebase resolution must preserve current-dev notification setting paths and enum-array validation; serialized artifact initialization/allocation and ephemeral-artifact retirement; cwdChanged rewrite/flush and instance breadcrumb handling; current managed-session/worktree opening and non-preflight parent-artifact adoption; and recovery artifact/receipt types.
  2. P1 — post-rename failures lose cleanup authority. Managed staged publication can rename the transcript or artifact and then throw during post-move verification/fsync. The caller records publication only after the awaited move returns, so the failure path can leave a discoverable final transcript or artifact while treating the operation as pre-publication. Artifact IDs may then be rewound/reused.
  3. P1 — normal parallel Task children race the shared parent artifact allocator. commitAttemptStaging() reserves and publishes through shared mutable #nextId/reservation state across awaits without serialization. Parallel routed children can scan/reserve the same range or reuse uncertain IDs after rollback.
  4. P1 — direct rollback is pathname-based, not identity-bound. Later setup failure removes the final path, and committed rollback checks bytes before deleting by pathname. A replacement in the check/delete window can be deleted as though it were the staged object.
  5. P2 — hand edits are checked before, not inside, the atomic write lock. Apply/Refresh can overwrite a concurrent manual tier edit without confirmation because commitAtomicBatchWithCurrent ignores the locked current configuration. See discussion_r3700575612.
  6. P2 — evidence bounding mutates live execution selectors. autoroutingCandidates.map(boundedSelector) normalizes/truncates selectors before assigning modelOverride, so a valid long or NFKC-sensitive registered selector can become a different, unresolvable model. See discussion_r3700575613.
  7. P2 — routing output bypasses TUI sanitization/width limits. effectiveModel and note are rendered raw, permitting tabs/control sequences and unbounded layout. See discussion_r3700575617.

The current head is therefore not review-eligible. Rebase onto the exact live target, preserve the conflict invariants above, fix the publication/allocation/identity blockers and the three exact-head review findings, then publish a new SHA with fresh exact-head checks. After those changes, the unjournaled artifact→transcript crash window still requires explicit owner confirmation unless a persisted recovery contract is added.

Signature: GJC owner batch | PR #3764@f0462a8ba248632881a70056aebd37c2d73686b1 | target dev@732856b3ccb3fade6e9fbc17908a4fbca5a7682f | REQUEST_CHANGES

@Yeachan-Heo

Copy link
Copy Markdown
Owner Author

Maintainer triage — LEFT OPEN, blocked (not mergeable in current state)

Verified against current dev (head 47099f0e3, after #3767 merged) and PR head f0462a8ba:

  • Conflicts: three-way merge against current dev reports textual conflicts in 5 filespackages/coding-agent/src/config/settings-schema.ts, packages/coding-agent/src/session/artifacts.ts, packages/coding-agent/src/session/session-manager.ts, packages/coding-agent/src/task/executor.ts, packages/coding-agent/src/task/types.ts. GitHub reports mergeable=false / mergeable_state=dirty.
  • Staleness: merge-base is 087a01c26; the head is 261 commits behind current dev and has not been updated since Aug 3.
  • CI: the exact head has 0 check runs and 0 workflow runs — CI has never executed for this PR, so nothing is green.
  • Structural: the PR bundles two features. Base commit 982e6b9 ("opt-in sub-agent model autorouting") is not on dev and has no separate PR, so this PR's diff includes the base autorouting feature plus the tier-generation feature. Please either split the base into its own PR first or state explicitly that both land here.
  • Prior findings stand: the Aug 5 REQUEST_CHANGES lane findings (P1: post-rename publication failure loses cleanup authority; P1: parallel Task children race the shared parent artifact allocator; P1: pathname-based, non-identity-bound rollback; P2: hand-edit check outside the atomic write lock; P2: bounded selectors mutating live execution; P2: routing output bypasses TUI sanitization) are unaddressed — the head SHA is unchanged.

Path to merge: rebase onto current dev, resolve the 5 conflict files while preserving current-dev invariants (settings enum-array validation, managed-session staging, recovery artifact/receipt types), address or explicitly acknowledge the P1/P2 findings, then push a new head and get CI green. The staging/rollback concurrency claims should get a fresh adversarial pass once the rebase lands, since the artifact allocator changes are core session/task infrastructure.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant