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:
@@ -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) |
|
||||
|
||||
@@ -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>`.
|
||||
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user