fix(logging): record structured abort errors in turn logs (#572)

Co-authored-by: Letta Code <noreply@letta.com>
This commit is contained in:
Cameron
2026-03-11 23:45:27 -07:00
committed by GitHub
parent 6ecd78e294
commit a6b1a43ec5
2 changed files with 16 additions and 1 deletions

View File

@@ -1222,6 +1222,7 @@ export class LettaBot implements AgentSession {
const maxRepeatedBashFailures = 3; const maxRepeatedBashFailures = 3;
let lastEventType: string | null = null; let lastEventType: string | null = null;
let abortedWithMessage = false; let abortedWithMessage = false;
let turnError: string | undefined;
const parseAndHandleDirectives = async () => { const parseAndHandleDirectives = async () => {
if (!response.trim()) return; if (!response.trim()) return;
@@ -1346,6 +1347,7 @@ export class LettaBot implements AgentSession {
log.error(`Agent stuck in tool loop (${msgTypeCounts['tool_call']} calls), aborting`); log.error(`Agent stuck in tool loop (${msgTypeCounts['tool_call']} calls), aborting`);
session?.abort().catch(() => {}); session?.abort().catch(() => {});
response = '(Agent got stuck in a tool loop and was stopped. Try sending your message again.)'; response = '(Agent got stuck in a tool loop and was stopped. Try sending your message again.)';
turnError = `tool loop abort after ${msgTypeCounts['tool_call'] || 0} tool calls`;
abortedWithMessage = true; abortedWithMessage = true;
break; break;
} }
@@ -1402,6 +1404,7 @@ export class LettaBot implements AgentSession {
log.error(`Stopping run after repeated Bash command failures (${repeatedBashFailureCount}) for: ${bashCommand}`); log.error(`Stopping run after repeated Bash command failures (${repeatedBashFailureCount}) for: ${bashCommand}`);
session?.abort().catch(() => {}); session?.abort().catch(() => {});
response = `(I stopped after repeated CLI command failures while running: ${bashCommand}. The command path appears mismatched. Please confirm Bluesky CLI commands are available, then resend your request.)`; response = `(I stopped after repeated CLI command failures while running: ${bashCommand}. The command path appears mismatched. Please confirm Bluesky CLI commands are available, then resend your request.)`;
turnError = `repeated Bash failure abort (${repeatedBashFailureCount}x): ${bashCommand}`;
abortedWithMessage = true; abortedWithMessage = true;
break; break;
} }
@@ -1652,7 +1655,7 @@ export class LettaBot implements AgentSession {
events, events,
output: output || response, output: output || response,
durationMs: Math.round(performance.now() - t0), durationMs: Math.round(performance.now() - t0),
error: lastErrorDetail?.message, error: turnError ?? lastErrorDetail?.message,
}).catch(() => {}); }).catch(() => {});
} }
} }

View File

@@ -111,6 +111,8 @@ describe('result divergence guard', () => {
allowedTools: [], allowedTools: [],
maxToolCalls: 100, maxToolCalls: 100,
}); });
const writeTurn = vi.fn(async () => {});
(bot as any).turnLogger = { write: writeTurn };
const abort = vi.fn(async () => {}); const abort = vi.fn(async () => {});
const adapter = { const adapter = {
@@ -152,6 +154,10 @@ describe('result divergence guard', () => {
expect(abort).toHaveBeenCalled(); expect(abort).toHaveBeenCalled();
const sentTexts = adapter.sendMessage.mock.calls.map(([payload]) => payload.text as string); const sentTexts = adapter.sendMessage.mock.calls.map(([payload]) => payload.text as string);
expect(sentTexts.some(text => text.includes('repeated CLI command failures'))).toBe(true); expect(sentTexts.some(text => text.includes('repeated CLI command failures'))).toBe(true);
expect(writeTurn).toHaveBeenCalledTimes(1);
expect(writeTurn).toHaveBeenCalledWith(expect.objectContaining({
error: 'repeated Bash failure abort (3x): lettabot bluesky post --text "hi" --agent Bot',
}));
}); });
it('stops consuming stream and avoids retry after explicit tool-loop abort', async () => { it('stops consuming stream and avoids retry after explicit tool-loop abort', async () => {
@@ -160,6 +166,8 @@ describe('result divergence guard', () => {
allowedTools: [], allowedTools: [],
maxToolCalls: 1, maxToolCalls: 1,
}); });
const writeTurn = vi.fn(async () => {});
(bot as any).turnLogger = { write: writeTurn };
const adapter = { const adapter = {
id: 'mock', id: 'mock',
@@ -207,6 +215,10 @@ describe('result divergence guard', () => {
expect(runSession).toHaveBeenCalledTimes(1); expect(runSession).toHaveBeenCalledTimes(1);
const sentTexts = adapter.sendMessage.mock.calls.map(([payload]) => payload.text); const sentTexts = adapter.sendMessage.mock.calls.map(([payload]) => payload.text);
expect(sentTexts).toEqual(['(Agent got stuck in a tool loop and was stopped. Try sending your message again.)']); expect(sentTexts).toEqual(['(Agent got stuck in a tool loop and was stopped. Try sending your message again.)']);
expect(writeTurn).toHaveBeenCalledTimes(1);
expect(writeTurn).toHaveBeenCalledWith(expect.objectContaining({
error: 'tool loop abort after 1 tool calls',
}));
}); });
it('does not deliver reasoning text from error results as the response', async () => { it('does not deliver reasoning text from error results as the response', async () => {