feat: configurable displayName prefix for agent messages in group chats (#255)

Add optional displayName field to agent config. When set, outbound
agent responses are prefixed (e.g. "💜 Signo: Hello!").

Useful in multi-agent group chats where multiple bots share a channel
and users need to tell them apart.

Closes #252

Written by Cameron ◯ Letta Code

"The details are not the details. They make the design." -- Charles Eames
This commit is contained in:
Cameron
2026-02-10 12:08:45 -08:00
committed by GitHub
parent dad510150a
commit df18cba565
7 changed files with 78 additions and 8 deletions

View File

@@ -126,6 +126,7 @@ The default config uses `agent:` and `channels:` at the top level for a single a
|--------|------|-------------|
| `agent.id` | string | Use existing agent (skips creation) |
| `agent.name` | string | Name for new agent |
| `agent.displayName` | string | Prefix outbound messages (e.g. `"💜 Signo"`) |
> **Note:** The model is configured on the Letta agent server-side, not in the config file.
> Use `lettabot model show` to see the current model and `lettabot model set <handle>` to change it.
@@ -146,6 +147,7 @@ server:
agents:
- name: work-assistant
# displayName: "🔧 Work" # Optional: prefix outbound messages
model: claude-sonnet-4
# id: agent-abc123 # Optional: use existing agent
channels:
@@ -184,6 +186,7 @@ Each entry in `agents:` accepts:
|--------|------|----------|-------------|
| `name` | string | Yes | Agent name (used for display, creation, and state isolation) |
| `id` | string | No | Use existing agent ID (skips creation) |
| `displayName` | string | No | Prefix outbound messages (e.g. `"💜 Signo"`) |
| `model` | string | No | Model for agent creation |
| `channels` | object | No | Channel configs (same schema as top-level `channels:`). At least one agent must have channels. |
| `features` | object | No | Per-agent features (cron, heartbeat, maxToolCalls) |

View File

@@ -15,6 +15,7 @@ server:
agent:
name: LettaBot
# displayName: "💜 Signo" # Prefix outbound messages (useful in multi-agent group chats)
# Note: model is configured on the Letta agent server-side.
# Select a model during `lettabot onboard` or change it with `lettabot model set <handle>`.

View File

@@ -332,4 +332,46 @@ describe('normalizeAgents', () => {
expect(agents[0].polling).toEqual(config.polling);
expect(agents[0].integrations).toEqual(config.integrations);
});
it('should pass through displayName', () => {
const config: LettaBotConfig = {
server: { mode: 'cloud' },
agent: {
name: 'Signo',
displayName: '💜 Signo',
},
channels: {
telegram: { enabled: true, token: 'test-token' },
},
};
const agents = normalizeAgents(config);
expect(agents[0].displayName).toBe('💜 Signo');
});
it('should pass through displayName in multi-agent config', () => {
const agentsArray: AgentConfig[] = [
{
name: 'Signo',
displayName: '💜 Signo',
channels: { telegram: { enabled: true, token: 't1' } },
},
{
name: 'DevOps',
displayName: '👾 DevOps',
channels: { discord: { enabled: true, token: 'd1' } },
},
];
const config = {
server: { mode: 'cloud' as const },
agents: agentsArray,
} as LettaBotConfig;
const agents = normalizeAgents(config);
expect(agents[0].displayName).toBe('💜 Signo');
expect(agents[1].displayName).toBe('👾 DevOps');
});
});

View File

@@ -15,6 +15,8 @@ export interface AgentConfig {
name: string;
/** Use existing agent ID (skip creation) */
id?: string;
/** Display name prefixed to outbound messages (e.g. "💜 Signo") */
displayName?: string;
/** Model for initial agent creation */
model?: string;
/** Channels this agent connects to */
@@ -62,6 +64,7 @@ export interface LettaBotConfig {
agent: {
id?: string;
name: string;
displayName?: string;
// model is configured on the Letta agent server-side, not in config
// Kept as optional for backward compat (ignored if present in existing configs)
model?: string;
@@ -330,6 +333,7 @@ export function normalizeAgents(config: LettaBotConfig): AgentConfig[] {
return [{
name: agentName,
id,
displayName: config.agent?.displayName,
model,
channels,
features: config.features,

View File

@@ -133,6 +133,19 @@ export class LettaBot implements AgentSession {
console.log(`LettaBot initialized. Agent ID: ${this.store.agentId || '(new)'}`);
}
// =========================================================================
// Response prefix (for multi-agent group chat identification)
// =========================================================================
/**
* Prepend configured displayName prefix to outbound agent responses.
* Returns text unchanged if no prefix is configured.
*/
private prefixResponse(text: string): string {
if (!this.config.displayName) return text;
return `${this.config.displayName}: ${text}`;
}
// =========================================================================
// Session options (shared by processMessage and sendToAgent)
// =========================================================================
@@ -630,10 +643,11 @@ export class LettaBot implements AgentSession {
}
if (response.trim()) {
try {
const prefixed = this.prefixResponse(response);
if (messageId) {
await adapter.editMessage(msg.chatId, messageId, response);
await adapter.editMessage(msg.chatId, messageId, prefixed);
} else {
await adapter.sendMessage({ chatId: msg.chatId, text: response, threadId: msg.threadId });
await adapter.sendMessage({ chatId: msg.chatId, text: prefixed, threadId: msg.threadId });
}
sentAnyMessage = true;
} catch {
@@ -703,10 +717,11 @@ export class LettaBot implements AgentSession {
const streamText = stripActionsBlock(response).trim();
if (canEdit && !mayBeHidden && streamText.length > 0 && Date.now() - lastUpdate > 500) {
try {
const prefixedStream = this.prefixResponse(streamText);
if (messageId) {
await adapter.editMessage(msg.chatId, messageId, streamText);
await adapter.editMessage(msg.chatId, messageId, prefixedStream);
} else {
const result = await adapter.sendMessage({ chatId: msg.chatId, text: streamText, threadId: msg.threadId });
const result = await adapter.sendMessage({ chatId: msg.chatId, text: prefixedStream, threadId: msg.threadId });
messageId = result.messageId;
sentAnyMessage = true;
}
@@ -818,18 +833,19 @@ export class LettaBot implements AgentSession {
// Send final response
if (response.trim()) {
const prefixedFinal = this.prefixResponse(response);
try {
if (messageId) {
await adapter.editMessage(msg.chatId, messageId, response);
await adapter.editMessage(msg.chatId, messageId, prefixedFinal);
} else {
await adapter.sendMessage({ chatId: msg.chatId, text: response, threadId: msg.threadId });
await adapter.sendMessage({ chatId: msg.chatId, text: prefixedFinal, threadId: msg.threadId });
}
sentAnyMessage = true;
this.store.resetRecoveryAttempts();
} catch {
// Edit failed -- send as new message so user isn't left with truncated text
try {
await adapter.sendMessage({ chatId: msg.chatId, text: response, threadId: msg.threadId });
await adapter.sendMessage({ chatId: msg.chatId, text: prefixedFinal, threadId: msg.threadId });
sentAnyMessage = true;
this.store.resetRecoveryAttempts();
} catch (retryError) {
@@ -982,7 +998,7 @@ export class LettaBot implements AgentSession {
}
if (options.text) {
const result = await adapter.sendMessage({ chatId, text: options.text });
const result = await adapter.sendMessage({ chatId, text: this.prefixResponse(options.text) });
return result.messageId;
}

View File

@@ -125,6 +125,9 @@ export interface BotConfig {
agentName?: string; // Name for the agent (set via API after creation)
allowedTools: string[];
// Display
displayName?: string; // Prefix outbound messages (e.g. "💜 Signo")
// Skills
skills?: SkillsConfig;

View File

@@ -472,6 +472,7 @@ async function main() {
workingDir: globalConfig.workingDir,
agentName: agentConfig.name,
allowedTools: globalConfig.allowedTools,
displayName: agentConfig.displayName,
maxToolCalls: agentConfig.features?.maxToolCalls,
skills: {
cronEnabled: agentConfig.features?.cron ?? globalConfig.cronEnabled,