fix: rebind foreground run ID on post-tool-call assistant events (#545)
This commit is contained in:
@@ -1374,16 +1374,33 @@ export class LettaBot implements AgentSession {
|
||||
continue;
|
||||
}
|
||||
} else if (expectedForegroundRunId && eventRunIds.length > 0 && !eventRunIds.includes(expectedForegroundRunId)) {
|
||||
// Strict no-rebind policy: once foreground is selected, never switch.
|
||||
sawCompetingRunEvent = true;
|
||||
filteredRunEventCount++;
|
||||
if (streamMsg.type === 'result') {
|
||||
ignoredNonForegroundResultCount++;
|
||||
log.warn(`Ignoring non-foreground result event (seq=${seq}, key=${convKey}, runIds=${eventRunIds.join(',')}, expected=${expectedForegroundRunId}, source=${expectedForegroundRunSource || 'unknown'})`);
|
||||
// After a tool call the Letta server may assign a new run ID for
|
||||
// the continuation. Rebind on assistant events -- background Task
|
||||
// agents run in separate sessions and don't produce assistant
|
||||
// events in the foreground stream. Other event types (reasoning,
|
||||
// tool_call, result) from different runs are still filtered to
|
||||
// prevent background Task output leaking into user display (#482).
|
||||
if (streamMsg.type === 'assistant') {
|
||||
const newRunId = eventRunIds[0];
|
||||
log.info(`Rebinding foreground run: ${expectedForegroundRunId} -> ${newRunId} (seq=${seq}, key=${convKey}, type=${streamMsg.type})`);
|
||||
expectedForegroundRunId = newRunId;
|
||||
expectedForegroundRunSource = 'assistant';
|
||||
// Flush any buffered display events for the new run.
|
||||
if (bufferedDisplayEvents.length > 0) {
|
||||
await flushBufferedDisplayEventsForRun(newRunId);
|
||||
}
|
||||
// Fall through to normal processing
|
||||
} else {
|
||||
log.trace(`Skipping non-foreground stream event (seq=${seq}, key=${convKey}, type=${streamMsg.type}, runIds=${eventRunIds.join(',')}, expected=${expectedForegroundRunId})`);
|
||||
sawCompetingRunEvent = true;
|
||||
filteredRunEventCount++;
|
||||
if (streamMsg.type === 'result') {
|
||||
ignoredNonForegroundResultCount++;
|
||||
log.warn(`Ignoring non-foreground result event (seq=${seq}, key=${convKey}, runIds=${eventRunIds.join(',')}, expected=${expectedForegroundRunId})`);
|
||||
} else {
|
||||
log.trace(`Skipping non-foreground stream event (seq=${seq}, key=${convKey}, type=${streamMsg.type}, runIds=${eventRunIds.join(',')}, expected=${expectedForegroundRunId})`);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
receivedAnyData = true;
|
||||
|
||||
@@ -159,7 +159,7 @@ describe('result divergence guard', () => {
|
||||
expect(lastSent).toMatch(/\(.*\)/); // Parenthesized system message
|
||||
});
|
||||
|
||||
it('ignores non-foreground result events and waits for the foreground result', async () => {
|
||||
it('rebinds foreground run on post-tool-call assistant events with new run ID (#527)', async () => {
|
||||
const bot = new LettaBot({
|
||||
workingDir: workDir,
|
||||
allowedTools: [],
|
||||
@@ -179,14 +179,15 @@ describe('result divergence guard', () => {
|
||||
sendFile: vi.fn(async () => ({ messageId: 'file-1' })),
|
||||
};
|
||||
|
||||
// Server assigns run-2 after tool call -- both runs are part of the same turn
|
||||
(bot as any).sessionManager.runSession = vi.fn(async () => ({
|
||||
session: { abort: vi.fn(async () => {}) },
|
||||
stream: async function* () {
|
||||
yield { type: 'assistant', content: 'main ', runId: 'run-main' };
|
||||
yield { type: 'assistant', content: 'background', runId: 'run-bg' };
|
||||
yield { type: 'result', success: true, result: 'background final', runIds: ['run-bg'] };
|
||||
yield { type: 'assistant', content: 'reply', runId: 'run-main' };
|
||||
yield { type: 'result', success: true, result: 'main reply', runIds: ['run-main'] };
|
||||
yield { type: 'assistant', content: 'Before tool. ', runId: 'run-1' };
|
||||
yield { type: 'tool_call', toolCallId: 'tc-1', toolName: 'Bash', toolInput: { command: 'echo ok' }, runId: 'run-1' };
|
||||
yield { type: 'tool_result', content: 'ok', isError: false, runId: 'run-1' };
|
||||
yield { type: 'assistant', content: 'After tool.', runId: 'run-2' };
|
||||
yield { type: 'result', success: true, result: 'Before tool. After tool.', runIds: ['run-2'] };
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -194,14 +195,15 @@ describe('result divergence guard', () => {
|
||||
channel: 'discord',
|
||||
chatId: 'chat-1',
|
||||
userId: 'user-1',
|
||||
text: 'hello',
|
||||
text: 'run a command',
|
||||
timestamp: new Date(),
|
||||
};
|
||||
|
||||
await (bot as any).processMessage(msg, adapter);
|
||||
|
||||
const sentTexts = adapter.sendMessage.mock.calls.map(([payload]) => payload.text);
|
||||
expect(sentTexts).toEqual(['main reply']);
|
||||
// Pre-tool and post-tool text are separate messages (finalized on type change)
|
||||
expect(sentTexts).toEqual(['Before tool. ', 'After tool.']);
|
||||
});
|
||||
|
||||
it('buffers pre-foreground run-scoped display events and drops non-foreground buffers', async () => {
|
||||
|
||||
Reference in New Issue
Block a user