feat(platform): add "Auto" agent mode with server-side routing#1829
Conversation
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'; | |||
📝 WalkthroughWalkthroughThis 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
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (1)
services/platform/convex/_generated/api.d.tsis excluded by!**/_generated/**
📒 Files selected for processing (17)
services/platform/app/components/ui/forms/model-selector.tsxservices/platform/app/components/ui/forms/reorder-list.test.tsxservices/platform/app/components/ui/forms/reorder-list.tsxservices/platform/app/features/agents/components/agent-create-dialog.tsxservices/platform/app/features/chat/components/agent-selector.test.tsxservices/platform/app/features/chat/components/agent-selector.tsxservices/platform/app/features/chat/components/model-selector.test.tsxservices/platform/app/features/chat/components/model-selector.tsxservices/platform/app/features/chat/hooks/use-send-message.tsservices/platform/convex/agents/auto_route.tsservices/platform/convex/agents/auto_route_helpers.test.tsservices/platform/convex/agents/auto_route_helpers.tsservices/platform/convex/agents/unified_chat.tsservices/platform/lib/shared/constants/agents.tsservices/platform/messages/de.jsonservices/platform/messages/en.jsonservices/platform/messages/fr.json
💤 Files with no reviewable changes (1)
- services/platform/app/features/chat/components/model-selector.test.tsx
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.
|
Addressed the CodeRabbit review across commits Fixed
Intentionally skipped
The earlier UI failure (a stale "does not send when no agent is selected" test) is fixed in |
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
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
📒 Files selected for processing (9)
services/platform/app/features/agents/components/agent-create-dialog.tsxservices/platform/app/features/chat/hooks/use-send-message.test.tsservices/platform/app/features/chat/hooks/use-send-message.tsservices/platform/convex/agents/auto_route.tsservices/platform/convex/agents/unified_chat.tsservices/platform/convex/projects/internal_queries.tsservices/platform/messages/de.jsonservices/platform/messages/en.jsonservices/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.", |
There was a problem hiding this comment.
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.
| "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.", |
🤖 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.
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_SLUGsentinel 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)
ROUTE_TIMEOUT_MS = 6s) or any failure, falls back to the default agent — first-token latency is never blocked.The classifier reuses the proven
generateThreadTitlepattern: tinyAgent, 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)
Pre-PR checklist
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/crawlerfailures are outside this diff.h-[…]on skeletons — loading uses<Skeletonize>+ skeleton-aware leaves.services/platform/messages/{en,de,fr}.json— newagentSelector/modelSelectorkeys added; orphanedcreateDialog.model{Placeholder,Search,Empty}keys removed (i18n orphan test green)./docs/{en,de,fr}/for every user-visible change — N/A (no documented user flow changes; selector behavior is self-evident in-product).bun run --filter @tale/docs lintandtest— N/A (no docs touched).README.md,README.de.md,README.fr.md— N/A.Summary by CodeRabbit
New Features
Bug Fixes
Docs / Localization