feat: add disabled conversation mode (#453)
This commit is contained in:
@@ -56,7 +56,7 @@ agent:
|
|||||||
|
|
||||||
# Conversation routing (optional)
|
# Conversation routing (optional)
|
||||||
conversations:
|
conversations:
|
||||||
mode: shared # "shared" | "per-channel" | "per-chat"
|
mode: shared # "disabled" | "shared" | "per-channel" | "per-chat"
|
||||||
heartbeat: last-active # "dedicated" | "last-active" | "<channel>"
|
heartbeat: last-active # "dedicated" | "last-active" | "<channel>"
|
||||||
|
|
||||||
# Channel configurations
|
# Channel configurations
|
||||||
@@ -268,7 +268,7 @@ Conversation routing controls which incoming messages share a Letta conversation
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
conversations:
|
conversations:
|
||||||
mode: shared # "shared" | "per-channel" | "per-chat"
|
mode: shared # "disabled" | "shared" | "per-channel" | "per-chat"
|
||||||
heartbeat: last-active # "dedicated" | "last-active" | "<channel>"
|
heartbeat: last-active # "dedicated" | "last-active" | "<channel>"
|
||||||
maxSessions: 10 # per-chat only: max concurrent sessions (LRU eviction)
|
maxSessions: 10 # per-chat only: max concurrent sessions (LRU eviction)
|
||||||
perChannel:
|
perChannel:
|
||||||
@@ -279,6 +279,7 @@ conversations:
|
|||||||
|
|
||||||
| Mode | Key | Description |
|
| Mode | Key | Description |
|
||||||
|------|-----|-------------|
|
|------|-----|-------------|
|
||||||
|
| `disabled` | `"default"` | Always uses the agent's built-in default conversation. No new conversations are created. |
|
||||||
| `shared` (default) | `"shared"` | One conversation across all channels and all chats |
|
| `shared` (default) | `"shared"` | One conversation across all channels and all chats |
|
||||||
| `per-channel` | `"telegram"`, `"discord"`, etc. | One conversation per channel adapter. All Telegram groups share one conversation, all Discord channels share another. |
|
| `per-channel` | `"telegram"`, `"discord"`, etc. | One conversation per channel adapter. All Telegram groups share one conversation, all Discord channels share another. |
|
||||||
| `per-chat` | `"telegram:12345"` | One conversation per unique chat within each channel. Every DM and group gets its own isolated message history. |
|
| `per-chat` | `"telegram:12345"` | One conversation per unique chat within each channel. Every DM and group gets its own isolated message history. |
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export interface AgentConfig {
|
|||||||
};
|
};
|
||||||
/** Conversation routing */
|
/** Conversation routing */
|
||||||
conversations?: {
|
conversations?: {
|
||||||
mode?: 'shared' | 'per-channel' | 'per-chat'; // Default: shared (single conversation across all channels)
|
mode?: 'disabled' | 'shared' | 'per-channel' | 'per-chat'; // Default: shared (single conversation across all channels)
|
||||||
heartbeat?: string; // "dedicated" | "last-active" | "<channel>" (default: last-active)
|
heartbeat?: string; // "dedicated" | "last-active" | "<channel>" (default: last-active)
|
||||||
perChannel?: string[]; // Channels that should always have their own conversation
|
perChannel?: string[]; // Channels that should always have their own conversation
|
||||||
maxSessions?: number; // Max concurrent sessions in per-chat mode (default: 10, LRU eviction)
|
maxSessions?: number; // Max concurrent sessions in per-chat mode (default: 10, LRU eviction)
|
||||||
@@ -154,7 +154,7 @@ export interface LettaBotConfig {
|
|||||||
|
|
||||||
// Conversation routing
|
// Conversation routing
|
||||||
conversations?: {
|
conversations?: {
|
||||||
mode?: 'shared' | 'per-channel' | 'per-chat'; // Default: shared (single conversation across all channels)
|
mode?: 'disabled' | 'shared' | 'per-channel' | 'per-chat'; // Default: shared (single conversation across all channels)
|
||||||
heartbeat?: string; // "dedicated" | "last-active" | "<channel>" (default: last-active)
|
heartbeat?: string; // "dedicated" | "last-active" | "<channel>" (default: last-active)
|
||||||
perChannel?: string[]; // Channels that should always have their own conversation
|
perChannel?: string[]; // Channels that should always have their own conversation
|
||||||
maxSessions?: number; // Max concurrent sessions in per-chat mode (default: 10, LRU eviction)
|
maxSessions?: number; // Max concurrent sessions in per-chat mode (default: 10, LRU eviction)
|
||||||
|
|||||||
@@ -138,6 +138,7 @@ export function resolveConversationKey(
|
|||||||
conversationOverrides: Set<string>,
|
conversationOverrides: Set<string>,
|
||||||
chatId?: string,
|
chatId?: string,
|
||||||
): string {
|
): string {
|
||||||
|
if (conversationMode === 'disabled') return 'default';
|
||||||
const normalized = channel.toLowerCase();
|
const normalized = channel.toLowerCase();
|
||||||
if (conversationMode === 'per-chat' && chatId) return `${normalized}:${chatId}`;
|
if (conversationMode === 'per-chat' && chatId) return `${normalized}:${chatId}`;
|
||||||
if (conversationMode === 'per-channel') return normalized;
|
if (conversationMode === 'per-channel') return normalized;
|
||||||
@@ -158,6 +159,7 @@ export function resolveHeartbeatConversationKey(
|
|||||||
lastActiveChannel?: string,
|
lastActiveChannel?: string,
|
||||||
lastActiveChatId?: string,
|
lastActiveChatId?: string,
|
||||||
): string {
|
): string {
|
||||||
|
if (conversationMode === 'disabled') return 'default';
|
||||||
const hb = heartbeatConversation || 'last-active';
|
const hb = heartbeatConversation || 'last-active';
|
||||||
|
|
||||||
if (conversationMode === 'per-chat') {
|
if (conversationMode === 'per-chat') {
|
||||||
@@ -605,18 +607,25 @@ export class LettaBot implements AgentSession {
|
|||||||
let session: Session;
|
let session: Session;
|
||||||
let sessionAgentId: string | undefined;
|
let sessionAgentId: string | undefined;
|
||||||
|
|
||||||
// In per-channel mode, look up per-key conversation ID.
|
// In disabled mode, always resume the agent's built-in default conversation.
|
||||||
// In shared mode (key === "shared"), use the legacy single conversationId.
|
// Skip store lookup entirely -- no conversation ID is persisted.
|
||||||
const convId = key === 'shared'
|
const convId = key === 'default'
|
||||||
? this.store.conversationId
|
? null
|
||||||
: this.store.getConversationId(key);
|
: key === 'shared'
|
||||||
|
? this.store.conversationId
|
||||||
|
: this.store.getConversationId(key);
|
||||||
|
|
||||||
// Propagate per-agent cron store path to CLI subprocesses (lettabot-schedule)
|
// Propagate per-agent cron store path to CLI subprocesses (lettabot-schedule)
|
||||||
if (this.config.cronStorePath) {
|
if (this.config.cronStorePath) {
|
||||||
process.env.CRON_STORE_PATH = this.config.cronStorePath;
|
process.env.CRON_STORE_PATH = this.config.cronStorePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (convId) {
|
if (key === 'default' && this.store.agentId) {
|
||||||
|
process.env.LETTA_AGENT_ID = this.store.agentId;
|
||||||
|
installSkillsToAgent(this.store.agentId, this.config.skills);
|
||||||
|
sessionAgentId = this.store.agentId;
|
||||||
|
session = resumeSession('default', opts);
|
||||||
|
} else if (convId) {
|
||||||
process.env.LETTA_AGENT_ID = this.store.agentId || undefined;
|
process.env.LETTA_AGENT_ID = this.store.agentId || undefined;
|
||||||
if (this.store.agentId) {
|
if (this.store.agentId) {
|
||||||
installSkillsToAgent(this.store.agentId, this.config.skills);
|
installSkillsToAgent(this.store.agentId, this.config.skills);
|
||||||
@@ -648,7 +657,11 @@ export class LettaBot implements AgentSession {
|
|||||||
installSkillsToAgent(newAgentId, this.config.skills);
|
installSkillsToAgent(newAgentId, this.config.skills);
|
||||||
sessionAgentId = newAgentId;
|
sessionAgentId = newAgentId;
|
||||||
|
|
||||||
session = createSession(newAgentId, opts);
|
// In disabled mode, resume the built-in default conversation instead of
|
||||||
|
// creating a new one. Other modes create a fresh conversation per key.
|
||||||
|
session = key === 'default'
|
||||||
|
? resumeSession('default', opts)
|
||||||
|
: createSession(newAgentId, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize eagerly so the subprocess is ready before the first send()
|
// Initialize eagerly so the subprocess is ready before the first send()
|
||||||
@@ -843,9 +856,10 @@ export class LettaBot implements AgentSession {
|
|||||||
const currentBaseUrl = process.env.LETTA_BASE_URL || 'https://api.letta.com';
|
const currentBaseUrl = process.env.LETTA_BASE_URL || 'https://api.letta.com';
|
||||||
this.store.setAgent(session.agentId, currentBaseUrl, session.conversationId || undefined);
|
this.store.setAgent(session.agentId, currentBaseUrl, session.conversationId || undefined);
|
||||||
log.info('Agent ID updated:', session.agentId);
|
log.info('Agent ID updated:', session.agentId);
|
||||||
} else if (session.conversationId && session.conversationId !== 'default') {
|
} else if (session.conversationId && session.conversationId !== 'default' && convKey !== 'default') {
|
||||||
// In per-channel mode, persist per-key. In shared mode, use legacy field.
|
// In per-channel mode, persist per-key. In shared mode, use legacy field.
|
||||||
// Skip saving "default" -- it's an API alias, not a real conversation ID.
|
// Skip saving "default" -- it's an API alias, not a real conversation ID.
|
||||||
|
// In disabled mode (convKey === 'default'), skip -- always use the built-in default.
|
||||||
if (convKey && convKey !== 'shared') {
|
if (convKey && convKey !== 'shared') {
|
||||||
const existing = this.store.getConversationId(convKey);
|
const existing = this.store.getConversationId(convKey);
|
||||||
if (session.conversationId !== existing) {
|
if (session.conversationId !== existing) {
|
||||||
@@ -1125,6 +1139,13 @@ export class LettaBot implements AgentSession {
|
|||||||
// resolveConversationKey returns 'shared' for non-override channels,
|
// resolveConversationKey returns 'shared' for non-override channels,
|
||||||
// the channel id for per-channel, or channel:chatId for per-chat.
|
// the channel id for per-channel, or channel:chatId for per-chat.
|
||||||
const convKey = channelId ? this.resolveConversationKey(channelId, chatId) : 'shared';
|
const convKey = channelId ? this.resolveConversationKey(channelId, chatId) : 'shared';
|
||||||
|
|
||||||
|
// In disabled mode the bot always uses the agent's built-in default
|
||||||
|
// conversation -- there's nothing to reset locally.
|
||||||
|
if (convKey === 'default') {
|
||||||
|
return 'Conversations are disabled -- nothing to reset.';
|
||||||
|
}
|
||||||
|
|
||||||
this.store.clearConversation(convKey);
|
this.store.clearConversation(convKey);
|
||||||
this.store.resetRecoveryAttempts();
|
this.store.resetRecoveryAttempts();
|
||||||
this.invalidateSession(convKey);
|
this.invalidateSession(convKey);
|
||||||
|
|||||||
@@ -74,6 +74,21 @@ describe('resolveConversationKey', () => {
|
|||||||
expect(resolveConversationKey('telegram', 'shared', new Set(), '12345')).toBe('shared');
|
expect(resolveConversationKey('telegram', 'shared', new Set(), '12345')).toBe('shared');
|
||||||
expect(resolveConversationKey('telegram', 'per-channel', new Set(), '12345')).toBe('telegram');
|
expect(resolveConversationKey('telegram', 'per-channel', new Set(), '12345')).toBe('telegram');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- disabled mode ---
|
||||||
|
|
||||||
|
it('returns "default" in disabled mode', () => {
|
||||||
|
expect(resolveConversationKey('telegram', 'disabled', new Set())).toBe('default');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns "default" in disabled mode regardless of chatId', () => {
|
||||||
|
expect(resolveConversationKey('telegram', 'disabled', new Set(), '12345')).toBe('default');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns "default" in disabled mode regardless of overrides', () => {
|
||||||
|
const overrides = new Set(['telegram']);
|
||||||
|
expect(resolveConversationKey('telegram', 'disabled', overrides)).toBe('default');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -148,4 +163,12 @@ describe('resolveHeartbeatConversationKey', () => {
|
|||||||
it('falls back to "shared" in per-chat mode when no last-active target', () => {
|
it('falls back to "shared" in per-chat mode when no last-active target', () => {
|
||||||
expect(resolveHeartbeatConversationKey('per-chat', 'last-active', new Set(), undefined, undefined)).toBe('shared');
|
expect(resolveHeartbeatConversationKey('per-chat', 'last-active', new Set(), undefined, undefined)).toBe('shared');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- disabled mode ---
|
||||||
|
|
||||||
|
it('returns "default" in disabled mode regardless of heartbeat setting', () => {
|
||||||
|
expect(resolveHeartbeatConversationKey('disabled', 'last-active', new Set(), 'telegram')).toBe('default');
|
||||||
|
expect(resolveHeartbeatConversationKey('disabled', 'dedicated', new Set(), 'telegram')).toBe('default');
|
||||||
|
expect(resolveHeartbeatConversationKey('disabled', undefined, new Set())).toBe('default');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ export interface BotConfig {
|
|||||||
cronStorePath?: string; // Resolved cron store path (per-agent in multi-agent mode)
|
cronStorePath?: string; // Resolved cron store path (per-agent in multi-agent mode)
|
||||||
|
|
||||||
// Conversation routing
|
// Conversation routing
|
||||||
conversationMode?: 'shared' | 'per-channel' | 'per-chat'; // Default: shared
|
conversationMode?: 'disabled' | 'shared' | 'per-channel' | 'per-chat'; // Default: shared
|
||||||
heartbeatConversation?: string; // "dedicated" | "last-active" | "<channel>" (default: last-active)
|
heartbeatConversation?: string; // "dedicated" | "last-active" | "<channel>" (default: last-active)
|
||||||
conversationOverrides?: string[]; // Channels that always use their own conversation (shared mode)
|
conversationOverrides?: string[]; // Channels that always use their own conversation (shared mode)
|
||||||
maxSessions?: number; // Max concurrent sessions in per-chat mode (default: 10, LRU eviction)
|
maxSessions?: number; // Max concurrent sessions in per-chat mode (default: 10, LRU eviction)
|
||||||
|
|||||||
Reference in New Issue
Block a user