fix: enrich sendToAgent() error messages with run metadata (#396)

This commit is contained in:
Cameron
2026-02-25 10:40:45 -08:00
committed by GitHub
parent cdfb7e1cd5
commit 0d5afd6326
2 changed files with 116 additions and 1 deletions

View File

@@ -2099,18 +2099,37 @@ export class LettaBot implements AgentSession {
try {
let response = '';
let lastErrorDetail: { message: string; stopReason: string; apiError?: Record<string, unknown> } | undefined;
for await (const msg of stream()) {
if (msg.type === 'tool_call') {
this.syncTodoToolCall(msg);
}
if (msg.type === 'error') {
lastErrorDetail = {
message: (msg as any).message || 'unknown',
stopReason: (msg as any).stopReason || 'error',
apiError: (msg as any).apiError,
};
}
if (msg.type === 'assistant') {
response += msg.content || '';
}
if (msg.type === 'result') {
// TODO(letta-code-sdk#31): Remove once SDK handles HITL approvals in bypassPermissions mode.
if (msg.success === false || msg.error) {
// Enrich opaque errors from run metadata (mirrors processMessage logic).
const convId = typeof msg.conversationId === 'string' ? msg.conversationId : undefined;
if (this.store.agentId &&
(!lastErrorDetail || lastErrorDetail.message === 'Agent stopped: error')) {
const enriched = await getLatestRunError(this.store.agentId, convId);
if (enriched) {
lastErrorDetail = { message: enriched.message, stopReason: enriched.stopReason };
}
}
const errMsg = lastErrorDetail?.message || msg.error || 'error';
const errReason = lastErrorDetail?.stopReason || msg.error || 'error';
const detail = typeof msg.result === 'string' ? msg.result.trim() : '';
throw new Error(detail ? `Agent run failed: ${msg.error || 'error'} (${detail})` : `Agent run failed: ${msg.error || 'error'}`);
throw new Error(detail ? `Agent run failed: ${errReason} (${errMsg})` : `Agent run failed: ${errReason} -- ${errMsg}`);
}
break;
}

View File

@@ -11,7 +11,17 @@ vi.mock('@letta-ai/letta-code-sdk', () => ({
imageFromURL: vi.fn(),
}));
vi.mock('../tools/letta-api.js', () => ({
updateAgentName: vi.fn(),
getPendingApprovals: vi.fn(),
rejectApproval: vi.fn(),
cancelRuns: vi.fn(),
recoverOrphanedConversationApproval: vi.fn(),
getLatestRunError: vi.fn().mockResolvedValue(null),
}));
import { createSession, resumeSession } from '@letta-ai/letta-code-sdk';
import { getLatestRunError } from '../tools/letta-api.js';
import { LettaBot } from './bot.js';
function deferred<T>() {
@@ -352,4 +362,90 @@ describe('SDK session contract', () => {
expect(botInternal.processingKeys.has('slack')).toBe(false);
expect(processSpy).toHaveBeenCalledWith('slack');
});
it('enriches opaque error via stream error event in sendToAgent', async () => {
const mockSession = {
initialize: vi.fn(async () => undefined),
send: vi.fn(async (_message: unknown) => undefined),
stream: vi.fn(() =>
(async function* () {
yield { type: 'error', message: 'Bad request to Anthropic: context too long', stopReason: 'llm_api_error' };
yield { type: 'result', success: false, error: 'error' };
})()
),
close: vi.fn(() => undefined),
agentId: 'agent-contract-test',
conversationId: 'conversation-contract-test',
};
vi.mocked(createSession).mockReturnValue(mockSession as never);
const bot = new LettaBot({
workingDir: join(dataDir, 'working'),
allowedTools: [],
});
await expect(bot.sendToAgent('trigger error')).rejects.toThrow(
'Bad request to Anthropic: context too long'
);
});
it('enriches error from run metadata when stream error is opaque', async () => {
const mockSession = {
initialize: vi.fn(async () => undefined),
send: vi.fn(async (_message: unknown) => undefined),
stream: vi.fn(() =>
(async function* () {
yield { type: 'result', success: false, error: 'error', conversationId: 'conv-123' };
})()
),
close: vi.fn(() => undefined),
agentId: 'agent-contract-test',
conversationId: 'conv-123',
};
vi.mocked(createSession).mockReturnValue(mockSession as never);
vi.mocked(getLatestRunError).mockResolvedValueOnce({
message: 'INTERNAL_SERVER_ERROR: Bad request to Anthropic: Error code: 400',
stopReason: 'llm_api_error',
isApprovalError: false,
});
const bot = new LettaBot({
workingDir: join(dataDir, 'working'),
allowedTools: [],
});
await expect(bot.sendToAgent('trigger error')).rejects.toThrow(
'INTERNAL_SERVER_ERROR: Bad request to Anthropic: Error code: 400'
);
expect(getLatestRunError).toHaveBeenCalledWith('agent-contract-test', 'conv-123');
});
it('falls back to msg.error when no enrichment is available', async () => {
const mockSession = {
initialize: vi.fn(async () => undefined),
send: vi.fn(async (_message: unknown) => undefined),
stream: vi.fn(() =>
(async function* () {
yield { type: 'result', success: false, error: 'timeout' };
})()
),
close: vi.fn(() => undefined),
agentId: 'agent-contract-test',
conversationId: 'conversation-contract-test',
};
vi.mocked(createSession).mockReturnValue(mockSession as never);
vi.mocked(getLatestRunError).mockResolvedValueOnce(null);
const bot = new LettaBot({
workingDir: join(dataDir, 'working'),
allowedTools: [],
});
await expect(bot.sendToAgent('trigger error')).rejects.toThrow(
'Agent run failed: timeout'
);
});
});