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