🏷️ fix: Persist the Ephemeral Agent's Display Label as Sender (#14899)

* 🏷️ feat: Add getEphemeralSender and Cover the Ephemeral-Id Format

* ♻️ refactor: Consolidate the Ephemeral Sender Chains

* 🏷️ fix: Decode the Ephemeral Sender for Persisted Messages

* 🏷️ fix: Mirror the Persisted Sender Chain in useGetSender

* ✅ test: Widen the Custom-Endpoint Fixture Type

* ✅ test: Expect the Spec Label in the Composer Placeholder

* 🏷️ fix: Resolve the Sender from Exact Labels, Not the Lossy Id
This commit is contained in:
Danny Avila 2026-08-16 19:50:30 -04:00 • committed by GitHub
parent c939a6fb17
commit 27ed491a2a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 561 additions and 44 deletions

View file

@ -2,6 +2,7 @@ const { logger } = require('@librechat/data-schemas');
const { createContentAggregator, GraphNodeKeys } = require('@librechat/agents');
const {
checkAccess,
resolveSender,
loadSkillStates,
initializeAgent,
isMemoryEnabled,
@ -28,7 +29,6 @@ const {
PermissionTypes,
MAX_SUBAGENT_DEPTH,
isAgentsEndpoint,
getResponseSender,
AgentCapabilities,
Tools,
MAX_SUBAGENT_GRAPH_NODES,
@ -1070,20 +1070,22 @@ const initializeClient = async ({
});
} catch (err) {
logger.error(
'[api/server/controllers/agents/client.js #titleConvo] Error getting custom endpoint config',
'[/api/server/services/Endpoints/agents/initialize.js] Error getting custom endpoint config',
err,
);
}
}
const sender =
primaryAgent.name ??
getResponseSender({
const sender = resolveSender({
agent: primaryConfig,
specLabel: selectedModelSpec?.label,
endpointOption: {
...endpointOption,
model: endpointOption.model_parameters.model,
modelDisplayLabel: endpointConfig?.modelDisplayLabel,
modelLabel: endpointOption.model_parameters.modelLabel,
});
},
});
/** History priming uses the user's full ACL-accessible skill set (not
* per-agent scoped) because prior turns may reference skills no longer

View file

@ -0,0 +1,94 @@
import { RecoilRoot } from 'recoil';
import { renderHook } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { QueryKeys, EModelEndpoint, getResponseSender } from 'librechat-data-provider';
import type { TStartupConfig, TEndpointsConfig, TEndpointOption } from 'librechat-data-provider';
import type { ReactNode } from 'react';
import { startupConfigKey } from '~/data-provider';
import useGetSender from '../useGetSender';
const startupConfig = {
modelSpecs: {
list: [{ name: 'fast-qwen', label: 'Fast Qwen' }],
},
} as unknown as TStartupConfig;
const endpointsConfig = {
'Together AI': { modelDisplayLabel: 'Together' },
openAI: {},
} as unknown as TEndpointsConfig;
const renderGetSender = () => {
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false, enabled: false } },
});
queryClient.setQueryData([QueryKeys.endpoints], endpointsConfig);
queryClient.setQueryData(startupConfigKey(false), startupConfig);
const { result } = renderHook(() => useGetSender(), {
wrapper: function Wrapper({ children }: { children: ReactNode }) {
return (
<RecoilRoot>
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
</RecoilRoot>
);
},
});
return result.current;
};
const asOption = (option: Record<string, unknown>) => option as TEndpointOption;
describe('useGetSender', () => {
it('prefers modelLabel over the spec and endpoint labels', () => {
const getSender = renderGetSender();
expect(
getSender(
asOption({
endpoint: 'Together AI',
endpointType: EModelEndpoint.custom,
model: 'Qwen/Qwen2.5-72B-Instruct',
modelLabel: 'My Qwen',
spec: 'fast-qwen',
}),
),
).toBe('My Qwen');
});
it('surfaces the model spec label when no modelLabel is set', () => {
const getSender = renderGetSender();
expect(
getSender(
asOption({
endpoint: 'Together AI',
endpointType: EModelEndpoint.custom,
model: 'Qwen/Qwen2.5-72B-Instruct',
spec: 'fast-qwen',
}),
),
).toBe('Fast Qwen');
});
it('lets the explicit modelDisplayLabel beat the family heuristic', () => {
const getSender = renderGetSender();
expect(
getSender(
asOption({
endpoint: 'Together AI',
endpointType: EModelEndpoint.custom,
model: 'mistralai/Mixtral-8x7B-Instruct-v0.1',
}),
),
).toBe('Together');
});
it('falls through to getResponseSender when no label resolves', () => {
const getSender = renderGetSender();
expect(getSender(asOption({ endpoint: EModelEndpoint.anthropic }))).toBe('Claude');
const unknownCustom = {
endpoint: 'LocalAI',
endpointType: EModelEndpoint.custom,
model: 'some-unknown-model',
};
expect(getSender(asOption(unknownCustom))).toBe(getResponseSender(asOption(unknownCustom)));
});
});

View file

@ -1,15 +1,28 @@
import { useCallback } from 'react';
import { getResponseSender } from 'librechat-data-provider';
import { getResponseSender, getEphemeralSender } from 'librechat-data-provider';
import type { TEndpointOption, TEndpointsConfig } from 'librechat-data-provider';
import { useGetEndpointsQuery } from '~/data-provider';
import { useGetEndpointsQuery, useGetStartupConfig } from '~/data-provider';
import { getModelSpec } from '~/utils';
/**
* Mirrors the server's sender resolution (`modelLabel` → spec label → endpoint
* `modelDisplayLabel` → `getResponseSender`) so the optimistic streaming label
* and composer placeholder match the persisted `message.sender`.
*/
export default function useGetSender() {
const { data: startupConfig } = useGetStartupConfig();
const { data: endpointsConfig = {} as TEndpointsConfig } = useGetEndpointsQuery();
return useCallback(
(endpointOption: TEndpointOption) => {
const { modelDisplayLabel } = endpointsConfig?.[endpointOption.endpoint ?? ''] ?? {};
return getResponseSender({ ...endpointOption, modelDisplayLabel });
const modelSpec = getModelSpec({ specName: endpointOption.spec, startupConfig });
const sender = getEphemeralSender({
modelLabel: endpointOption.modelLabel,
specLabel: modelSpec?.label,
modelDisplayLabel,
});
return sender || getResponseSender({ ...endpointOption, modelDisplayLabel });
},
[endpointsConfig],
[endpointsConfig, startupConfig],
);
}

View file

@ -1,6 +1,7 @@
import { QueryClient } from '@tanstack/react-query';
import { Constants, QueryKeys } from 'librechat-data-provider';
import type { TMessage, TConversation } from 'librechat-data-provider';
import type { TEndpointsConfig } from 'librechat-data-provider';
import type { LocalizeFunction, TMessageProps } from '~/common';
import {
clearMessagesCache,
@ -12,6 +13,7 @@ import {
areMessageFieldsEqual,
areMessageRowPropsEqual,
isSubmittableMessage,
createDualMessageContent,
} from '../messages';
const translations: Record<string, string> = {
@ -369,3 +371,59 @@ describe('isSubmittableMessage', () => {
expect(isSubmittableMessage('Translate this', 1)).toBe(true);
});
});
describe('createDualMessageContent', () => {
/** Custom endpoints carry their configured name (e.g. "Together AI") in
* `endpoint` at runtime, which `TConversation` types as `EModelEndpoint`. */
const asConvo = (convo: Partial<Omit<TConversation, 'endpoint'>> & { endpoint: string }) =>
convo as unknown as TConversation;
const agentIds = (parts: ReturnType<typeof createDualMessageContent>) =>
parts.map((part) => (part as unknown as { agentId: string }).agentId);
it('encodes the model spec label into ephemeral agent ids', () => {
const parts = createDualMessageContent(
asConvo({ endpoint: 'Together AI', model: 'Qwen/Qwen2.5-72B-Instruct', spec: 'fast-qwen' }),
asConvo({ endpoint: 'openAI', model: 'gpt-4o', modelLabel: 'My GPT' }),
undefined,
[{ name: 'fast-qwen', label: 'Fast Qwen' }],
);
expect(agentIds(parts)).toEqual([
'Together AI__Qwen/Qwen2.5-72B-Instruct___Fast Qwen',
'openAI__gpt-4o___My GPT____1',
]);
});
it('falls back to the endpoint modelDisplayLabel when no labels are set', () => {
const endpointsConfig = {
'Together AI': { modelDisplayLabel: 'Together' },
} as unknown as TEndpointsConfig;
const parts = createDualMessageContent(
asConvo({ endpoint: 'Together AI', model: 'mixtral-8x7b' }),
asConvo({ endpoint: 'Together AI', model: 'mixtral-8x7b' }),
endpointsConfig,
);
expect(agentIds(parts)).toEqual([
'Together AI__mixtral-8x7b___Together',
'Together AI__mixtral-8x7b___Together____1',
]);
});
it('omits the sender segment entirely when no label resolves', () => {
const parts = createDualMessageContent(
asConvo({ endpoint: 'Together AI', model: 'mixtral-8x7b' }),
asConvo({ endpoint: 'Together AI', model: 'mixtral-8x7b' }),
);
expect(agentIds(parts)).toEqual([
'Together AI__mixtral-8x7b',
'Together AI__mixtral-8x7b____1',
]);
});
it('passes real agent ids through, suffixing only the added agent', () => {
const parts = createDualMessageContent(
asConvo({ endpoint: 'agents', agent_id: 'agent_abc123' }),
asConvo({ endpoint: 'agents', agent_id: 'agent_abc123' }),
);
expect(agentIds(parts)).toEqual(['agent_abc123', 'agent_abc123____1']);
});
});

View file

@ -4,6 +4,7 @@ import {
buildTree,
ContentTypes,
isEphemeralAgentId,
getEphemeralSender,
appendAgentIdSuffix,
encodeEphemeralAgentId,
} from 'librechat-data-provider';
@ -494,13 +495,13 @@ export const createDualMessageContent = (
primaryConvo.spec != null && primaryConvo.spec !== ''
? modelSpecs?.find((s) => s.name === primaryConvo.spec)
: undefined;
// For ephemeral agents, use modelLabel if provided, then model spec's label,
// then modelDisplayLabel from endpoint config, otherwise empty string to show model name
const primarySender =
primaryConvo.modelLabel ??
primarySpec?.label ??
(primaryEndpoint ? endpointsConfig?.[primaryEndpoint]?.modelDisplayLabel : undefined) ??
'';
const primarySender = getEphemeralSender({
modelLabel: primaryConvo.modelLabel,
specLabel: primarySpec?.label,
modelDisplayLabel: primaryEndpoint
? endpointsConfig?.[primaryEndpoint]?.modelDisplayLabel
: undefined,
});
primaryAgentId = encodeEphemeralAgentId({
endpoint: primaryEndpoint ?? '',
model: primaryModel,
@ -534,13 +535,13 @@ export const createDualMessageContent = (
addedConvo.spec != null && addedConvo.spec !== ''
? modelSpecs?.find((s) => s.name === addedConvo.spec)
: undefined;
// For ephemeral agents, use modelLabel if provided, then model spec's label,
// then modelDisplayLabel from endpoint config, otherwise empty string to show model name
const addedSender =
addedConvo.modelLabel ??
addedSpec?.label ??
(addedEndpoint ? endpointsConfig?.[addedEndpoint]?.modelDisplayLabel : undefined) ??
'';
const addedSender = getEphemeralSender({
modelLabel: addedConvo.modelLabel,
specLabel: addedSpec?.label,
modelDisplayLabel: addedEndpoint
? endpointsConfig?.[addedEndpoint]?.modelDisplayLabel
: undefined,
});
addedAgentId = encodeEphemeralAgentId({
endpoint: addedEndpoint ?? '',
model: addedModel,

View file

@ -262,7 +262,11 @@ test.describe('soft default model spec', () => {
const main = page.getByRole('main');
const composer = page.getByRole('textbox', { name: 'Message input' });
const user = getPrimaryE2EUser();
await expect(composer).toHaveAttribute('placeholder', /Mock Provider A/, { timeout: 15000 });
// The placeholder mirrors the sender chain, so a spec-launched chat shows the
// spec label (matching the model selector), not the endpoint's display label.
await expect(composer).toHaveAttribute('placeholder', new RegExp(SOFT_DEFAULT_LABEL), {
timeout: 15000,
});
await expect(main).toContainText(user.name, { timeout: 15000 });
await expect(main).not.toContainText(agentName);
await expect(main).not.toContainText(agentDescription);

View file

@ -4,6 +4,7 @@ import {
Constants,
isAgentsEndpoint,
isEphemeralAgentId,
getEphemeralSender,
appendAgentIdSuffix,
encodeEphemeralAgentId,
} from 'librechat-data-provider';
@ -146,11 +147,11 @@ export async function loadAddedAgent(
const modelSpecs = (appConfig?.modelSpecs as { list?: TModelSpec[] })?.list;
const modelSpec = spec != null && spec !== '' ? modelSpecs?.find((s) => s.name === spec) : null;
const sender =
rest.modelLabel ??
modelSpec?.label ??
(endpointConfig?.modelDisplayLabel as string | undefined) ??
'';
const sender = getEphemeralSender({
modelLabel: rest.modelLabel,
specLabel: modelSpec?.label,
modelDisplayLabel: endpointConfig?.modelDisplayLabel as string | undefined,
});
const ephemeralId = encodeEphemeralAgentId({ endpoint, model, sender, index: 1 });
const result: Record<string, unknown> = {
@ -268,11 +269,11 @@ export async function loadAddedAgent(
}
}
const sender =
rest.modelLabel ??
modelSpec?.label ??
(endpointConfig?.modelDisplayLabel as string | undefined) ??
'';
const sender = getEphemeralSender({
modelLabel: rest.modelLabel,
specLabel: modelSpec?.label,
modelDisplayLabel: endpointConfig?.modelDisplayLabel as string | undefined,
});
const ephemeralId = encodeEphemeralAgentId({ endpoint, model, sender, index: 1 });
const result: Record<string, unknown> = {

View file

@ -39,6 +39,7 @@ export * from './tools';
export * from './validation';
export * from './added';
export * from './load';
export * from './sender';
export * from './hitl';
export * from './hooks';
export * from './steering';

View file

@ -1,5 +1,7 @@
import { EModelEndpoint, parseEphemeralAgentId } from 'librechat-data-provider';
import type { LoadAgentDeps } from './load';
import { loadEphemeralAgent } from './load';
import { resolveSender } from './sender';
const deps: LoadAgentDeps = {
getAgent: async () => null,
@ -50,3 +52,51 @@ describe('loadEphemeralAgent ephemeral id stability (#14253 Bug 2)', () => {
expect(a).toEqual(b);
});
});
/** Custom endpoints carry their configured name in `endpoint` at runtime,
* which `TEndpointOption` types as `EModelEndpoint`. */
const customEndpointOption = {
endpoint: 'my-custom-endpoint' as EModelEndpoint,
model: 'claude-opus-4',
};
describe('loadEphemeralAgent → resolveSender parity', () => {
test('the persisted sender matches the spec label encoded into the agent id', async () => {
const agent = await loadEphemeralAgent(
{
req: baseReq,
spec: 'my-opus-spec',
endpoint: 'my-custom-endpoint',
model_parameters: { model: 'claude-opus-4' } as never,
},
deps,
);
expect(agent?.id).toBeTruthy();
const sender = resolveSender({
agent: { id: agent?.id },
specLabel: 'Spec Label',
endpointOption: customEndpointOption,
});
expect(sender).toBe('Spec Label');
expect(sender).toBe(parseEphemeralAgentId(agent?.id ?? '')?.sender);
});
test('a user modelLabel wins over the spec label in the persisted sender', async () => {
const agent = await loadEphemeralAgent(
{
req: baseReq,
spec: 'my-opus-spec',
endpoint: 'my-custom-endpoint',
model_parameters: { model: 'claude-opus-4', modelLabel: 'My Opus' } as never,
},
deps,
);
const sender = resolveSender({
agent: { id: agent?.id },
specLabel: 'Spec Label',
endpointOption: { ...customEndpointOption, modelLabel: 'My Opus' },
});
expect(sender).toBe('My Opus');
expect(sender).toBe(parseEphemeralAgentId(agent?.id ?? '')?.sender);
});
});

View file

@ -4,6 +4,7 @@ import {
Constants,
isAgentsEndpoint,
isEphemeralAgentId,
getEphemeralSender,
encodeEphemeralAgentId,
} from 'librechat-data-provider';
import type {
@ -134,19 +135,18 @@ export async function loadEphemeralAgent(
}
}
// For ephemeral agents, use modelLabel if provided, then model spec's label,
// then modelDisplayLabel from endpoint config, otherwise empty string to show model name
const sender =
(model_parameters as AgentModelParameters & { modelLabel?: string })?.modelLabel ??
modelSpec?.label ??
(endpointConfig as { modelDisplayLabel?: string } | undefined)?.modelDisplayLabel ??
'';
const sender = getEphemeralSender({
modelLabel: (model_parameters as AgentModelParameters & { modelLabel?: string })?.modelLabel,
specLabel: modelSpec?.label,
modelDisplayLabel: (endpointConfig as { modelDisplayLabel?: string } | undefined)
?.modelDisplayLabel,
});
// Encode ephemeral agent ID with endpoint, model, and computed sender for display
const ephemeralId = encodeEphemeralAgentId({
endpoint,
model: model as string,
sender: sender as string,
sender,
});
const result: Partial<Agent> = {

View file

@ -0,0 +1,119 @@
import { EModelEndpoint, getResponseSender, encodeEphemeralAgentId } from 'librechat-data-provider';
import type { TEndpointOption } from 'librechat-data-provider';
import { resolveSender } from './sender';
/** Custom endpoints carry their configured name (e.g. "Together AI") in
* `endpoint` at runtime, which `TEndpointOption` types as `EModelEndpoint`. */
const customOption = (
model: string,
labels: Partial<Pick<TEndpointOption, 'modelLabel' | 'modelDisplayLabel'>> = {},
): Partial<TEndpointOption> => ({
endpoint: 'Together AI' as EModelEndpoint,
endpointType: EModelEndpoint.custom,
model,
...labels,
});
const ephemeralAgent = (model: string, sender?: string) => ({
id: encodeEphemeralAgentId({ endpoint: 'Together AI', model, sender }),
});
describe('resolveSender', () => {
test('prefers a real agent name over everything else', () => {
expect(
resolveSender({
agent: { id: 'agent_abc123', name: 'My Agent' },
endpointOption: { endpoint: EModelEndpoint.anthropic },
}),
).toBe('My Agent');
});
test('returns an empty-string name as-is, preserving legacy `??` semantics', () => {
expect(
resolveSender({
agent: { id: 'agent_abc123', name: '' },
endpointOption: { endpoint: EModelEndpoint.anthropic },
}),
).toBe('');
});
test('skips the label chain for a nameless real agent', () => {
expect(
resolveSender({
agent: { id: 'agent_abc123', name: null },
specLabel: 'Spec Label',
endpointOption: { endpoint: EModelEndpoint.agents, modelLabel: 'A Label' },
}),
).toBe('');
});
test('resolves the modelLabel for an ephemeral agent', () => {
expect(
resolveSender({
agent: ephemeralAgent('Qwen/Qwen2.5-72B-Instruct', 'My Qwen'),
endpointOption: customOption('Qwen/Qwen2.5-72B-Instruct', { modelLabel: 'My Qwen' }),
}),
).toBe('My Qwen');
});
test('persists delimiter-heavy labels verbatim instead of decoding the lossy id', () => {
for (const modelLabel of ['A___B', 'My__Bot', 'v2____3']) {
expect(
resolveSender({
agent: ephemeralAgent('Qwen/Qwen2.5-72B-Instruct', modelLabel),
endpointOption: customOption('Qwen/Qwen2.5-72B-Instruct', { modelLabel }),
}),
).toBe(modelLabel);
}
});
test('falls back to the spec label, then the endpoint display label', () => {
expect(
resolveSender({
agent: ephemeralAgent('Qwen/Qwen2.5-72B-Instruct', 'Spec Label'),
specLabel: 'Spec Label',
endpointOption: customOption('Qwen/Qwen2.5-72B-Instruct', {
modelDisplayLabel: 'Together',
}),
}),
).toBe('Spec Label');
expect(
resolveSender({
agent: ephemeralAgent('mistralai/Mixtral-8x7B-Instruct-v0.1', 'Together'),
endpointOption: customOption('mistralai/Mixtral-8x7B-Instruct-v0.1', {
modelDisplayLabel: 'Together',
}),
}),
).toBe('Together');
});
test('keeps the family heuristic for a label-less custom endpoint', () => {
expect(
resolveSender({
agent: ephemeralAgent('mistralai/Mixtral-8x7B-Instruct-v0.1'),
endpointOption: customOption('mistralai/Mixtral-8x7B-Instruct-v0.1'),
}),
).toBe('Mistral');
});
test('keeps the model-derived name for a label-less anthropic agent', () => {
expect(
resolveSender({
agent: {
id: encodeEphemeralAgentId({
endpoint: EModelEndpoint.anthropic,
model: 'claude-sonnet-5',
}),
},
endpointOption: { endpoint: EModelEndpoint.anthropic, model: 'claude-sonnet-5' },
}),
).toBe('Claude');
});
test('matches getResponseSender exactly for an unknown label-less custom model', () => {
const endpointOption = customOption('some-unknown-model');
expect(resolveSender({ agent: ephemeralAgent('some-unknown-model'), endpointOption })).toBe(
getResponseSender(endpointOption),
);
});
});

View file

@ -0,0 +1,39 @@
import { getResponseSender, getEphemeralSender, isEphemeralAgentId } from 'librechat-data-provider';
import type { Agent, TEndpointOption } from 'librechat-data-provider';
/**
* Resolves the display name persisted as `message.sender` for a response.
*
* Real agents use their own name. Ephemeral agents run the same label chain
* `loadEphemeralAgent` encodes into their id (`modelLabel` → spec label →
* endpoint `modelDisplayLabel`), recomputed here from the exact source values
* rather than decoded from the id — the id encoding is lossy for labels
* containing `__`/`___` sequences, and the persisted sender must match the
* configured label verbatim. `getResponseSender` remains the fallback for
* label-less agents (model-derived names such as `Claude` or `GPT-5`, family
* heuristics, `'AI'`).
*/
export function resolveSender({
agent,
specLabel,
endpointOption,
}: {
agent: Partial<Pick<Agent, 'id' | 'name'>>;
specLabel?: string | null;
endpointOption: Partial<TEndpointOption>;
}): string {
if (agent.name != null) {
return agent.name;
}
if (agent.id != null && isEphemeralAgentId(agent.id)) {
const sender = getEphemeralSender({
modelLabel: endpointOption.modelLabel,
specLabel,
modelDisplayLabel: endpointOption.modelDisplayLabel,
});
if (sender) {
return sender;
}
}
return getResponseSender(endpointOption);
}

View file

@ -1,4 +1,12 @@
import { replaceSpecialVars, parseConvo, parseCompactConvo, parseTextParts } from '../src/parsers';
import {
parseConvo,
parseTextParts,
parseCompactConvo,
replaceSpecialVars,
getEphemeralSender,
encodeEphemeralAgentId,
parseEphemeralAgentId,
} from '../src/parsers';
import { specialVariables } from '../src/config';
import { EModelEndpoint, Providers } from '../src/schemas';
import { ContentTypes } from '../src/types/runs';
@ -701,3 +709,113 @@ describe('parseTextParts', () => {
);
});
});
describe('encodeEphemeralAgentId / parseEphemeralAgentId', () => {
test('round-trips endpoint and model without a sender', () => {
const id = encodeEphemeralAgentId({ endpoint: 'openAI', model: 'gpt-4o' });
expect(id).toBe('openAI__gpt-4o');
expect(parseEphemeralAgentId(id)).toEqual({
endpoint: 'openAI',
model: 'gpt-4o',
sender: undefined,
index: undefined,
});
});
test('round-trips a sender', () => {
const id = encodeEphemeralAgentId({
endpoint: 'Together AI',
model: 'Qwen/Qwen2.5-72B-Instruct',
sender: 'Fast Qwen',
});
expect(id).toBe('Together AI__Qwen/Qwen2.5-72B-Instruct___Fast Qwen');
expect(parseEphemeralAgentId(id)?.sender).toBe('Fast Qwen');
expect(parseEphemeralAgentId(id)?.model).toBe('Qwen/Qwen2.5-72B-Instruct');
});
test('round-trips a sender alongside an index suffix', () => {
const id = encodeEphemeralAgentId({
endpoint: 'openAI',
model: 'gpt-4o',
sender: 'GPT-4o',
index: 1,
});
expect(id).toBe('openAI__gpt-4o___GPT-4o____1');
expect(parseEphemeralAgentId(id)).toEqual({
endpoint: 'openAI',
model: 'gpt-4o',
sender: 'GPT-4o',
index: 1,
});
});
test('omits the sender segment for an empty sender, parsing back to undefined', () => {
const id = encodeEphemeralAgentId({ endpoint: 'openAI', model: 'gpt-4o', sender: '' });
expect(id).toBe('openAI__gpt-4o');
expect(parseEphemeralAgentId(id)?.sender).toBeUndefined();
});
test('restores colons in the endpoint, model, and sender', () => {
const id = encodeEphemeralAgentId({
endpoint: 'custom',
model: 'claude-3:opus',
sender: 'Label:With:Colons',
});
expect(parseEphemeralAgentId(id)).toEqual({
endpoint: 'custom',
model: 'claude-3:opus',
sender: 'Label:With:Colons',
index: undefined,
});
});
test('returns undefined for ids without the ephemeral format', () => {
expect(parseEphemeralAgentId('agent_abc123')).toBeUndefined();
});
/** Characterization of known format quirks (SiblingHeader and the persisted
* sender both decode this format, so lock the behavior rather than change it):
* the parser splits on the first `___` and keeps only the next segment, and
* restores every `__` in the sender to `:`. */
test('truncates a sender containing a triple underscore (known quirk)', () => {
const id = encodeEphemeralAgentId({ endpoint: 'openAI', model: 'gpt-4o', sender: 'A___B' });
expect(parseEphemeralAgentId(id)?.sender).toBe('A');
});
test('decodes a literal double underscore in a sender to a colon (known quirk)', () => {
const id = encodeEphemeralAgentId({ endpoint: 'openAI', model: 'gpt-4o', sender: 'My__Bot' });
expect(parseEphemeralAgentId(id)?.sender).toBe('My:Bot');
});
});
describe('getEphemeralSender', () => {
test('prefers modelLabel over the spec and endpoint labels', () => {
expect(
getEphemeralSender({
modelLabel: 'My Label',
specLabel: 'Spec Label',
modelDisplayLabel: 'Endpoint Label',
}),
).toBe('My Label');
});
test('falls back to the spec label, then the endpoint display label', () => {
expect(
getEphemeralSender({ specLabel: 'Spec Label', modelDisplayLabel: 'Endpoint Label' }),
).toBe('Spec Label');
expect(getEphemeralSender({ modelDisplayLabel: 'Endpoint Label' })).toBe('Endpoint Label');
});
test('returns an empty string when no label is set', () => {
expect(getEphemeralSender({})).toBe('');
expect(getEphemeralSender({ modelLabel: null, specLabel: null, modelDisplayLabel: null })).toBe(
'',
);
});
/** `??` chain: an empty-string label short-circuits, preserving the exact
* pre-consolidation behavior of every call site. */
test('an empty-string modelLabel short-circuits the chain', () => {
expect(getEphemeralSender({ modelLabel: '', specLabel: 'Spec Label' })).toBe('');
});
});

View file

@ -489,6 +489,23 @@ export type ParsedEphemeralAgentId = {
index?: number;
};
/**
* Resolves the display label ("sender") for an ephemeral agent:
* `modelLabel` (user/preset) → model spec's `label` → endpoint config's
* `modelDisplayLabel` → `''` (lets consumers fall back to the model name).
*/
export function getEphemeralSender({
modelLabel,
specLabel,
modelDisplayLabel,
}: {
modelLabel?: string | null;
specLabel?: string | null;
modelDisplayLabel?: string | null;
}): string {
return modelLabel ?? specLabel ?? modelDisplayLabel ?? '';
}
/**
* Encodes an ephemeral agent ID from endpoint, model, optional sender, and optional index.
* Uses __ to replace : (reserved in graph node names) and ___ to separate sender.