fix: enrich sendToAgent() error messages with run metadata (#396)
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user