Skip to content

feat(platform): add "Auto" agent mode with server-side routing#1829

Merged
Israeltheminer merged 5 commits into
mainfrom
feat/auto-agent-routing
Jun 5, 2026
Merged

feat(platform): add "Auto" agent mode with server-side routing#1829
Israeltheminer merged 5 commits into
mainfrom
feat/auto-agent-routing

Conversation

@Israeltheminer

@Israeltheminer Israeltheminer commented Jun 4, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds an "Auto" default to the composer's agent selector. When the user hasn't pinned a specific assistant, the composer sends an AUTO_AGENT_SLUG sentinel and the server resolves it to a concrete agent before generation — so the thread commits the real agent from the first frame, with no re-patch.

This closes Tale's per-message AI-UX gap: instead of forcing users to choose an agent up front, "Auto" lets the system route, mirroring the Adaptive Reasoning Governor's philosophy — infer silently, spend an LLM call only when it changes the answer, always degrade to a safe default.

Routing cost ladder (cheapest first)

  1. ≤1 candidate agent → return it. No LLM call.
  2. Trivial greeting / ack → return the default agent. No LLM call.
  3. Otherwise → one cheap, timeout-raced classifier call over candidate descriptions. On timeout (ROUTE_TIMEOUT_MS = 6s) or any failure, falls back to the default agent — first-token latency is never blocked.

The classifier reuses the proven generateThreadTitle pattern: tiny Agent, reasoning forced off (kind: 'utility'), tag-resolved model with org failover, saveMessages: 'none', hard timeout race.

Pure routing helpers (auto_route_helpers.ts) are split from the 'use node' action so they're unit-testable without the Convex runtime — the same split the reasoning governor uses.

Selector polish (same PR)

  • Per-row provider/agent detail links (gated by the relevant ability) replace the always-on provider badge — provenance becomes an affordance that does something.
  • Cause- and role-aware no-models hints: distinguishes no-provider-configured, project-restricted, and not-reaching-this-agent cases; only asserts a cause it can actually tell apart client-side.
  • Honest empty state when an org genuinely has no agents (muted "No agents" rather than a phantom "Assistant").
  • Multi-model default hint in the agent create dialog (first model = default, rest = fallbacks).
  • Arena mode still requires an explicit agent (it compares two models on one chosen agent) — enforced before any optimistic UI, releasing the send lock on the fail-fast path.

Pre-PR checklist

  • Ran bun run check (format, lint, typecheck, all tests). Platform: typecheck ✅, lint ✅ (0 errors), tests ✅ (71,516 pass). Pre-existing, unrelated @tale/rag (Python env interrupt) and @tale/crawler failures are outside this diff.
  • No hand-rolled skeletons or magic h-[…] on skeletons — loading uses <Skeletonize> + skeleton-aware leaves.
  • Updated services/platform/messages/{en,de,fr}.json — new agentSelector/modelSelector keys added; orphaned createDialog.model{Placeholder,Search,Empty} keys removed (i18n orphan test green).
  • Updated /docs/{en,de,fr}/ for every user-visible change — N/A (no documented user flow changes; selector behavior is self-evident in-product).
  • Every touched docs page has a real opening/closing — N/A (no docs touched).
  • Ran bun run --filter @tale/docs lint and testN/A (no docs touched).
  • Updated README.md, README.de.md, README.fr.mdN/A.

Summary by CodeRabbit

  • New Features

    • "Auto" agent selection mode for automatic routing.
    • Agents support ordered multi-model selection (first is default, others are fallbacks).
    • Per-agent “view details” links in the selector and improved model/agent empty-state guidance.
  • Bug Fixes

    • Prevented accidental form submissions from list controls; remove button is disabled at minimum items.
  • Docs / Localization

    • Expanded translation strings and new arena-mode guidance in multiple languages.

Introduce an "Auto" default for the composer's agent selector: when the
user hasn't pinned a specific assistant, the composer sends the
AUTO_AGENT_SLUG sentinel and the server resolves it to a concrete agent
before generation. Routing follows a cheapest-first cost ladder — a
single candidate or a trivial greeting skips the LLM entirely; only a
genuinely ambiguous multi-agent message spends one timeout-raced
classifier call, falling back to the default agent on timeout or error.

Mirrors the Adaptive Reasoning Governor's philosophy: infer silently,
spend an LLM call only when it changes the answer, always degrade to a
safe default. Pure routing helpers live in auto_route_helpers.ts (unit
tested) split from the 'use node' action, matching the governor's
signals/action split.

Also polishes the selectors: per-row provider/agent detail links replace
the always-on provider badge, cause- and role-aware no-models hints, an
honest empty state when an org has no agents, and a multi-model default
hint in the agent create dialog. Arena mode still requires an explicit
agent (it compares two models on one chosen agent), enforced before any
optimistic UI.

Translations synced across en/de/fr; removed the orphaned
createDialog.model{Placeholder,Search,Empty} keys left by the dialog's
move to the multi-model selector.
@@ -0,0 +1,180 @@
'use node';
@coderabbitai

coderabbitai Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR implements agent auto-routing and multi-model selection across the chat and agent management UIs. It adds a server-side resolveAutoRoute action that selects a concrete agent for "Auto" mode (LLM-based classifier with timeouts and fallbacks). The agent selector gains an "Auto" pseudo-option and per-agent detail links. Model selectors show provider-setup links and role-aware empty-state guidance and use ReorderList with minItems enforcement. Agent creation now accepts ordered supportedModels; the send hook and unified chat pipeline resolve Auto before generation. Translations were added/updated for en/fr/de.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • tale-project/tale#1720: Overlaps AgentSelector UI behavior (trigger/label handling and loading/skeleton presentation).
  • tale-project/tale#1735: Related changes to chat send/generation paths and project-scoped routing context.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(platform): add "Auto" agent mode with server-side routing' clearly and concisely summarizes the main change — adding Auto agent mode with server-side routing.
Description check ✅ Passed The description covers the summary of changes, addresses all pre-merge checklist items (with clear N/A justifications where needed), and includes a detailed test plan explanation.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/auto-agent-routing

Warning

Billing warning: we have not been able to collect payment for this subscription for more than 72 hours. Please update the payment method or pay any pending invoices in Billing to avoid service interruption.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 10

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@services/platform/app/components/ui/forms/reorder-list.test.tsx`:
- Around line 84-87: Replace the hardcoded English ARIA label assertions in
reorder-list.test.tsx with the project translation lookups: stop asserting
against the literals used for moveUpLabel, moveDownLabel, dragHandleLabel and
removeLabel and instead import/use the translation helper/mock (e.g., i18n.t or
the test translations fixture used elsewhere) to get the expected strings, then
assert against those translated values so the tests use the same
keys/translation function as production code.

In `@services/platform/app/features/agents/components/agent-create-dialog.tsx`:
- Around line 115-119: The effect that seeds selectedModels only handles the
empty case and can leave stale refs when modelCatalog changes; update the
useEffect around selectedModels/modelCatalog to reconcile current selectedModels
against modelCatalog (e.g., filter selectedModels to refs that exist in
modelCatalog), then if the filtered list is empty
setSelectedModels([modelCatalog[0].ref]) (or [] if no models), and ensure the
same reconciliation logic is applied to the other block referenced (the
handler/effect around lines 170-178) so submit cannot include UNKNOWN_MODEL
refs; locate functions/variables named selectedModels, modelCatalog,
setSelectedModels and the useEffect(s) to implement this change.
- Around line 291-298: The Label with htmlFor="model-select" is orphaned because
ModelSelector does not render a control with that id; update ModelSelector to
accept an id prop and forward it to the actual interactive control (e.g., the
select/input inside ModelSelector) so the Label's htmlFor matches the control,
or alternatively replace the Label with a legend and wrap ModelSelector in a
fieldset for group semantics; specifically modify the call-site that renders
<ModelSelector ... /> to pass id="model-select" and update the ModelSelector
component to accept an id prop and apply it to the rendered control (ensure any
internal generated ids or focus handling use that id).

In `@services/platform/app/features/chat/hooks/use-send-message.ts`:
- Around line 276-283: The toast shown when arena mode rejects sending due to no
pinned agent is too generic; update the branch in useSendMessage (the isArena &&
!selectedAgent check) to call toast with a new translation key (e.g.
t('toast.arenaSelectAgent') or similar) instead of t('toast.sendFailed') and
keep the same variant and sendingRef reset; also add the corresponding
translation entry to the i18n/locales so the message reads something like "Arena
mode requires selecting a specific agent before sending."

In `@services/platform/convex/agents/auto_route_helpers.ts`:
- Around line 25-41: The filterRoutingCandidates function currently includes a
redundant runtime type guard checking "a && typeof a.name === 'string'" even
though raw is typed as AgentListEntry[] where name is required; remove that
redundant check and simplify the predicate to rely on the declared type (keep
checks for visibleInChat, primaryBehavior, and allowedAgentSlugs logic), or if
you intend to guard against malformed external input, add a short comment above
filterRoutingCandidates documenting that rationale instead of the inline type
check.

In `@services/platform/convex/agents/auto_route.ts`:
- Around line 109-114: The current fallback branch in auto_route.ts returns
DEFAULT_CHAT_AGENT_SLUG when no usable agents exist, which can cause misleading
"agent not found" errors; change the fallback to return a distinct sentinel or
structured error instead of DEFAULT_CHAT_AGENT_SLUG (e.g., set agentSlug to null
or include an errorCode like 'no_agents_available' and reason 'no_agents') so
unified_chat can detect and surface "No agents are available in this
organization." Update the branch that checks the fallback variable (the if
(!fallback) block) and any callers that expect {agentSlug, reason} to handle the
new sentinel/errorCode (or throw a specific error) rather than assuming
DEFAULT_CHAT_AGENT_SLUG.
- Around line 95-102: The consumer cast hides a missing field: update the Convex
action internal.agents.internal_actions.listAgentsInternal so its returns
projection includes primaryBehavior (set from result.config.primaryBehavior) and
declare its return type as a Convex v.array(v.object(...)) matching the
AgentListEntry shape; then remove the unsafe cast in auto_route.handler and rely
on the correctly typed listAgentsInternal result (used by
filterRoutingCandidates) rather than asserting as AgentListEntry[].

In `@services/platform/convex/agents/unified_chat.ts`:
- Around line 113-125: The auto-routing call to
internal.agents.auto_route.resolveAutoRoute in chatWithAgent doesn't forward
project-scoped allowedAgentSlugs, so resolveAutoRoute may pick an agent outside
the project's allow-list; fetch the project's allowedAgentSlugs (using
args.projectId or by reusing the project fetch from assertProjectAccessForChat)
and pass it as the allowedAgentSlugs parameter to resolveAutoRoute, ensuring you
call resolveAutoRoute({ organizationId: args.organizationId, message:
args.message, allowedAgentSlugs }) and keep the existing resolvedAgentSlug
assignment and logging unchanged.

In `@services/platform/messages/fr.json`:
- Line 4357: Find the French translation entry that contains the word "Glissez"
(the model-default hint) in services/platform/messages/fr.json—it's located near
the "noModelsTitle" entry—and change the phrasing to the informal imperative
form "Glisse" (or the appropriate informal plural/singular imperative you need)
so the string uses the informal "tu" voice consistently across the locale.
- Around line 854-857: Update the four message keys to use informal French (tu)
instead of formal/plural wording: change noModelsAdminHint to use "Clique" and
"Paramètres → Fournisseurs d'IA" with informal phrasing, change
noModelsAgentHint to "Vérifie" and refer to "les fournisseurs d'IA" informally,
change noModelsMemberHint to "Demande à l'administrateur de ton espace de
travail" (use "ton"), and change noModelsRestricted to "Aucun modèle ne t'est
disponible ici" and "contacte ton administrateur" so all imperatives and
pronouns use the informal singular form; update the strings for keys
noModelsAdminHint, noModelsAgentHint, noModelsMemberHint, and noModelsRestricted
accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: af4e2984-d8fd-4097-8fa4-799c8f3bb95d

📥 Commits

Reviewing files that changed from the base of the PR and between 296841c and b3a6103.

⛔ Files ignored due to path filters (1)
  • services/platform/convex/_generated/api.d.ts is excluded by !**/_generated/**
📒 Files selected for processing (17)
  • services/platform/app/components/ui/forms/model-selector.tsx
  • services/platform/app/components/ui/forms/reorder-list.test.tsx
  • services/platform/app/components/ui/forms/reorder-list.tsx
  • services/platform/app/features/agents/components/agent-create-dialog.tsx
  • services/platform/app/features/chat/components/agent-selector.test.tsx
  • services/platform/app/features/chat/components/agent-selector.tsx
  • services/platform/app/features/chat/components/model-selector.test.tsx
  • services/platform/app/features/chat/components/model-selector.tsx
  • services/platform/app/features/chat/hooks/use-send-message.ts
  • services/platform/convex/agents/auto_route.ts
  • services/platform/convex/agents/auto_route_helpers.test.ts
  • services/platform/convex/agents/auto_route_helpers.ts
  • services/platform/convex/agents/unified_chat.ts
  • services/platform/lib/shared/constants/agents.ts
  • services/platform/messages/de.json
  • services/platform/messages/en.json
  • services/platform/messages/fr.json
💤 Files with no reviewable changes (1)
  • services/platform/app/features/chat/components/model-selector.test.tsx

Comment thread services/platform/app/components/ui/forms/reorder-list.test.tsx
Comment thread services/platform/app/features/agents/components/agent-create-dialog.tsx Outdated
Comment thread services/platform/app/features/agents/components/agent-create-dialog.tsx Outdated
Comment thread services/platform/app/features/chat/hooks/use-send-message.ts
Comment thread services/platform/convex/agents/auto_route_helpers.ts
Comment thread services/platform/convex/agents/auto_route.ts
Comment thread services/platform/convex/agents/auto_route.ts
Comment thread services/platform/convex/agents/unified_chat.ts
Comment thread services/platform/messages/fr.json Outdated
Comment thread services/platform/messages/fr.json
The "does not send when no agent is selected" test asserted the old
behavior — a null selection blocked the send. Under Auto mode a null
selection now sends the AUTO_AGENT_SLUG sentinel and the server routes,
so split the case in two: Auto sends with agentSlug 'auto', and arena
mode (which still requires a pinned agent) blocks with a toast.
- agent-create-dialog: reconcile selected models against the live catalog
  on every refresh, not just the empty case — a provider refresh that
  drops a model no longer leaves a stale ref that fails submit with
  UNKNOWN_MODEL.
- agent-create-dialog: replace the orphaned `Label htmlFor="model-select"`
  (ModelSelector is a composite group with no single control) with a
  fieldset/legend so assistive tech gets the group association.
- use-send-message: give the arena "no agent pinned" rejection its own
  toast (toast.arenaRequiresAgent) so users know to pick a specific
  agent rather than seeing a generic send-failed.
- auto_route: clarify why the zero-candidate fallback returns the
  conventional default slug.
…form

- Auto routing now respects a project's allowedAgentSlugs. chatWithAgent
  loads the project's allow-list (new getProjectAllowedAgentSlugs internal
  query) and forwards it to resolveAutoRoute, so a project-scoped "Auto"
  send can't route to an agent outside the project's pinned set.
- French: rewrite the new no-models / model-default / arena strings to the
  informal `tu` form per the locale style guide (no `vous`), with
  typographic apostrophes and NBSP.
@Israeltheminer

Copy link
Copy Markdown
Collaborator Author

Addressed the CodeRabbit review across commits edbb5db, 5349c41, and 1cccc36:

Fixed

  • unified_chat.ts — project allow-list not forwarded to Auto routing (Major): added getProjectAllowedAgentSlugs internal query; chatWithAgent now loads a project's allowedAgentSlugs and forwards them to resolveAutoRoute, so a project-scoped Auto send can't route outside the pinned set.
  • agent-create-dialog.tsx — stale model refs (Major): the seeding effect now reconciles selectedModels against the live catalog on every refresh, so a removed model can't survive to submit as UNKNOWN_MODEL.
  • agent-create-dialog.tsx — orphaned Label htmlFor (Minor): replaced with fieldset/legend (ModelSelector is a composite group, no single control to target).
  • use-send-message.ts — generic arena toast (Nitpick): dedicated toast.arenaRequiresAgent key (en/de/fr) explaining Auto isn't supported in Arena.
  • fr.json — formal voice (Minor ×2): rewrote the new strings to the informal tu form with typographic apostrophes + NBSP, per the FR locale style guide.

Intentionally skipped

  • auto_route.ts:114 — fallback to chat-agent when zero candidates: only fires in an already-broken org (no chat-capable agents at all); the downstream resolve step already raises a clear error. Not worth threading a new error channel through unified_chat. Comment sharpened to record the rationale.
  • auto_route_helpers.ts:41 — "redundant" runtime type guard: intentional — listAgentsInternal returns v.any(), so the a && typeof a.name === 'string' guard is real defense against malformed runtime data, not redundant with the compile-time type.
  • reorder-list.test.tsx — "hardcoded English ARIA labels": false positive. Those labels are self-defined test fixtures passed as props (moveUpLabel: 'Move up') and queried back; the test asserts the component wires its props through, not production i18n output.

The earlier UI failure (a stale "does not send when no agent is selected" test) is fixed in edbb5db — split into an Auto-sends case and an Arena-blocks case.

@Israeltheminer

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@services/platform/messages/fr.json`:
- Line 4357: The string value for the key "modelDefaultHint" uses the incorrect
verb form "ajoutes-en"; update it to the correct informal imperative "ajoute-en"
so the hint reads "Le premier modèle est celui par défaut ; les autres servent
de solutions de repli. Glisse pour réordonner ou ajoute-en d’autres — modifiable
à tout moment." to maintain consistent informal "tu" voice.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: ac6c1133-a632-4fd6-b2bc-d6dd71726151

📥 Commits

Reviewing files that changed from the base of the PR and between b3a6103 and 1cccc36.

📒 Files selected for processing (9)
  • services/platform/app/features/agents/components/agent-create-dialog.tsx
  • services/platform/app/features/chat/hooks/use-send-message.test.ts
  • services/platform/app/features/chat/hooks/use-send-message.ts
  • services/platform/convex/agents/auto_route.ts
  • services/platform/convex/agents/unified_chat.ts
  • services/platform/convex/projects/internal_queries.ts
  • services/platform/messages/de.json
  • services/platform/messages/en.json
  • services/platform/messages/fr.json

"modelPlaceholder": "Sélectionner un modèle",
"modelSearch": "Rechercher des modèles…",
"modelEmpty": "Aucun modèle trouvé",
"modelDefaultHint": "Le premier modèle est celui par défaut ; les autres servent de solutions de repli. Glisse pour réordonner ou ajoutes-en d’autres — modifiable à tout moment.",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use correct informal imperative form in this hint.

Line 4357 uses ajoutes-en, which is not the imperative form. Use ajoute-en to keep the sentence grammatically correct and consistent with informal tu voice.

✏️ Suggested fix
-        "modelDefaultHint": "Le premier modèle est celui par défaut ; les autres servent de solutions de repli. Glisse pour réordonner ou ajoutes-en d’autres — modifiable à tout moment.",
+        "modelDefaultHint": "Le premier modèle est celui par défaut ; les autres servent de solutions de repli. Glisse pour réordonner ou ajoute-en d’autres — modifiable à tout moment.",

As per coding guidelines: "Use informal form across all languages — du in German, tu in French; never Sie or vous."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"modelDefaultHint": "Le premier modèle est celui par défaut ; les autres servent de solutions de repli. Glisse pour réordonner ou ajoutes-en dautres — modifiable à tout moment.",
"modelDefaultHint": "Le premier modèle est celui par défaut ; les autres servent de solutions de repli. Glisse pour réordonner ou ajoute-en d'autres — modifiable à tout moment.",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/platform/messages/fr.json` at line 4357, The string value for the
key "modelDefaultHint" uses the incorrect verb form "ajoutes-en"; update it to
the correct informal imperative "ajoute-en" so the hint reads "Le premier modèle
est celui par défaut ; les autres servent de solutions de repli. Glisse pour
réordonner ou ajoute-en d’autres — modifiable à tout moment." to maintain
consistent informal "tu" voice.

@Israeltheminer Israeltheminer merged commit d71e927 into main Jun 5, 2026
10 checks passed
@Israeltheminer Israeltheminer deleted the feat/auto-agent-routing branch June 5, 2026 15:05
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