fix: execute directives in background flows, reject partial send-file targeting
Two fixes from PR review: 1. sendToAgent() now parses and executes directives from agent responses. Previously, directives were only executed in processMessage() (foreground), so <send-message> and targeted <send-file> never fired from heartbeats, cron, or webhook contexts. Targeted directives resolve their own adapter; non-targeted directives use source context from the trigger if available. 2. send-file with only one of channel/chat (partial targeting) is now rejected with a warning instead of silently falling back to the triggering chat, which could send to an unintended destination. Written by Cameron ◯ Letta Code "Be conservative in what you send, be liberal in what you accept." -- Jon Postel
This commit is contained in:
@@ -363,6 +363,13 @@ export class LettaBot implements AgentSession {
|
||||
}
|
||||
|
||||
if (directive.type === 'send-file') {
|
||||
// Reject partial targeting: both channel and chat must be set together.
|
||||
// Without this guard, a missing field silently falls back to the triggering chat.
|
||||
if ((directive.channel && !directive.chat) || (!directive.channel && directive.chat)) {
|
||||
log.warn(`Directive send-file skipped: cross-channel targeting requires both channel and chat (got channel=${directive.channel || 'missing'}, chat=${directive.chat || 'missing'})`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Resolve target adapter: use cross-channel targeting if both channel and chat are set,
|
||||
// otherwise fall back to the adapter/chatId that triggered this response.
|
||||
const targetAdapter = (directive.channel && directive.chat)
|
||||
@@ -1908,11 +1915,29 @@ export class LettaBot implements AgentSession {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse and execute directives from the response.
|
||||
// Targeted directives (send-message, cross-channel send-file) work in any context.
|
||||
// Non-targeted directives work when source adapter context is available.
|
||||
let executedDirectives = false;
|
||||
if (response.trim()) {
|
||||
const parsed = parseDirectives(response);
|
||||
if (parsed.directives.length > 0) {
|
||||
const sourceAdapter = (context?.sourceChannel ? this.channels.get(context.sourceChannel) : undefined)
|
||||
?? this.channels.values().next().value;
|
||||
if (sourceAdapter) {
|
||||
executedDirectives = await this.executeDirectives(
|
||||
parsed.directives, sourceAdapter, context?.sourceChatId ?? '',
|
||||
);
|
||||
}
|
||||
response = parsed.cleanText;
|
||||
}
|
||||
}
|
||||
|
||||
if (isSilent && response.trim()) {
|
||||
if (usedMessageCli) {
|
||||
log.info(`Silent mode: agent used lettabot-message CLI, collected ${response.length} chars (not delivered)`);
|
||||
if (usedMessageCli || executedDirectives) {
|
||||
log.info(`Silent mode: agent delivered via ${[usedMessageCli && 'CLI', executedDirectives && 'directives'].filter(Boolean).join(' + ')}, remaining text (${response.length} chars) not delivered`);
|
||||
} else {
|
||||
log.warn(`Silent mode: agent produced ${response.length} chars but did NOT use lettabot-message CLI — response discarded. If this keeps happening, the agent's model may not be following silent mode instructions.`);
|
||||
log.warn(`Silent mode: agent produced ${response.length} chars but did NOT use lettabot-message CLI or directives — response discarded. If this keeps happening, the agent's model may not be following silent mode instructions.`);
|
||||
}
|
||||
}
|
||||
return response;
|
||||
|
||||
@@ -306,6 +306,24 @@ describe('parseDirectives', () => {
|
||||
{ type: 'send-file', path: 'report.pdf', caption: 'Report' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('parses send-file with only channel (no chat) -- stores partial targeting', () => {
|
||||
const result = parseDirectives(
|
||||
'<actions><send-file path="report.pdf" channel="whatsapp" /></actions>',
|
||||
);
|
||||
expect(result.directives).toEqual([
|
||||
{ type: 'send-file', path: 'report.pdf', channel: 'whatsapp' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('parses send-file with only chat (no channel) -- stores partial targeting', () => {
|
||||
const result = parseDirectives(
|
||||
'<actions><send-file path="report.pdf" chat="123" /></actions>',
|
||||
);
|
||||
expect(result.directives).toEqual([
|
||||
{ type: 'send-file', path: 'report.pdf', chat: '123' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('stripActionsBlock', () => {
|
||||
|
||||
Reference in New Issue
Block a user