takt/src/infra/claude/executor.ts
2026-02-02 21:52:40 +09:00

186 lines
5.1 KiB
TypeScript

/**
* Claude query executor
*
* Executes Claude queries using the Agent SDK and handles
* response processing and error handling.
*/
import {
query,
AbortError,
type SDKResultMessage,
type SDKAssistantMessage,
} from '@anthropic-ai/claude-agent-sdk';
import { createLogger, getErrorMessage } from '../../shared/utils/index.js';
import {
generateQueryId,
registerQuery,
unregisterQuery,
} from './query-manager.js';
import { sdkMessageToStreamEvent } from './stream-converter.js';
import { SdkOptionsBuilder } from './options-builder.js';
import type {
ClaudeSpawnOptions,
ClaudeResult,
} from './types.js';
const log = createLogger('claude-sdk');
/**
* Executes Claude queries using the Agent SDK.
*
* Handles query lifecycle (register/unregister), streaming,
* assistant text accumulation, and error classification.
*/
export class QueryExecutor {
/**
* Execute a Claude query.
*/
async execute(
prompt: string,
options: ClaudeSpawnOptions,
): Promise<ClaudeResult> {
const queryId = generateQueryId();
log.debug('Executing Claude query via SDK', {
queryId,
cwd: options.cwd,
model: options.model,
hasSystemPrompt: !!options.systemPrompt,
allowedTools: options.allowedTools,
});
const sdkOptions = new SdkOptionsBuilder(options).build();
let sessionId: string | undefined;
let success = false;
let resultContent: string | undefined;
let hasResultMessage = false;
let accumulatedAssistantText = '';
try {
const q = query({ prompt, options: sdkOptions });
registerQuery(queryId, q);
for await (const message of q) {
if ('session_id' in message) {
sessionId = message.session_id;
}
if (options.onStream) {
sdkMessageToStreamEvent(message, options.onStream, true);
}
if (message.type === 'assistant') {
const assistantMsg = message as SDKAssistantMessage;
for (const block of assistantMsg.message.content) {
if (block.type === 'text') {
accumulatedAssistantText += block.text;
}
}
}
if (message.type === 'result') {
hasResultMessage = true;
const resultMsg = message as SDKResultMessage;
if (resultMsg.subtype === 'success') {
resultContent = resultMsg.result;
success = true;
} else {
success = false;
if (resultMsg.errors && resultMsg.errors.length > 0) {
resultContent = resultMsg.errors.join('\n');
}
}
}
}
unregisterQuery(queryId);
const finalContent = resultContent || accumulatedAssistantText;
log.info('Claude query completed', {
queryId,
sessionId,
contentLength: finalContent.length,
success,
hasResultMessage,
});
return {
success,
content: finalContent.trim(),
sessionId,
fullContent: accumulatedAssistantText.trim(),
};
} catch (error) {
unregisterQuery(queryId);
return QueryExecutor.handleQueryError(error, queryId, sessionId, hasResultMessage, success, resultContent);
}
}
/**
* Handle query execution errors.
* Classifies errors (abort, rate limit, auth, timeout) and returns appropriate ClaudeResult.
*/
private static handleQueryError(
error: unknown,
queryId: string,
sessionId: string | undefined,
hasResultMessage: boolean,
success: boolean,
resultContent: string | undefined,
): ClaudeResult {
if (error instanceof AbortError) {
log.info('Claude query was interrupted', { queryId });
return {
success: false,
content: '',
error: 'Query interrupted',
interrupted: true,
};
}
const errorMessage = getErrorMessage(error);
if (hasResultMessage && success) {
log.info('Claude query completed with post-completion error (ignoring)', {
queryId,
sessionId,
error: errorMessage,
});
return {
success: true,
content: (resultContent ?? '').trim(),
sessionId,
};
}
log.error('Claude query failed', { queryId, error: errorMessage });
if (errorMessage.includes('rate_limit') || errorMessage.includes('rate limit')) {
return { success: false, content: '', error: 'Rate limit exceeded. Please try again later.' };
}
if (errorMessage.includes('authentication') || errorMessage.includes('unauthorized')) {
return { success: false, content: '', error: 'Authentication failed. Please check your API credentials.' };
}
if (errorMessage.includes('timeout')) {
return { success: false, content: '', error: 'Request timed out. Please try again.' };
}
return { success: false, content: '', error: errorMessage };
}
}
// ---- Backward-compatible module-level function ----
/** @deprecated Use QueryExecutor.execute() instead */
export async function executeClaudeQuery(
prompt: string,
options: ClaudeSpawnOptions,
): Promise<ClaudeResult> {
return new QueryExecutor().execute(prompt, options);
}