/** * AnthropicSdkExecutor — direct @anthropic-ai/sdk integration. * Manages conversation history locally (no --resume needed). */ import type { IExecutor, ExecutorConfig, ExecutorStreamEvent } from "./types.ts"; export class AnthropicSdkExecutor implements IExecutor { readonly executorId: string; readonly executorType = "anthropic-sdk" as const; private messages: Array<{ role: "user" | "assistant"; content: string }> = []; private pendingContent: string | null = null; private model: string; private apiKey: string; private systemPrompt: string; private _turnCount = 0; private static readonly MAX_TURNS = 50; private static readonly WARN_TURNS = 40; private pendingSummary: string | null = null; private abortController: AbortController | null = null; private pendingAbort = false; constructor(config: ExecutorConfig) { this.executorId = config.sessionId; this.model = config.model || "claude-sonnet-4-6"; this.apiKey = config.apiKey || process.env.ANTHROPIC_API_KEY || ""; this.systemPrompt = config.systemPrompt || ""; } sendMessage(content: string): void { if (!content?.trim()) { console.warn(`[Executor] sendMessage called with empty content, ignoring`); return; } let msg = content; if (this.pendingSummary) { msg = `\n${this.pendingSummary}\n\n\n${content}`; this.pendingSummary = null; } this.pendingContent = msg; } async *getOutputStream(): AsyncGenerator { if (!this.pendingContent) { yield { type: "error", error: "No message queued" }; return; } const userMsg = this.pendingContent; this.pendingContent = null; // Dynamically import SDK to avoid startup errors if not installed let Anthropic: any; try { const sdk = await import("@anthropic-ai/sdk"); Anthropic = sdk.default || sdk.Anthropic; } catch { yield { type: "error", error: "@anthropic-ai/sdk not installed. Run: npm install @anthropic-ai/sdk" }; return; } const client = new Anthropic({ apiKey: this.apiKey }); this.abortController = new AbortController(); if (this.pendingAbort) { this.abortController.abort(); this.pendingAbort = false; } const signal = this.abortController.signal; // capture before try so catch can read it let assistantText = ""; let userPushed = false; try { this.messages.push({ role: "user", content: userMsg }); userPushed = true; const stream = await client.messages.stream({ model: this.model, max_tokens: 8096, system: this.systemPrompt || undefined, messages: this.messages, }, { signal }); for await (const event of stream) { if (event.type === "content_block_delta" && event.delta?.type === "text_delta") { assistantText += event.delta.text; yield { type: "text_delta", text: event.delta.text }; } } this.messages.push({ role: "assistant", content: assistantText }); yield { type: "result", cost: null, duration: null }; } catch (err) { const isAbort = signal.aborted || (err instanceof Error && err.name === "AbortError"); if (isAbort) { // Keep history consistent: push partial assistant response if any, else rollback user if (assistantText) { this.messages.push({ role: "assistant", content: assistantText }); } else if (userPushed) { this.messages.pop(); } } else { if (userPushed) this.messages.pop(); // rollback so caller can retry cleanly yield { type: "error", error: err instanceof Error ? err.message : String(err) }; } } finally { this.abortController = null; } } interrupt(): void { this.pendingContent = null; this.pendingAbort = true; this.abortController?.abort(); this.abortController = null; } get turnCount(): number { return this._turnCount; } get shouldRotate(): boolean { return this._turnCount >= AnthropicSdkExecutor.MAX_TURNS; } get shouldWarnRotation(): boolean { return this._turnCount >= AnthropicSdkExecutor.WARN_TURNS && !this.shouldRotate; } incrementTurn(): void { this._turnCount++; } async rotate(): Promise { // Generate compact summary from conversation history const historyText = this.messages .map(m => `${m.role}: ${m.content.slice(0, 500)}`) .join("\n"); // Reset for new session this._turnCount = 0; this.pendingSummary = `[Conversation summary — previous ${this.messages.length} messages]\n${historyText.slice(0, 3000)}`; this.messages = []; } }