diff --git a/src/core/bot.ts b/src/core/bot.ts index 3312b33..3158d91 100644 --- a/src/core/bot.ts +++ b/src/core/bot.ts @@ -1222,6 +1222,7 @@ export class LettaBot implements AgentSession { const maxRepeatedBashFailures = 3; let lastEventType: string | null = null; let abortedWithMessage = false; + let turnError: string | undefined; const parseAndHandleDirectives = async () => { 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`); session?.abort().catch(() => {}); 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; break; } @@ -1402,6 +1404,7 @@ export class LettaBot implements AgentSession { log.error(`Stopping run after repeated Bash command failures (${repeatedBashFailureCount}) for: ${bashCommand}`); 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.)`; + turnError = `repeated Bash failure abort (${repeatedBashFailureCount}x): ${bashCommand}`; abortedWithMessage = true; break; } @@ -1652,7 +1655,7 @@ export class LettaBot implements AgentSession { events, output: output || response, durationMs: Math.round(performance.now() - t0), - error: lastErrorDetail?.message, + error: turnError ?? lastErrorDetail?.message, }).catch(() => {}); } } diff --git a/src/core/result-guard.test.ts b/src/core/result-guard.test.ts index 015d33e..ce9bb5e 100644 --- a/src/core/result-guard.test.ts +++ b/src/core/result-guard.test.ts @@ -111,6 +111,8 @@ describe('result divergence guard', () => { allowedTools: [], maxToolCalls: 100, }); + const writeTurn = vi.fn(async () => {}); + (bot as any).turnLogger = { write: writeTurn }; const abort = vi.fn(async () => {}); const adapter = { @@ -152,6 +154,10 @@ describe('result divergence guard', () => { expect(abort).toHaveBeenCalled(); 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(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 () => { @@ -160,6 +166,8 @@ describe('result divergence guard', () => { allowedTools: [], maxToolCalls: 1, }); + const writeTurn = vi.fn(async () => {}); + (bot as any).turnLogger = { write: writeTurn }; const adapter = { id: 'mock', @@ -207,6 +215,10 @@ describe('result divergence guard', () => { expect(runSession).toHaveBeenCalledTimes(1); 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(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 () => {