/**
* OpenAIExecutor — OpenAI API (also supports Grok via baseURL override).
*/
import type { IExecutor, ExecutorConfig, ExecutorStreamEvent } from "./types.ts";
export class OpenAIExecutor implements IExecutor {
readonly executorId: string;
readonly executorType: "openai" | "grok";
private messages: Array<{ role: "system" | "user" | "assistant"; content: string }> = [];
private pendingContent: string | null = null;
private model: string;
private apiKey: string;
private baseURL: string | undefined;
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.executorType = config.type as "openai" | "grok";
this.model = config.model || (config.type === "grok" ? "grok-3" : "gpt-4o");
this.apiKey = config.apiKey || (config.type === "grok" ? process.env.GROK_API_KEY || "" : process.env.OPENAI_API_KEY || "");
this.baseURL = config.baseURL || (config.type === "grok" ? "https://api.x.ai/v1" : undefined);
this.systemPrompt = config.systemPrompt || "";
if (this.systemPrompt) {
this.messages.push({ role: "system", content: this.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;
let OpenAI: any;
try {
const sdk = await import("openai");
OpenAI = sdk.default || sdk.OpenAI;
} catch {
yield { type: "error", error: "openai package not installed. Run: npm install openai" };
return;
}
const client = new OpenAI({
apiKey: this.apiKey,
...(this.baseURL ? { baseURL: this.baseURL } : {}),
});
this.abortController = new AbortController();
if (this.pendingAbort) { this.abortController.abort(); this.pendingAbort = false; }
const signal = this.abortController.signal;
let assistantText = "";
let userPushed = false;
try {
this.messages.push({ role: "user", content: userMsg });
userPushed = true;
const stream = await client.chat.completions.create({
model: this.model,
messages: this.messages,
stream: true,
}, { signal });
for await (const chunk of stream) {
const delta = chunk.choices?.[0]?.delta?.content;
if (delta) {
assistantText += delta;
yield { type: "text_delta", text: delta };
}
}
this.messages.push({ role: "assistant", content: assistantText });
yield { type: "result", cost: null, duration: null };
} catch (err) {
// openai v6 throws APIUserAbortError; check signal.aborted as the canonical source
const isAbort = signal.aborted ||
(err instanceof Error && (err.name === "AbortError" || err.constructor?.name === "APIUserAbortError"));
if (isAbort) {
if (assistantText) {
this.messages.push({ role: "assistant", content: assistantText });
} else if (userPushed) {
this.messages.pop();
}
} else {
if (userPushed) this.messages.pop();
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 >= OpenAIExecutor.MAX_TURNS; }
get shouldWarnRotation(): boolean { return this._turnCount >= OpenAIExecutor.WARN_TURNS && !this.shouldRotate; }
incrementTurn(): void { this._turnCount++; }
async rotate(): Promise {
const sysMsg = this.messages.find(m => m.role === "system");
const historyText = this.messages.filter(m => m.role !== "system")
.map(m => `${m.role}: ${m.content.slice(0, 300)}`).join("\n");
this.pendingSummary = historyText.slice(0, 2000);
this.messages = sysMsg ? [sysMsg] : [];
this._turnCount = 0;
}
}