From df18cba565751faa49d5fe8d1a9d342bb77f0ca6 Mon Sep 17 00:00:00 2001 From: Cameron Date: Tue, 10 Feb 2026 12:08:45 -0800 Subject: [PATCH] feat: configurable displayName prefix for agent messages in group chats (#255) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- docs/configuration.md | 3 +++ lettabot.example.yaml | 1 + src/config/normalize.test.ts | 42 ++++++++++++++++++++++++++++++++++++ src/config/types.ts | 4 ++++ src/core/bot.ts | 32 ++++++++++++++++++++------- src/core/types.ts | 3 +++ src/main.ts | 1 + 7 files changed, 78 insertions(+), 8 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index c071b23..f4d836b 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -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 ` 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) | diff --git a/lettabot.example.yaml b/lettabot.example.yaml index 98cb94a..7888ddb 100644 --- a/lettabot.example.yaml +++ b/lettabot.example.yaml @@ -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 `. diff --git a/src/config/normalize.test.ts b/src/config/normalize.test.ts index 7880986..d79acee 100644 --- a/src/config/normalize.test.ts +++ b/src/config/normalize.test.ts @@ -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'); + }); }); diff --git a/src/config/types.ts b/src/config/types.ts index d617d31..087bc99 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -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, diff --git a/src/core/bot.ts b/src/core/bot.ts index 2e65fc5..b8ed142 100644 --- a/src/core/bot.ts +++ b/src/core/bot.ts @@ -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; } diff --git a/src/core/types.ts b/src/core/types.ts index cae871f..5616ce7 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -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; diff --git a/src/main.ts b/src/main.ts index 2f122ed..d1c0677 100644 --- a/src/main.ts +++ b/src/main.ts @@ -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,