feat(platform): i18n-first agent data model + truthful agent response language#1605
Conversation
… language switch
Previously, the org-level "What language should your agents use?" dropdown
was misleading — it only flagged the authoring locale for UI translation
lookups and had no effect on what agents actually output. The translatable
fields (displayName / description / conversationStarters / systemInstructions)
also coexisted at the top level and under `i18n.<locale>`, creating
duplication once all locales were populated.
- Data model: top-level translatable fields become optional legacy
fallbacks; canonical values live under `i18n.<locale>.*`. Resolution
precedence is `i18n[locale] → i18n[base-language] → i18n.en → top-level`.
Schema superRefine requires displayName (and systemInstructions for chat
agents) in at least one locale. BCP-47 narrowing so fr-CH / de-AT still
match their base language.
- Agent response language: new `getOrganizationDefaultLocale` internal
query is parallelized into `resolveAgentConfigInline` and
`resolveAgentConfig` (no TTFT regression). `toSerializableConfig` takes
an optional `locale` and resolves `systemInstructions` through the same
precedence, so agents actually respond in the org's chosen language.
Delegation threaded — parent + delegates speak the same language;
scaffold text ("DELEGATION AGENTS" / "You can delegate...") localized
in en/de/fr.
- Hybrid display resolution: agent chrome (displayName, description,
conversationStarters) follows each user's UI locale via `useChatAgents`
so the agent picker, composer-mode menu and chat welcome all render in
the reader's language — independent of the org-wide output setting.
- Editor UI: new `LocaleTabs` component powers tabbed i18n editing with
auto-translate on the instructions tab (systemInstructions) and the
general tab (displayName / description). The existing conversation-
starters editor writes its default-tab edits into `i18n[defaultLocale]`
and retires the legacy top-level field on first save.
- Label: org-settings dropdown renamed to "Agent response language" (en /
de / fr) with a one-line helper clarifying it governs agent output and
not the per-user UI locale.
- fr added to SUPPORTED_AGENT_LOCALES; `getOrganizationDefaultLocale`
clamps stale values to supported locales.
- Example agents: all 9 files under examples/agents/ backfilled to the
i18n-first shape with complete en + de + fr translations for every
translatable field. A dev script (scripts/backfill-example-i18n.ts)
mechanically restructured top-level → i18n.en.
- Tests: new config.test.ts covers the server-side resolution precedence;
resolve-agent-locale tests rewritten for i18n-first; use-effective-agent
and TTFT tests updated for the new parallel query.
…ilter placeholder The content-safety word-filter placeholder included a Chinese profanity to demonstrate CJK substring matching. Replaced with the three supported UI locales (forbidden / verboten / interdit) — same multilingual hint, no profanity, each locale leads with its own language.
📝 WalkthroughWalkthroughThis pull request implements a multi-locale (i18n) structure for agent configurations. Agent metadata—including Estimated code review effort🎯 4 (Complex) | ⏱️ ~65 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)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 17
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
services/platform/convex/agents/file_actions.ts (1)
512-618:⚠️ Potential issue | 🟠 MajorInclude docs updates for this user-visible behavior change (en/de/fr) in the same PR.
Agents now respect the organization's default locale when resolving localized instructions. This affects what users see the agent output, so documentation must ship together across all published locales (en, de, fr) per project requirements.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@services/platform/convex/agents/file_actions.ts` around lines 512 - 618, The PR changes resolveAgentConfig to pass orgLocale into toSerializableConfig, which causes agent instructions to be localized using the organization's default locale; update the user-facing documentation across all published locales (en, de, fr) in the same PR to reflect this behavior change—update docs that describe agent output/localization, any examples referencing instruction language, and release notes for this feature in the project's documentation files so readers in each locale see the new default-locale behavior; ensure the doc edits reference resolveAgentConfig/toSerializableConfig behavior and mention that effectiveConfig.supportedModels validation is unaffected.services/platform/convex/agents/config.ts (1)
37-57:⚠️ Potential issue | 🔴 CriticalUpdate documentation for locale-aware agent instructions in all three locales.
This PR adds locale-based resolution for agent
systemInstructions— agents now return different instructions based on the user's organization locale — but ships with no docs. Documentation explaining the i18n field structure and how to set locale-specific instructions must be added todocs/AGENTS.md,docs/de/platform/agents/, anddocs/fr/platform/agents/in this PR.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@services/platform/convex/agents/config.ts` around lines 37 - 57, Add documentation describing the new locale-aware agent instructions introduced by resolveInstructions used in toSerializableConfig: update docs/AGENTS.md and the localized folders docs/de/platform/agents/ and docs/fr/platform/agents/ to explain the i18n field structure on AgentJsonConfig (how to provide systemInstructions per locale, fallback behavior when locale is missing, examples for en/de/fr, and how resolveInstructions picks the locale); include a small JSON/YAML example showing locale keys and the default key, note how to supply locale to toSerializableConfig, and document any validation rules or supported locale codes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@services/platform/app/routes/dashboard/`$id/agents/$agentId/index.tsx:
- Around line 288-295: The UI change adds locale-aware agent authoring via
LocaleTabs (props: defaultLocale, editingLocale, onEditingLocaleChange,
hasTranslation, onAutoTranslate using handleAutoTranslate when hasSource, and
isTranslating driven by translateMutation.isPending), but the PR must also
update user docs; add corresponding documentation updates under docs/ for en,
de, and fr that describe the new editing flow (how to switch editingLocale, what
hasTranslation and onAutoTranslate do) and the org-level language behavior
(defaultLocale semantics and when auto-translate is available), ensuring the
same PR includes these localized markdown pages and links from the relevant
settings/agents documentation so the UI and docs remain in sync.
- Around line 82-109: The current writeFields (function writeFields) can drop an
untouched legacy top-level field when migrating to i18n; to fix, seed the
per-locale `next` object with the default-locale legacy values before applying
patch and overrides so untouched fields are preserved (e.g., compute a base =
editingLocale === defaultLocale ? { displayName: legacyDisplayName, description:
legacyDescription } : {} and then const next = { ...base, ...existingOverrides
}; apply the patch changes as before), and then call updateConfig(i18n:
nextI18n, ...) as now; this preserves untouched legacy fields during the first
edit and allows the existing shouldClearLegacy logic to safely retire top-level
values.
- Around line 50-51: The editingLocale state is initialized from
getOrganizationDefaultLocale before useOrganization finishes, causing stale tab
selection; add a useEffect that watches the resolved defaultLocale (computed via
getOrganizationDefaultLocale(organization?.metadata)) and calls
setEditingLocale(defaultLocale) whenever defaultLocale changes so editingLocale
always stays in sync with the org's resolved default locale; reference the
defaultLocale variable, editingLocale state, setEditingLocale setter, and
getOrganizationDefaultLocale/useOrganization to locate where to add the effect.
In `@services/platform/app/routes/dashboard/`$id/agents/$agentId/instructions.tsx:
- Around line 59-60: The editingLocale state is initialized from
getOrganizationDefaultLocale but never updates when useOrganization resolves;
add a useEffect that watches defaultLocale (derived via
getOrganizationDefaultLocale(organization?.metadata)) and calls
setEditingLocale(defaultLocale) when defaultLocale is defined and different from
the current editingLocale to sync the active locale; reference defaultLocale,
editingLocale, setEditingLocale, and getOrganizationDefaultLocale to locate
where to add the effect.
In `@services/platform/convex/agent_tools/delegation/create_delegation_tool.ts`:
- Around line 180-182: The scaffold lookup is using the full locale string which
fails for regional tags like "de-CH"; normalize the locale to its primary
language subtag before indexing DELEGATION_SCAFFOLD by deriving a baseLocale
(e.g., take locale?.split('-')[0] or equivalent) and then use (baseLocale ?
DELEGATION_SCAFFOLD[baseLocale] : undefined) ?? DELEGATION_SCAFFOLD.en so
scaffold resolves "de-CH"/"fr-CH" to "de"/"fr" correctly while still falling
back to English.
In `@services/platform/convex/agents/__tests__/config.test.ts`:
- Around line 10-84: Add a test that verifies BCP-47 narrowing: create an
AgentJsonConfig with i18n.de.systemInstructions (and optionally i18n.en) and
call toSerializableConfig('test', config, undefined, 'de-AT'), then assert
result.instructions === 'German i18n' to confirm that toSerializableConfig falls
back from 'de-AT' to 'de'; reference the existing test style (use baseConfig,
AgentJsonConfig, and systemInstructions keys) and mirror the structure of the
other cases in the describe block.
In `@services/platform/convex/agents/file_utils.ts`:
- Around line 29-34: The comment describing resolution precedence is stale:
update the docstring in file_utils.ts to match runtime behavior in
services/platform/convex/agents/config.ts by adding the base-language fallback
into the chain so it reads something like "i18n[locale] → i18n[base-language] →
i18n['en'] → top-level"; ensure you reference the same terminology used in
config.ts (e.g., base-language or baseLanguage) so readers can map the comment
to the runtime lookup implemented there.
- Line 35: The change made displayName optional which breaks downstream code
that builds a copy name using `${source.config.displayName} (Copy)`; either
revert displayName to a required property in the type definition in
file_utils.ts (remove the `?` on displayName) or add a safe fallback in
file_actions.ts when formatting the copy name (e.g., use
`source.config.displayName ?? <fallback>` or derive from another stable field)
so `${source.config.displayName} (Copy)` never becomes `undefined (Copy)`.
Ensure you update the declaration of displayName or the formatting in
file_actions.ts accordingly.
In `@services/platform/convex/agents/unified_chat.ts`:
- Line 301: The helper resolveAgentConfigInline currently returns an extra
orgLocale field that isn’t used by any caller in this file; remove orgLocale
from the return contract (the Promise generic and any object construction/return
that includes orgLocale) in resolveAgentConfigInline and in the other similar
helper mentioned (same file, the other return at 349) so the function returns
only InlineConfigResult; update any internal code that sets orgLocale to stop
including it or keep it local-only.
In `@services/platform/convex/lib/agent_chat/internal_actions.ts`:
- Around line 693-696: The comment falsely claims the org's `defaultLocale` is
fetched in parallel with delegate loading; update the comment near the
`orgLocale` fetch and the delegate-loading block in internal_actions.ts to
accurately state that the code performs these operations sequentially (the code
`await`s `orgLocale` before starting delegate loading), or if you prefer true
parallelism, start both promises without awaiting (e.g., kick off
delegate-loading promise and orgLocale promise and `await` them both later).
Ensure the comment references `orgLocale` and the delegate loading
function/variable so readers can locate the relevant lines.
In `@services/platform/convex/organizations/internal_queries.ts`:
- Around line 30-34: The parse block for metadata currently swallows errors and
returns appDefaultLocale; update the catch in the try { metadata =
JSON.parse(metadata) } catch { ... } to log a warning before returning: call
console.warn with a clear message that includes the malformed metadata and the
caught error (e.g., "Failed to parse org metadata" plus metadata and error) so
the failure is visible while still returning appDefaultLocale. Ensure you
reference the same variables (metadata, appDefaultLocale) and keep behavior
otherwise unchanged.
In `@services/platform/lib/shared/schemas/agents.ts`:
- Around line 41-45: Add documentation describing the new i18n-first agent
model: explain that translatable fields live under the i18n object keyed by
locale
(i18n.<locale>.(displayName|description|conversationStarters|systemInstructions)),
show example structure for one or more locales, and state that i18n entries take
precedence over legacy top-level fields (displayName, description,
conversationStarters, systemInstructions) while the superRefine still requires
those fields to exist in at least one locale or top-level fallback; apply this
content to the English, German and French create.md docs in the platform agents
documentation so authors know how to author per-locale overrides and which
values win.
In `@services/platform/lib/shared/utils/__tests__/resolve-agent-locale.test.ts`:
- Around line 34-100: Add a new unit test in the resolveAgentLocale suite that
asserts BCP-47 base-language fallback: call resolveAgentLocale(i18nFirstAgent,
'de-AT') and verify it selects fields from i18n.de (e.g., displayName ===
'Chat-Assistent', conversationStarters === ['Hallo','Hilf mir'],
systemInstructions === 'Du bist ein hilfreicher deutscher Assistent.') to ensure
region-variant narrowing uses the base language; reference the
resolveAgentLocale function and the existing i18nFirstAgent fixture so the test
follows the same patterns as the other cases.
In `@services/platform/lib/shared/utils/resolve-agent-locale.ts`:
- Around line 20-24: The locale-field precedence logic (requested locale → base
language → app default 'en' → top-level legacy field) is duplicated in
resolveAgentLocale() and the systemInstructions resolution in
services/platform/convex/agents/config.ts; extract that logic into a single
helper (e.g., resolveLocaleField or resolveAgentLocaleField) that accepts
(i18nMap, requestedLocale, fieldName, appDefault='en', topLevelFallback?) and
returns the resolved string following the four-step precedence, then replace the
inlined logic in resolveAgentLocale() and the systemInstructions resolution to
call this helper; update imports/exports accordingly so both modules use the
shared helper.
In `@services/platform/scripts/backfill-example-i18n.ts`:
- Around line 15-17: The import specifiers are not consistently sorted; please
reorder them to follow the repo import-order rules by alphabetizing members
within each import and ordering import statements per project convention: for
these lines, alphabetize the node:fs named imports (readFileSync, readdirSync,
writeFileSync) and the node:path named imports (dirname, join, resolvePath), and
keep the node:url import (fileURLToPath) grouped appropriately with the other
node imports so all import specifiers are consistently sorted.
- Line 12: Update the usage comment string to reference the actual filename;
change the commented usage "tsx
services/platform/scripts/backfill_example_i18n.ts" to "tsx
services/platform/scripts/backfill-example-i18n.ts" so the usage example matches
the real file name (look for the top-of-file usage comment that contains
backfill_example_i18n.ts).
- Around line 26-29: The AgentFile interface uses unknown and the script uses an
`as` cast around the i18n data; replace `unknown` with a concrete type (e.g.,
make i18n?: Record<string, Record<string, string>> or another precise shape that
matches your i18n values) and remove the `as` assertion at the location where
the parsed/loaded data is cast (the code around the `as` on line 78). Instead of
casting, parse/assign the value into a variable annotated with the concrete type
and perform a simple runtime validation/type guard before using it (or
map/transform the raw object into the strongly typed shape). Update the
AgentFile interface and the variable handling the parsed i18n so no `unknown`,
`any`, or `as` remain.
---
Outside diff comments:
In `@services/platform/convex/agents/config.ts`:
- Around line 37-57: Add documentation describing the new locale-aware agent
instructions introduced by resolveInstructions used in toSerializableConfig:
update docs/AGENTS.md and the localized folders docs/de/platform/agents/ and
docs/fr/platform/agents/ to explain the i18n field structure on AgentJsonConfig
(how to provide systemInstructions per locale, fallback behavior when locale is
missing, examples for en/de/fr, and how resolveInstructions picks the locale);
include a small JSON/YAML example showing locale keys and the default key, note
how to supply locale to toSerializableConfig, and document any validation rules
or supported locale codes.
In `@services/platform/convex/agents/file_actions.ts`:
- Around line 512-618: The PR changes resolveAgentConfig to pass orgLocale into
toSerializableConfig, which causes agent instructions to be localized using the
organization's default locale; update the user-facing documentation across all
published locales (en, de, fr) in the same PR to reflect this behavior
change—update docs that describe agent output/localization, any examples
referencing instruction language, and release notes for this feature in the
project's documentation files so readers in each locale see the new
default-locale behavior; ensure the doc edits reference
resolveAgentConfig/toSerializableConfig behavior and mention that
effectiveConfig.supportedModels validation is unaffected.
🪄 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: 8bdbe5fd-e72d-4748-9b78-10604784369a
⛔ Files ignored due to path filters (1)
services/platform/convex/_generated/api.d.tsis excluded by!**/_generated/**
📒 Files selected for processing (37)
examples/agents/chat-agent.jsonexamples/agents/crm-assistant.jsonexamples/agents/file-assistant.jsonexamples/agents/image-creator.jsonexamples/agents/integration-assistant.jsonexamples/agents/researcher.jsonexamples/agents/translator.jsonexamples/agents/web-assistant.jsonexamples/agents/workflow-assistant.jsonservices/platform/app/features/agents/components/locale-tabs.tsxservices/platform/app/features/chat/hooks/__tests__/use-effective-agent.test.tsservices/platform/app/features/chat/hooks/queries.tsservices/platform/app/features/chat/hooks/use-effective-agent.tsservices/platform/app/features/settings/organization/components/organization-settings.tsxservices/platform/app/routes/dashboard/$id/agents/$agentId/conversation-starters.tsxservices/platform/app/routes/dashboard/$id/agents/$agentId/index.tsxservices/platform/app/routes/dashboard/$id/agents/$agentId/instructions.tsxservices/platform/convex/agent_tools/delegation/create_delegation_tool.tsservices/platform/convex/agent_tools/delegation/load_delegation_agents.tsservices/platform/convex/agents/__tests__/config.test.tsservices/platform/convex/agents/__tests__/unified_chat_ttft.test.tsservices/platform/convex/agents/config.tsservices/platform/convex/agents/file_actions.tsservices/platform/convex/agents/file_utils.tsservices/platform/convex/agents/unified_chat.tsservices/platform/convex/lib/agent_chat/__tests__/tool_building_parallelization.test.tsservices/platform/convex/lib/agent_chat/internal_actions.tsservices/platform/convex/organizations/internal_queries.tsservices/platform/lib/shared/constants/agents.tsservices/platform/lib/shared/schemas/agents.tsservices/platform/lib/shared/utils/__tests__/resolve-agent-locale.test.tsservices/platform/lib/shared/utils/get-organization-default-locale.tsservices/platform/lib/shared/utils/resolve-agent-locale.tsservices/platform/messages/de.jsonservices/platform/messages/en.jsonservices/platform/messages/fr.jsonservices/platform/scripts/backfill-example-i18n.ts
| const defaultLocale = getOrganizationDefaultLocale(organization?.metadata); | ||
| const [editingLocale, setEditingLocale] = useState(defaultLocale); |
There was a problem hiding this comment.
Keep editingLocale in sync with the resolved org default.
This state is captured before useOrganization() finishes, so orgs whose default locale is not the initial fallback keep editing the wrong tab. That means the first edits can be written into i18n.en even when the org-level response language is actually de or fr.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@services/platform/app/routes/dashboard/`$id/agents/$agentId/index.tsx around
lines 50 - 51, The editingLocale state is initialized from
getOrganizationDefaultLocale before useOrganization finishes, causing stale tab
selection; add a useEffect that watches the resolved defaultLocale (computed via
getOrganizationDefaultLocale(organization?.metadata)) and calls
setEditingLocale(defaultLocale) whenever defaultLocale changes so editingLocale
always stays in sync with the org's resolved default locale; reference the
defaultLocale variable, editingLocale state, setEditingLocale setter, and
getOrganizationDefaultLocale/useOrganization to locate where to add the effect.
| const writeFields = useCallback( | ||
| (patch: { displayName?: string; description?: string }) => { | ||
| const existingI18n = config.i18n ?? {}; | ||
| const existingOverrides = existingI18n[editingLocale] ?? {}; | ||
| const next = { ...existingOverrides }; | ||
| if ('displayName' in patch) { | ||
| const v = patch.displayName?.trim(); | ||
| next.displayName = v ? patch.displayName : undefined; | ||
| } | ||
| if ('description' in patch) { | ||
| const v = patch.description?.trim(); | ||
| next.description = v ? patch.description : undefined; | ||
| } | ||
| const nextI18n = { | ||
| ...existingI18n, | ||
| [editingLocale]: next, | ||
| }; | ||
| // On first edit of the default-locale tab on a legacy agent, retire the | ||
| // top-level fields so the i18n entries become canonical. | ||
| const shouldClearLegacy = | ||
| editingLocale === defaultLocale && | ||
| (!!legacyDisplayName || !!legacyDescription); | ||
| updateConfig({ | ||
| i18n: nextI18n, | ||
| ...(shouldClearLegacy | ||
| ? { displayName: undefined, description: undefined } | ||
| : {}), | ||
| }); |
There was a problem hiding this comment.
Don't clear the untouched legacy field during migration.
On a legacy agent, editing only description seeds next with the new description, then shouldClearLegacy removes both top-level fields. Because displayName was never copied into next, that edit silently drops the existing name entirely; the inverse happens when only displayName is edited. Seed next with the default-locale legacy values before clearing them, or only clear the specific field(s) migrated in this patch.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@services/platform/app/routes/dashboard/`$id/agents/$agentId/index.tsx around
lines 82 - 109, The current writeFields (function writeFields) can drop an
untouched legacy top-level field when migrating to i18n; to fix, seed the
per-locale `next` object with the default-locale legacy values before applying
patch and overrides so untouched fields are preserved (e.g., compute a base =
editingLocale === defaultLocale ? { displayName: legacyDisplayName, description:
legacyDescription } : {} and then const next = { ...base, ...existingOverrides
}; apply the patch changes as before), and then call updateConfig(i18n:
nextI18n, ...) as now; this preserves untouched legacy fields during the first
edit and allows the existing shouldClearLegacy logic to safely retire top-level
values.
| <LocaleTabs | ||
| defaultLocale={defaultLocale} | ||
| editingLocale={editingLocale} | ||
| onEditingLocaleChange={setEditingLocale} | ||
| hasTranslation={hasTranslation} | ||
| onAutoTranslate={hasSource ? handleAutoTranslate : undefined} | ||
| isTranslating={translateMutation.isPending} | ||
| /> |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Confirm the matching docs shipped with this UI change.
This introduces locale-aware agent authoring in a user-facing settings screen, but I don't see corresponding docs in the provided diff. Please verify that the same PR updates docs/ in en, de, and fr for the new editing flow and org-level language behavior.
I can help draft the docs delta if needed. Based on learnings: "Documentation must be updated in the same PR as any code change that alters what users see, configure, or interact with" and "Docs ship with the code... update docs/ in all three locales (en, de, fr)."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@services/platform/app/routes/dashboard/`$id/agents/$agentId/index.tsx around
lines 288 - 295, The UI change adds locale-aware agent authoring via LocaleTabs
(props: defaultLocale, editingLocale, onEditingLocaleChange, hasTranslation,
onAutoTranslate using handleAutoTranslate when hasSource, and isTranslating
driven by translateMutation.isPending), but the PR must also update user docs;
add corresponding documentation updates under docs/ for en, de, and fr that
describe the new editing flow (how to switch editingLocale, what hasTranslation
and onAutoTranslate do) and the org-level language behavior (defaultLocale
semantics and when auto-translate is available), ensuring the same PR includes
these localized markdown pages and links from the relevant settings/agents
documentation so the UI and docs remain in sync.
| const defaultLocale = getOrganizationDefaultLocale(organization?.metadata); | ||
| const [editingLocale, setEditingLocale] = useState(defaultLocale); |
There was a problem hiding this comment.
Sync the active locale when the org default loads.
useOrganization() resolves after the first render, so this state is initialized with the fallback locale and never updates for orgs whose default is de/fr. The page can therefore open on the wrong tab and persist instructions into the wrong i18n bucket until the user notices and switches manually.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@services/platform/app/routes/dashboard/`$id/agents/$agentId/instructions.tsx
around lines 59 - 60, The editingLocale state is initialized from
getOrganizationDefaultLocale but never updates when useOrganization resolves;
add a useEffect that watches defaultLocale (derived via
getOrganizationDefaultLocale(organization?.metadata)) and calls
setEditingLocale(defaultLocale) when defaultLocale is defined and different from
the current editingLocale to sync the active locale; reference defaultLocale,
editingLocale, setEditingLocale, and getOrganizationDefaultLocale to locate
where to add the effect.
| const scaffold = | ||
| (locale ? DELEGATION_SCAFFOLD[locale] : undefined) ?? | ||
| DELEGATION_SCAFFOLD.en; |
There was a problem hiding this comment.
Normalize regional locale tags before scaffold lookup.
Line 181 does an exact key lookup only. Regional tags like de-CH/fr-CH will incorrectly fall back to English instead of de/fr.
💡 Proposed fix
- const scaffold =
- (locale ? DELEGATION_SCAFFOLD[locale] : undefined) ??
- DELEGATION_SCAFFOLD.en;
+ const normalizedLocale = locale?.toLowerCase();
+ const baseLocale = normalizedLocale?.split('-')[0];
+ const scaffold =
+ (normalizedLocale ? DELEGATION_SCAFFOLD[normalizedLocale] : undefined) ??
+ (baseLocale ? DELEGATION_SCAFFOLD[baseLocale] : undefined) ??
+ DELEGATION_SCAFFOLD.en;📝 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.
| const scaffold = | |
| (locale ? DELEGATION_SCAFFOLD[locale] : undefined) ?? | |
| DELEGATION_SCAFFOLD.en; | |
| const normalizedLocale = locale?.toLowerCase(); | |
| const baseLocale = normalizedLocale?.split('-')[0]; | |
| const scaffold = | |
| (normalizedLocale ? DELEGATION_SCAFFOLD[normalizedLocale] : undefined) ?? | |
| (baseLocale ? DELEGATION_SCAFFOLD[baseLocale] : undefined) ?? | |
| DELEGATION_SCAFFOLD.en; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@services/platform/convex/agent_tools/delegation/create_delegation_tool.ts`
around lines 180 - 182, The scaffold lookup is using the full locale string
which fails for regional tags like "de-CH"; normalize the locale to its primary
language subtag before indexing DELEGATION_SCAFFOLD by deriving a baseLocale
(e.g., take locale?.split('-')[0] or equivalent) and then use (baseLocale ?
DELEGATION_SCAFFOLD[baseLocale] : undefined) ?? DELEGATION_SCAFFOLD.en so
scaffold resolves "de-CH"/"fr-CH" to "de"/"fr" correctly while still falling
back to English.
| describe('resolveAgentLocale', () => { | ||
| it('returns i18n[locale] values when fully present', () => { | ||
| const result = resolveAgentLocale(i18nFirstAgent, 'en'); | ||
| expect(result.displayName).toBe('Chat Agent'); | ||
| expect(result.description).toBe('English description'); | ||
| expect(result.conversationStarters).toEqual(['Hello', 'Help me']); | ||
| expect(result.systemInstructions).toBe( | ||
| 'You are a helpful English assistant.', | ||
| ); | ||
| }); | ||
|
|
||
| it('returns full i18n overrides for a translated locale', () => { | ||
| const result = resolveAgentLocale(agent, 'de', 'en'); | ||
|
|
||
| expect(result).toEqual({ | ||
| displayName: 'Chat-Assistent', | ||
| description: 'English description', | ||
| conversationStarters: ['Hallo', 'Hilf mir'], | ||
| }); | ||
| it('falls back field-by-field to i18n.en when requested locale has partial overrides', () => { | ||
| const result = resolveAgentLocale(i18nFirstAgent, 'de'); | ||
| expect(result.displayName).toBe('Chat-Assistent'); | ||
| expect(result.description).toBe('English description'); // from en | ||
| expect(result.conversationStarters).toEqual(['Hallo', 'Hilf mir']); | ||
| expect(result.systemInstructions).toBe( | ||
| 'Du bist ein hilfreicher deutscher Assistent.', | ||
| ); | ||
| }); | ||
|
|
||
| it('falls back field-by-field for partial overrides', () => { | ||
| const result = resolveAgentLocale(agent, 'fr', 'en'); | ||
|
|
||
| expect(result).toEqual({ | ||
| displayName: 'Agent de chat', | ||
| description: 'English description', | ||
| conversationStarters: ['Hello', 'Help me'], | ||
| }); | ||
| it('falls back to i18n.en for unknown locale, never uses i18n.de', () => { | ||
| const result = resolveAgentLocale(i18nFirstAgent, 'es'); | ||
| expect(result.displayName).toBe('Chat Agent'); // en, not de | ||
| expect(result.description).toBe('English description'); | ||
| expect(result.conversationStarters).toEqual(['Hello', 'Help me']); | ||
| expect(result.systemInstructions).toBe( | ||
| 'You are a helpful English assistant.', | ||
| ); | ||
| }); | ||
|
|
||
| it('falls back to top-level for unknown locale', () => { | ||
| const result = resolveAgentLocale(agent, 'es', 'en'); | ||
| it('falls back to i18n.en when requested locale has no entry', () => { | ||
| const result = resolveAgentLocale(i18nFirstAgent, 'fr'); | ||
| expect(result.displayName).toBe('Agent de chat'); // fr override | ||
| expect(result.description).toBe('English description'); // en fallback | ||
| expect(result.conversationStarters).toEqual(['Hello', 'Help me']); // en | ||
| expect(result.systemInstructions).toBe( | ||
| 'You are a helpful English assistant.', | ||
| ); | ||
| }); | ||
|
|
||
| expect(result).toEqual({ | ||
| displayName: 'Chat Agent', | ||
| description: 'English description', | ||
| conversationStarters: ['Hello', 'Help me'], | ||
| }); | ||
| it('resolves legacy agent (no i18n) to top-level fields', () => { | ||
| const result = resolveAgentLocale(legacyAgent, 'de'); | ||
| expect(result.displayName).toBe('Legacy'); | ||
| expect(result.description).toBe('Old format'); | ||
| expect(result.conversationStarters).toEqual(['Hi']); | ||
| expect(result.systemInstructions).toBe('You are legacy.'); | ||
| }); | ||
|
|
||
| it('handles agent with no i18n field', () => { | ||
| const simpleAgent = { | ||
| displayName: 'Simple', | ||
| description: 'No translations', | ||
| it('falls through to top-level when neither i18n[locale] nor i18n.en have the field', () => { | ||
| const agent = { | ||
| displayName: 'Top-level Name', | ||
| systemInstructions: 'Top-level instructions', | ||
| i18n: { | ||
| de: { displayName: 'DE Name' }, | ||
| }, | ||
| }; | ||
| const result = resolveAgentLocale(simpleAgent, 'de', 'en'); | ||
| const result = resolveAgentLocale(agent, 'fr'); | ||
| expect(result.displayName).toBe('Top-level Name'); | ||
| expect(result.systemInstructions).toBe('Top-level instructions'); | ||
| }); | ||
|
|
||
| expect(result).toEqual({ | ||
| displayName: 'Simple', | ||
| description: 'No translations', | ||
| conversationStarters: undefined, | ||
| }); | ||
| it('returns empty displayName when no source has one', () => { | ||
| const result = resolveAgentLocale({}, 'en'); | ||
| expect(result.displayName).toBe(''); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Add an explicit BCP-47 base-language fallback test.
This suite should directly assert region-variant narrowing (e.g., de-AT resolving via i18n.de) to protect the intended precedence behavior.
🧪 Suggested test case
describe('resolveAgentLocale', () => {
+ it('uses base-language locale when region variant is requested', () => {
+ const result = resolveAgentLocale(i18nFirstAgent, 'de-AT');
+ expect(result.displayName).toBe('Chat-Assistent');
+ expect(result.description).toBe('English description'); // en fallback
+ expect(result.conversationStarters).toEqual(['Hallo', 'Hilf mir']);
+ expect(result.systemInstructions).toBe(
+ 'Du bist ein hilfreicher deutscher Assistent.',
+ );
+ });
+
it('returns i18n[locale] values when fully present', () => {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@services/platform/lib/shared/utils/__tests__/resolve-agent-locale.test.ts`
around lines 34 - 100, Add a new unit test in the resolveAgentLocale suite that
asserts BCP-47 base-language fallback: call resolveAgentLocale(i18nFirstAgent,
'de-AT') and verify it selects fields from i18n.de (e.g., displayName ===
'Chat-Assistent', conversationStarters === ['Hallo','Hilf mir'],
systemInstructions === 'Du bist ein hilfreicher deutscher Assistent.') to ensure
region-variant narrowing uses the base language; reference the
resolveAgentLocale function and the existing i18nFirstAgent fixture so the test
follows the same patterns as the other cases.
| * Resolves locale-specific agent fields with i18n-first precedence: | ||
| * 1. `i18n[requestedLocale].<field>` | ||
| * 2. `i18n[baseLanguage].<field>` — e.g. `fr-CH` narrows to `fr` | ||
| * 3. `i18n[appDefault='en'].<field>` | ||
| * 4. top-level `<field>` (legacy fallback for pre-i18n agents) |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Extract this precedence chain into one shared helper.
resolveAgentLocale() now duplicates the same requested → base-language → en → legacy fallback logic that services/platform/convex/agents/config.ts:16-35 already carries for systemInstructions. If those drift, the UI can resolve one value while the server serializes another. Please centralize the field-resolution logic and have both paths call it.
Also applies to: 33-63
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@services/platform/lib/shared/utils/resolve-agent-locale.ts` around lines 20 -
24, The locale-field precedence logic (requested locale → base language → app
default 'en' → top-level legacy field) is duplicated in resolveAgentLocale() and
the systemInstructions resolution in services/platform/convex/agents/config.ts;
extract that logic into a single helper (e.g., resolveLocaleField or
resolveAgentLocaleField) that accepts (i18nMap, requestedLocale, fieldName,
appDefault='en', topLevelFallback?) and returns the resolved string following
the four-step precedence, then replace the inlined logic in resolveAgentLocale()
and the systemInstructions resolution to call this helper; update
imports/exports accordingly so both modules use the shared helper.
| * Does NOT call the LLM translator — de/fr gaps remain for the author (or a | ||
| * follow-up script) to fill via the shipped auto-translate UI. | ||
| * | ||
| * Usage: tsx services/platform/scripts/backfill_example_i18n.ts |
There was a problem hiding this comment.
Fix the usage command path typo.
Line 12 references backfill_example_i18n.ts, but the file is backfill-example-i18n.ts.
Proposed fix
- * Usage: tsx services/platform/scripts/backfill_example_i18n.ts
+ * Usage: tsx services/platform/scripts/backfill-example-i18n.ts📝 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.
| * Usage: tsx services/platform/scripts/backfill_example_i18n.ts | |
| * Usage: tsx services/platform/scripts/backfill-example-i18n.ts |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@services/platform/scripts/backfill-example-i18n.ts` at line 12, Update the
usage comment string to reference the actual filename; change the commented
usage "tsx services/platform/scripts/backfill_example_i18n.ts" to "tsx
services/platform/scripts/backfill-example-i18n.ts" so the usage example matches
the real file name (look for the top-of-file usage comment that contains
backfill_example_i18n.ts).
| import { readFileSync, writeFileSync, readdirSync } from 'node:fs'; | ||
| import { join, dirname, resolve as resolvePath } from 'node:path'; | ||
| import { fileURLToPath } from 'node:url'; |
There was a problem hiding this comment.
Sort import specifiers to match repo import-order rules.
Please sort imported members consistently.
Proposed fix
-import { readFileSync, writeFileSync, readdirSync } from 'node:fs';
-import { join, dirname, resolve as resolvePath } from 'node:path';
+import { readdirSync, readFileSync, writeFileSync } from 'node:fs';
+import { dirname, join, resolve as resolvePath } from 'node:path';📝 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.
| import { readFileSync, writeFileSync, readdirSync } from 'node:fs'; | |
| import { join, dirname, resolve as resolvePath } from 'node:path'; | |
| import { fileURLToPath } from 'node:url'; | |
| import { readdirSync, readFileSync, writeFileSync } from 'node:fs'; | |
| import { dirname, join, resolve as resolvePath } from 'node:path'; | |
| import { fileURLToPath } from 'node:url'; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@services/platform/scripts/backfill-example-i18n.ts` around lines 15 - 17, The
import specifiers are not consistently sorted; please reorder them to follow the
repo import-order rules by alphabetizing members within each import and ordering
import statements per project convention: for these lines, alphabetize the
node:fs named imports (readFileSync, readdirSync, writeFileSync) and the
node:path named imports (dirname, join, resolvePath), and keep the node:url
import (fileURLToPath) grouped appropriately with the other node imports so all
import specifiers are consistently sorted.
| interface AgentFile { | ||
| [key: string]: unknown; | ||
| i18n?: Record<string, Record<string, unknown>>; | ||
| } |
There was a problem hiding this comment.
Remove unknown and as from this script’s types.
This script currently uses both unknown and an as assertion (Line 78), which violates the repo TypeScript rule.
Proposed fix
+type JsonValue =
+ | string
+ | number
+ | boolean
+ | null
+ | { [key: string]: JsonValue }
+ | JsonValue[];
+
interface AgentFile {
- [key: string]: unknown;
- i18n?: Record<string, Record<string, unknown>>;
+ [key: string]: JsonValue;
+ i18n?: Record<string, Record<string, JsonValue>>;
}
@@
- const agent = JSON.parse(raw) as AgentFile;
+ const agent: AgentFile = JSON.parse(raw);Also applies to: 78-78
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@services/platform/scripts/backfill-example-i18n.ts` around lines 26 - 29, The
AgentFile interface uses unknown and the script uses an `as` cast around the
i18n data; replace `unknown` with a concrete type (e.g., make i18n?:
Record<string, Record<string, string>> or another precise shape that matches
your i18n values) and remove the `as` assertion at the location where the
parsed/loaded data is cast (the code around the `as` on line 78). Instead of
casting, parse/assign the value into a variable annotated with the concrete type
and perform a simple runtime validation/type guard before using it (or
map/transform the raw object into the strongly typed shape). Update the
AgentFile interface and the variable handling the parsed i18n so no `unknown`,
`any`, or `as` remain.
Server-side `normalizeAgentConfig` is the single chokepoint for canonical agent state, keyed to the org's configured `defaultLocale`. The UI writes naive payloads into `i18n[locale]` and the server strips empty placeholders (I-2) and retires the legacy top-level fields per-field when the default locale carries content (I-1). Legacy-only agents (I-4) are untouched so the resolver's fallback chain keeps working for pre-migration data. - `normalizeAgentConfig(config, defaultLocale)` wired into `saveAgent`, `duplicateAgent`, and — as a softened validator — `restoreFromHistory`. Snapshot/restore stay byte-faithful; old snapshots that no longer pass the current schema log a warning instead of failing the restore. Prod warn-log when normalize changes anything for observability. - `duplicateAgent` no longer yields "undefined (Copy)" for i18n-only sources. Each populated i18n locale gets its own suffixed displayName; legacy top-level falls back to the agent slug via `resolveAgentLocale`. - `buildDelegationTools` passes a real org-scoped `orgSlug` (was hardcoded `'default'`, a multi-tenant bug). `orgSlug` + `orgLocale` are hoisted into the outer `Promise.all` and shared with `buildWorkflowTools` and model lookup — one query instead of three. - Resolver and normalize share `isEffectivelyEmpty` so `""`, `[]`, and whitespace-only values fall through the i18n chain instead of masquerading as present. `pickField` preserves `displayName: string` via an explicit `?? ''` guard. - `narrowBcp47` extracted so `resolve-agent-locale` and the delegation scaffold lookup agree: `fr-CH` narrows to `fr` before falling to `en`. - Agent editor UI drops all client-side legacy-retirement logic; server normalization takes over. Fixes the `||` fallthrough in `instructions.tsx` that leaked legacy content into non-default locale tabs. - Page title resolves `displayName` through the locale chain and refetches on save so it stays in sync with server-normalized state. - `agent-navigation` normalizes client-side inside `markSaved` so the dirty-state baseline matches disk, and surfaces schema errors in the save-failed toast. - Org-settings helper text moves to `Select.description` (proper `aria-describedby`) and invalidates the auth-session query so in-flight tabs see locale changes without reload. - CI test asserts every `examples/agents/*.json` is already normalized, since `scaffoldNewOrganization` copies examples into new orgs outside the write boundary. - Delete the one-shot `backfill-example-i18n.ts` script (migration done; a future one will be written fresh).
… lists `agents-table` (admin), `top-agents-table` (analytics), and the agent `delegation` picker filtered `listAgents` output by `typeof displayName === 'string'` (or `!== undefined`). After the write-boundary normalization clears top-level translatable fields, i18n-first agents have `displayName: undefined` at the raw level — so every migrated agent silently disappeared from those three lists. Each consumer now calls `resolveAgentLocale(agent, userLocale)` (matching what `useChatAgents` already does for the chat picker) and falls back to the slug only when no locale has a displayName. Error entries are recognised by their `status` field instead.
…undary - Drop unused re-exports of TRANSLATABLE_FIELDS / TranslatableField; the "exposed for the delegation-scaffold lookup" consumer doesn't exist, and Knip was failing on the dead exports. - AgentNavigation now reads org metadata via useOrganization; mock it in agent-navigation.test.tsx so the axe audit renders without hitting api.organizations.queries on the mocked api object.
Summary
Two intertwined fixes to the org-level "What language should your agents use?" dropdown:
i18n.<locale>.*, so onceen/de/frwere all populated the top-level copy was redundant.Now the dropdown truthfully controls agent output language (renamed to "Agent response language"), and the data model is i18n-first with top-level fields demoted to legacy fallbacks. Display chrome continues to resolve by each user's own UI locale, so agent labels in the sidebar / picker / composer menu follow the reader.
Resolution precedence
BCP-47 narrowing so
fr-FR/de-AT/fr-CHstill match their base language. SchemasuperRefinerequiresdisplayName(andsystemInstructionsfor chat agents) in at least one locale.Scope
getOrganizationDefaultLocaleinternal query, parallelized intoresolveAgentConfigInlineandresolveAgentConfig(no TTFT regression).toSerializableConfigtakes an optionallocaleand resolvessystemInstructionswith the same precedence. Delegation threaded — parent + delegates speak the same language; scaffold text ("DELEGATION AGENTS" / "You can delegate...") localized in en / de / fr.useChatAgentsresolves against the user's UI locale so the agent picker, composer-mode menu, and chat welcome all render in the reader's language — independent of the org-wide output setting.useEffectiveAgentcall simplified.LocaleTabscomponent powers tabbed i18n editing with auto-translate on the instructions tab (new, forsystemInstructions) and the general tab (displayName/description). The conversation-starters editor writes its default-tab edits intoi18n[defaultLocale]and retires the legacy top-level field on first save.fradded toSUPPORTED_AGENT_LOCALES;getOrganizationDefaultLocaleclamps stale values to the supported list.examples/agents/restructured to the i18n-first shape with complete en + de + fr translations for every translatable field. A dev script (scripts/backfill-example-i18n.ts) mechanically moved top-level →i18n.en; translations were generated by parallel sub-agents and committed as-is.forbidden / verboten / interdit).Hybrid semantics (confirmed with the user)
defaultLocale— an editorial decision applied org-wide.Test plan
npx tsc --noEmitcleannpm run lint --workspace=@tale/platformcleannpx vitest run— 5069/5069 pass (newconfig.test.tscovers server-side resolution precedence;resolve-agent-locale.test.tsrewritten for i18n-first;use-effective-agent.test.tsand the TTFT test updated for the new parallel query)agentJsonSchema(including the newsuperRefine)orgLocale=dedelegates → delegate responds in German; delegation scaffold text also in German.Summary by CodeRabbit
Release Notes