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.id` | string | Use existing agent (skips creation) |
| `agent.name` | string | Name for new agent | | `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. > **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. > Use `lettabot model show` to see the current model and `lettabot model set <handle>` to change it.
@@ -146,6 +147,7 @@ server:
agents: agents:
- name: work-assistant - name: work-assistant
# displayName: "🔧 Work" # Optional: prefix outbound messages
model: claude-sonnet-4 model: claude-sonnet-4
# id: agent-abc123 # Optional: use existing agent # id: agent-abc123 # Optional: use existing agent
channels: channels:
@@ -184,6 +186,7 @@ Each entry in `agents:` accepts:
|--------|------|----------|-------------| |--------|------|----------|-------------|
| `name` | string | Yes | Agent name (used for display, creation, and state isolation) | | `name` | string | Yes | Agent name (used for display, creation, and state isolation) |
| `id` | string | No | Use existing agent ID (skips creation) | | `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 | | `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. | | `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) | | `features` | object | No | Per-agent features (cron, heartbeat, maxToolCalls) |

View File

@@ -15,6 +15,7 @@ server:
agent: agent:
name: LettaBot name: LettaBot
# displayName: "💜 Signo" # Prefix outbound messages (useful in multi-agent group chats)
# Note: model is configured on the Letta agent server-side. # Note: model is configured on the Letta agent server-side.
# Select a model during `lettabot onboard` or change it with `lettabot model set <handle>`. # 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].polling).toEqual(config.polling);
expect(agents[0].integrations).toEqual(config.integrations); 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; name: string;
/** Use existing agent ID (skip creation) */ /** Use existing agent ID (skip creation) */
id?: string; id?: string;
/** Display name prefixed to outbound messages (e.g. "💜 Signo") */
displayName?: string;
/** Model for initial agent creation */ /** Model for initial agent creation */
model?: string; model?: string;
/** Channels this agent connects to */ /** Channels this agent connects to */
@@ -62,6 +64,7 @@ export interface LettaBotConfig {
agent: { agent: {
id?: string; id?: string;
name: string; name: string;
displayName?: string;
// model is configured on the Letta agent server-side, not in config // model is configured on the Letta agent server-side, not in config
// Kept as optional for backward compat (ignored if present in existing configs) // Kept as optional for backward compat (ignored if present in existing configs)
model?: string; model?: string;
@@ -330,6 +333,7 @@ export function normalizeAgents(config: LettaBotConfig): AgentConfig[] {
return [{ return [{
name: agentName, name: agentName,
id, id,
displayName: config.agent?.displayName,
model, model,
channels, channels,
features: config.features, features: config.features,

View File

@@ -133,6 +133,19 @@ export class LettaBot implements AgentSession {
console.log(`LettaBot initialized. Agent ID: ${this.store.agentId || '(new)'}`); 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) // Session options (shared by processMessage and sendToAgent)
// ========================================================================= // =========================================================================
@@ -630,10 +643,11 @@ export class LettaBot implements AgentSession {
} }
if (response.trim()) { if (response.trim()) {
try { try {
const prefixed = this.prefixResponse(response);
if (messageId) { if (messageId) {
await adapter.editMessage(msg.chatId, messageId, response); await adapter.editMessage(msg.chatId, messageId, prefixed);
} else { } 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; sentAnyMessage = true;
} catch { } catch {
@@ -703,10 +717,11 @@ export class LettaBot implements AgentSession {
const streamText = stripActionsBlock(response).trim(); const streamText = stripActionsBlock(response).trim();
if (canEdit && !mayBeHidden && streamText.length > 0 && Date.now() - lastUpdate > 500) { if (canEdit && !mayBeHidden && streamText.length > 0 && Date.now() - lastUpdate > 500) {
try { try {
const prefixedStream = this.prefixResponse(streamText);
if (messageId) { if (messageId) {
await adapter.editMessage(msg.chatId, messageId, streamText); await adapter.editMessage(msg.chatId, messageId, prefixedStream);
} else { } 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; messageId = result.messageId;
sentAnyMessage = true; sentAnyMessage = true;
} }
@@ -818,18 +833,19 @@ export class LettaBot implements AgentSession {
// Send final response // Send final response
if (response.trim()) { if (response.trim()) {
const prefixedFinal = this.prefixResponse(response);
try { try {
if (messageId) { if (messageId) {
await adapter.editMessage(msg.chatId, messageId, response); await adapter.editMessage(msg.chatId, messageId, prefixedFinal);
} else { } 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; sentAnyMessage = true;
this.store.resetRecoveryAttempts(); this.store.resetRecoveryAttempts();
} catch { } catch {
// Edit failed -- send as new message so user isn't left with truncated text // Edit failed -- send as new message so user isn't left with truncated text
try { 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; sentAnyMessage = true;
this.store.resetRecoveryAttempts(); this.store.resetRecoveryAttempts();
} catch (retryError) { } catch (retryError) {
@@ -982,7 +998,7 @@ export class LettaBot implements AgentSession {
} }
if (options.text) { 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; return result.messageId;
} }

View File

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

View File

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