feat: abort controller for work order processing
Add end-to-end abort support: AbortSignal in @gadget/ai providers, abortWorkOrder socket message, drone AbortController handling, Cancel button and double-Esc in frontend, and aborted turn status display.
This commit is contained in:
parent
c635209201
commit
4780b79148
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,4 +1,5 @@
|
||||
gadget*log
|
||||
gadget-code.*.log
|
||||
gadget-drone.*.log
|
||||
logfetch
|
||||
|
||||
.gadget
|
||||
|
||||
@ -49,7 +49,9 @@ const ChatTurn = memo(function ChatTurn({ turn }: ChatTurnProps) {
|
||||
? "text-green-500"
|
||||
: turn.status === "error"
|
||||
? "text-red-500"
|
||||
: "text-yellow-500"
|
||||
: turn.status === "aborted"
|
||||
? "text-yellow-500"
|
||||
: "text-yellow-500"
|
||||
}
|
||||
>
|
||||
{statusLabel}
|
||||
@ -88,6 +90,17 @@ const ChatTurn = memo(function ChatTurn({ turn }: ChatTurnProps) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Aborted notice */}
|
||||
{turn.status === "aborted" && (
|
||||
<div className="max-w-[80%] ml-0 mb-2">
|
||||
<div className="bg-bg-tertiary border border-border-default rounded p-3">
|
||||
<div className="whitespace-pre-wrap text-sm text-text-secondary">
|
||||
The turn was aborted by you.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* User Prompt */}
|
||||
<div className="max-w-[80%] ml-0 mb-4">
|
||||
<div className="bg-[#4a0000] text-white rounded p-4">
|
||||
|
||||
@ -408,7 +408,7 @@ export interface ChatTurn {
|
||||
provider: string | AiProvider;
|
||||
llm: string;
|
||||
mode: ChatSessionMode;
|
||||
status: "processing" | "finished" | "error";
|
||||
status: "processing" | "finished" | "aborted" | "error";
|
||||
prompts: ChatTurnPrompts;
|
||||
blocks: ChatTurnBlock[];
|
||||
errorMessage?: string;
|
||||
|
||||
@ -42,6 +42,7 @@ export interface ServerToClientEvents {
|
||||
|
||||
export interface ClientToServerEvents {
|
||||
submitPrompt: (content: string) => void;
|
||||
abortWorkOrder: (cb: (success: boolean, message?: string) => void) => void;
|
||||
requestSessionLock: (
|
||||
registration: any,
|
||||
project: any,
|
||||
@ -353,6 +354,16 @@ class SocketClient {
|
||||
}
|
||||
}
|
||||
|
||||
abortWorkOrder(cb?: (success: boolean, message?: string) => void): void {
|
||||
if (this._socket?.connected) {
|
||||
this._socket.emit('abortWorkOrder', (success: boolean, message?: string) => {
|
||||
cb?.(success, message);
|
||||
});
|
||||
} else {
|
||||
cb?.(false, 'Socket not connected');
|
||||
}
|
||||
}
|
||||
|
||||
requestSessionLock(
|
||||
registration: any,
|
||||
project: any,
|
||||
|
||||
@ -50,6 +50,7 @@ export default function ChatSessionView() {
|
||||
const [turns, setTurns] = useState<ChatTurn[]>([]);
|
||||
const [promptInput, setPromptInput] = useState('');
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
const [isAborting, setIsAborting] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
const [sessionLocked, setSessionLocked] = useState(true);
|
||||
@ -81,6 +82,8 @@ export default function ChatSessionView() {
|
||||
const updateRafRef = useRef<number | null>(null);
|
||||
const currentTurnIdRef = useRef<string | null>(null);
|
||||
const streamingStateRef = useRef<Map<string, StreamingState>>(new Map());
|
||||
const escTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const escFlagRef = useRef(false);
|
||||
const subagentStateRef = useRef<Map<string, SubagentStreamState>>(new Map());
|
||||
const sessionRef = useRef<ChatSession | null>(null);
|
||||
const projectRef = useRef<Project | null>(null);
|
||||
@ -534,6 +537,16 @@ export default function ChatSessionView() {
|
||||
scheduleUpdate();
|
||||
}, [mergePendingUpdate, scheduleUpdate]);
|
||||
|
||||
const showToast = useCallback((message: string) => {
|
||||
setToast(message);
|
||||
if (toastTimerRef.current) {
|
||||
clearTimeout(toastTimerRef.current);
|
||||
}
|
||||
toastTimerRef.current = setTimeout(() => {
|
||||
setToast(null);
|
||||
}, 4000);
|
||||
}, []);
|
||||
|
||||
const handleWorkOrderComplete = useCallback((turnId: string, success: boolean, message?: string) => {
|
||||
// Backend has already flushed and persisted all streaming content
|
||||
// Just clean up frontend streaming state and update status
|
||||
@ -551,19 +564,38 @@ export default function ChatSessionView() {
|
||||
subagentStateRef.current.delete(agentId);
|
||||
}
|
||||
|
||||
setTurns(prevTurns =>
|
||||
prevTurns.map(turn =>
|
||||
turn._id === turnId
|
||||
? { ...turn, status: success ? 'finished' : 'error', errorMessage: message && !success ? message : turn.errorMessage }
|
||||
: turn
|
||||
)
|
||||
);
|
||||
if (success && message === 'aborted') {
|
||||
setTurns(prevTurns =>
|
||||
prevTurns.map(turn =>
|
||||
turn._id === turnId
|
||||
? { ...turn, status: 'aborted', errorMessage: 'The turn was aborted by you.' }
|
||||
: turn
|
||||
)
|
||||
);
|
||||
showToast('The turn was aborted by you.');
|
||||
} else {
|
||||
setTurns(prevTurns =>
|
||||
prevTurns.map(turn =>
|
||||
turn._id === turnId
|
||||
? { ...turn, status: success ? 'finished' : 'error', errorMessage: message && !success ? message : turn.errorMessage }
|
||||
: turn
|
||||
)
|
||||
);
|
||||
if (!success) {
|
||||
setError(message || 'Work order failed');
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up abort state
|
||||
if (escTimerRef.current) {
|
||||
clearTimeout(escTimerRef.current);
|
||||
escTimerRef.current = null;
|
||||
}
|
||||
escFlagRef.current = false;
|
||||
setIsAborting(false);
|
||||
setIsProcessing(false);
|
||||
currentTurnIdRef.current = null;
|
||||
if (!success) {
|
||||
setError(message || 'Work order failed');
|
||||
}
|
||||
}, []);
|
||||
}, [showToast]);
|
||||
|
||||
const handleWorkspaceModeChanged = useCallback((mode: string) => {
|
||||
setWorkspaceMode(mode as WorkspaceMode);
|
||||
@ -712,16 +744,6 @@ export default function ChatSessionView() {
|
||||
});
|
||||
}, [updateSubagentBlock]);
|
||||
|
||||
const showToast = useCallback((message: string) => {
|
||||
setToast(message);
|
||||
if (toastTimerRef.current) {
|
||||
clearTimeout(toastTimerRef.current);
|
||||
}
|
||||
toastTimerRef.current = setTimeout(() => {
|
||||
setToast(null);
|
||||
}, 4000);
|
||||
}, []);
|
||||
|
||||
const handleWorkspaceModeChange = async (mode: WorkspaceMode) => {
|
||||
if (!session || !project) return;
|
||||
|
||||
@ -936,6 +958,64 @@ export default function ChatSessionView() {
|
||||
});
|
||||
};
|
||||
|
||||
const handleCancel = useCallback(() => {
|
||||
if (isAborting) return;
|
||||
setIsAborting(true);
|
||||
socketClient.abortWorkOrder((success, message) => {
|
||||
if (success) {
|
||||
showToast('Aborting Agentic Workflow Loop...');
|
||||
} else {
|
||||
showToast(message || 'Failed to abort');
|
||||
setIsAborting(false);
|
||||
}
|
||||
});
|
||||
}, [isAborting, showToast]);
|
||||
|
||||
// Global Esc key handler for abort: first Esc shows prompt, second Esc within 3s aborts
|
||||
useEffect(() => {
|
||||
if (!isProcessing) {
|
||||
if (escTimerRef.current) {
|
||||
clearTimeout(escTimerRef.current);
|
||||
escTimerRef.current = null;
|
||||
}
|
||||
escFlagRef.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key !== 'Escape') return;
|
||||
e.preventDefault();
|
||||
|
||||
if (!escFlagRef.current) {
|
||||
escFlagRef.current = true;
|
||||
showToast('Press Esc again to abort');
|
||||
escTimerRef.current = setTimeout(() => {
|
||||
escFlagRef.current = false;
|
||||
setToast(null);
|
||||
escTimerRef.current = null;
|
||||
}, 3000);
|
||||
} else {
|
||||
if (escTimerRef.current) {
|
||||
clearTimeout(escTimerRef.current);
|
||||
escTimerRef.current = null;
|
||||
}
|
||||
escFlagRef.current = false;
|
||||
setToast(null);
|
||||
handleCancel();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
if (escTimerRef.current) {
|
||||
clearTimeout(escTimerRef.current);
|
||||
escTimerRef.current = null;
|
||||
}
|
||||
escFlagRef.current = false;
|
||||
};
|
||||
}, [isProcessing, showToast, handleCancel]);
|
||||
|
||||
const scrollToBottom = () => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
};
|
||||
@ -1037,13 +1117,24 @@ export default function ChatSessionView() {
|
||||
rows={3}
|
||||
disabled={promptDisabled}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={promptDisabled || !promptInput.trim()}
|
||||
className="px-6 py-2 bg-brand text-white rounded hover:bg-red-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isProcessing ? 'Processing...' : 'Send'}
|
||||
</button>
|
||||
{isProcessing ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCancel}
|
||||
disabled={isAborting}
|
||||
className="px-6 py-2 bg-red-600 text-white rounded hover:bg-red-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isAborting ? 'Aborting...' : 'Cancel'}
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="submit"
|
||||
disabled={promptDisabled || !promptInput.trim()}
|
||||
className="px-6 py-2 bg-brand text-white rounded hover:bg-brand/80 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -19,6 +19,7 @@ import {
|
||||
ChatTurnDocument,
|
||||
WorkspaceMode,
|
||||
SubmitPromptCallback,
|
||||
AbortWorkOrderCallback,
|
||||
} from "@gadget/api";
|
||||
|
||||
import ChatSession from "../models/chat-session.ts";
|
||||
@ -55,6 +56,7 @@ export class CodeSession extends SocketSession {
|
||||
this.onRequestWorkspaceMode.bind(this),
|
||||
);
|
||||
this.socket.on("submitPrompt", this.onSubmitPrompt.bind(this));
|
||||
this.socket.on("abortWorkOrder", this.onAbortWorkOrder.bind(this));
|
||||
this.socket.on("releaseSessionLock", this.onReleaseSessionLock.bind(this));
|
||||
this.socket.on("sessionHeartbeat", this.onSessionHeartbeat.bind(this));
|
||||
|
||||
@ -401,6 +403,23 @@ export class CodeSession extends SocketSession {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the IDE sends an abortWorkOrder event to cancel the
|
||||
* currently running work order. Forwards to the drone.
|
||||
*/
|
||||
onAbortWorkOrder(cb: AbortWorkOrderCallback): void {
|
||||
if (!this.selectedDrone) {
|
||||
return cb(false, "No drone selected");
|
||||
}
|
||||
try {
|
||||
const droneSession = SocketService.getDroneSession(this.selectedDrone);
|
||||
droneSession.socket.emit("abortWorkOrder", cb);
|
||||
} catch (error) {
|
||||
this.log.error("failed to forward abortWorkOrder to drone", { error });
|
||||
cb(false, "Failed to reach drone");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the IDE sends a releaseSessionLock event to release a
|
||||
* previously-acquired session lock on a gadget-drone instance.
|
||||
|
||||
@ -288,9 +288,14 @@ export class DroneSession extends SocketSession {
|
||||
|
||||
const turn = await ChatTurn.findById(turnId);
|
||||
if (turn) {
|
||||
turn.status = success ? ChatTurnStatus.Finished : ChatTurnStatus.Error;
|
||||
if (!success && message) {
|
||||
turn.errorMessage = message;
|
||||
if (success && message === "aborted") {
|
||||
turn.status = ChatTurnStatus.Aborted;
|
||||
turn.errorMessage = "The turn was aborted by you.";
|
||||
} else {
|
||||
turn.status = success ? ChatTurnStatus.Finished : ChatTurnStatus.Error;
|
||||
if (!success && message) {
|
||||
turn.errorMessage = message;
|
||||
}
|
||||
}
|
||||
await turn.save();
|
||||
}
|
||||
|
||||
@ -248,6 +248,10 @@ class GadgetDrone extends GadgetProcess {
|
||||
this.onReleaseSessionLock.bind(this),
|
||||
);
|
||||
this.socket.on("sessionHeartbeat", this.onSessionHeartbeat.bind(this));
|
||||
this.socket.on(
|
||||
"abortWorkOrder",
|
||||
this.onAbortWorkOrder.bind(this),
|
||||
);
|
||||
this.socket.on(
|
||||
"requestTermination",
|
||||
this.onRequestTermination.bind(this),
|
||||
@ -704,6 +708,16 @@ class GadgetDrone extends GadgetProcess {
|
||||
});
|
||||
}
|
||||
|
||||
async onAbortWorkOrder(cb: (success: boolean, message?: string) => void): Promise<void> {
|
||||
this.log.info("abortWorkOrder received from platform", {
|
||||
registrationId: this.registration?._id,
|
||||
isProcessing: this.isProcessingWorkOrder,
|
||||
});
|
||||
|
||||
const aborted = AgentService.abortCurrentWorkOrder();
|
||||
cb(aborted, aborted ? "Abort signaled" : "No active work order to abort");
|
||||
}
|
||||
|
||||
async onRequestTermination(cb: (success: boolean) => void): Promise<void> {
|
||||
this.log.info("requestTermination received from platform", {
|
||||
registrationId: this.registration?._id,
|
||||
|
||||
@ -79,6 +79,7 @@ class AgentService extends GadgetService {
|
||||
private currentWorkOrder: IAgentWorkOrder | null = null;
|
||||
private currentSocket: DroneSocket | null = null;
|
||||
private currentToolCallId: string | null = null;
|
||||
private abortController: AbortController | null = null;
|
||||
|
||||
get name(): string {
|
||||
return "AgentService";
|
||||
@ -143,6 +144,7 @@ class AgentService extends GadgetService {
|
||||
): Promise<void> {
|
||||
this.currentWorkOrder = workOrder;
|
||||
this.currentSocket = socket;
|
||||
this.abortController = new AbortController();
|
||||
|
||||
const { turn } = workOrder;
|
||||
let toolCallCount = 0;
|
||||
@ -204,6 +206,7 @@ class AgentService extends GadgetService {
|
||||
const chatOptions: IAiChatOptions = {
|
||||
context: messages,
|
||||
tools: this.getToolsForMode(turn.mode),
|
||||
signal: this.abortController.signal,
|
||||
};
|
||||
|
||||
let response: IAiChatResponse;
|
||||
@ -302,6 +305,11 @@ class AgentService extends GadgetService {
|
||||
|
||||
socket.emit("workOrderComplete", turn._id, true);
|
||||
} catch (cause) {
|
||||
if (cause instanceof Error && cause.name === "AbortError") {
|
||||
this.log.info("work order aborted by user", { turnId: turn._id });
|
||||
socket.emit("workOrderComplete", turn._id, true, "aborted");
|
||||
return;
|
||||
}
|
||||
const msg = cause instanceof Error ? cause.message : String(cause);
|
||||
this.log.error("agent loop failed, sending workOrderComplete(false)", {
|
||||
turnId: turn._id,
|
||||
@ -309,9 +317,21 @@ class AgentService extends GadgetService {
|
||||
});
|
||||
socket.emit("workOrderComplete", turn._id, false, msg);
|
||||
throw cause;
|
||||
} finally {
|
||||
this.abortController = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals the abort controller for the currently running work order.
|
||||
* Returns true if an abort was signaled, false if there was no active work order.
|
||||
*/
|
||||
abortCurrentWorkOrder(): boolean {
|
||||
if (!this.abortController) return false;
|
||||
this.abortController.abort();
|
||||
return true;
|
||||
}
|
||||
|
||||
buildSessionContext(workOrder: IAgentWorkOrder): IContextChatMessage[] {
|
||||
const session = workOrder.turn.session as IChatSession;
|
||||
if (!session.user) {
|
||||
@ -596,6 +616,7 @@ class AgentService extends GadgetService {
|
||||
const chatOptions: IAiChatOptions = {
|
||||
context: messages,
|
||||
tools,
|
||||
signal: this.abortController?.signal,
|
||||
};
|
||||
|
||||
this.log.info("subagent loop iteration", {
|
||||
|
||||
@ -46,6 +46,7 @@ export interface IAiInferenceStats {
|
||||
export interface IAiGenerateOptions {
|
||||
prompt: string;
|
||||
systemPrompt?: string;
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
|
||||
export interface IAiGenerateResponse {
|
||||
@ -89,6 +90,7 @@ export interface IAiChatOptions {
|
||||
userPrompt?: string;
|
||||
context?: IContextChatMessage[];
|
||||
tools?: IAiTool[];
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
|
||||
export interface IAiChatResponse {
|
||||
|
||||
@ -144,11 +144,16 @@ export class OllamaAiApi extends AiApi {
|
||||
modelId: model.modelId,
|
||||
});
|
||||
|
||||
if (options.signal?.aborted) {
|
||||
throw new DOMException("The operation was aborted", "AbortError");
|
||||
}
|
||||
|
||||
const response = await this.client.generate({
|
||||
model: model.modelId,
|
||||
prompt: options.prompt,
|
||||
system: options.systemPrompt,
|
||||
stream: true,
|
||||
...(options.signal ? { signal: options.signal } : {}),
|
||||
options: {
|
||||
num_ctx: model.params.numCtx,
|
||||
num_predict: model.params.numPredict,
|
||||
@ -161,6 +166,10 @@ export class OllamaAiApi extends AiApi {
|
||||
};
|
||||
let lastChunk;
|
||||
for await (const chunk of response) {
|
||||
if (options.signal?.aborted) {
|
||||
throw new DOMException("The operation was aborted", "AbortError");
|
||||
}
|
||||
|
||||
lastChunk = chunk;
|
||||
|
||||
if (chunk.thinking) {
|
||||
@ -216,6 +225,10 @@ export class OllamaAiApi extends AiApi {
|
||||
modelId: model.modelId,
|
||||
});
|
||||
|
||||
if (options.signal?.aborted) {
|
||||
throw new DOMException("The operation was aborted", "AbortError");
|
||||
}
|
||||
|
||||
const messages: OllamaMessage[] = [];
|
||||
|
||||
if (options.systemPrompt) {
|
||||
@ -268,6 +281,7 @@ export class OllamaAiApi extends AiApi {
|
||||
stream: true,
|
||||
think: model.params.reasoning,
|
||||
tools: ollamaTools,
|
||||
...(options.signal ? { signal: options.signal } : {}),
|
||||
options: {
|
||||
num_ctx: model.params.numCtx,
|
||||
num_predict: model.params.numPredict,
|
||||
@ -280,6 +294,10 @@ export class OllamaAiApi extends AiApi {
|
||||
const toolCalls: IToolCall[] = [];
|
||||
|
||||
for await (const chunk of response) {
|
||||
if (options.signal?.aborted) {
|
||||
throw new DOMException("The operation was aborted", "AbortError");
|
||||
}
|
||||
|
||||
lastChunk = chunk;
|
||||
|
||||
if (chunk.message.thinking) {
|
||||
|
||||
@ -192,6 +192,10 @@ export class OpenAiApi extends AiApi {
|
||||
modelId: model.modelId,
|
||||
});
|
||||
|
||||
if (options.signal?.aborted) {
|
||||
throw new DOMException("The operation was aborted", "AbortError");
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
const response = await this.client.chat.completions.create({
|
||||
model: model.modelId,
|
||||
@ -202,6 +206,7 @@ export class OpenAiApi extends AiApi {
|
||||
{ role: "user" as const, content: options.prompt },
|
||||
],
|
||||
stream: true,
|
||||
...(options.signal ? { signal: options.signal } : {}),
|
||||
...(model.params.maxCompletionTokens
|
||||
? { max_completion_tokens: model.params.maxCompletionTokens }
|
||||
: {}),
|
||||
@ -219,6 +224,10 @@ export class OpenAiApi extends AiApi {
|
||||
let accumulatedThinking = "";
|
||||
|
||||
for await (const chunk of response) {
|
||||
if (options.signal?.aborted) {
|
||||
throw new DOMException("The operation was aborted", "AbortError");
|
||||
}
|
||||
|
||||
const delta = chunk.choices[0]?.delta;
|
||||
if (delta) {
|
||||
if (delta.content) {
|
||||
@ -282,6 +291,7 @@ export class OpenAiApi extends AiApi {
|
||||
messages,
|
||||
tools,
|
||||
streamCallback,
|
||||
options.signal,
|
||||
);
|
||||
|
||||
await this.log.debug("OpenAI chat stream iteration finished", {
|
||||
@ -295,7 +305,7 @@ export class OpenAiApi extends AiApi {
|
||||
});
|
||||
|
||||
if (this.isEmptyIteration(iteration)) {
|
||||
iteration = await this.readNonStreamingChatCompletion(model, messages, tools);
|
||||
iteration = await this.readNonStreamingChatCompletion(model, messages, tools, options.signal);
|
||||
if (streamCallback && iteration.response) {
|
||||
await streamCallback({ type: "response", data: iteration.response });
|
||||
}
|
||||
@ -370,12 +380,18 @@ export class OpenAiApi extends AiApi {
|
||||
messages: ChatCompletionMessageParam[],
|
||||
tools: ChatCompletionTool[],
|
||||
streamCallback?: IAiResponseStreamFn,
|
||||
signal?: AbortSignal,
|
||||
): Promise<OpenAiChatIterationResult> {
|
||||
if (signal?.aborted) {
|
||||
throw new DOMException("The operation was aborted", "AbortError");
|
||||
}
|
||||
|
||||
const response = await this.client.chat.completions.create({
|
||||
model: model.modelId,
|
||||
messages,
|
||||
tools,
|
||||
stream: true,
|
||||
...(signal ? { signal } : {}),
|
||||
...(model.params.maxCompletionTokens
|
||||
? { max_completion_tokens: model.params.maxCompletionTokens }
|
||||
: {}),
|
||||
@ -398,6 +414,10 @@ export class OpenAiApi extends AiApi {
|
||||
const toolCallMap = new Map<number, StreamingToolCallAccumulator>();
|
||||
|
||||
for await (const chunk of response) {
|
||||
if (signal?.aborted) {
|
||||
throw new DOMException("The operation was aborted", "AbortError");
|
||||
}
|
||||
|
||||
chunkCount++;
|
||||
finishReason = chunk.choices[0]?.finish_reason ?? finishReason;
|
||||
const delta = chunk.choices[0]?.delta;
|
||||
@ -439,12 +459,14 @@ export class OpenAiApi extends AiApi {
|
||||
model: IAiModelConfig,
|
||||
messages: ChatCompletionMessageParam[],
|
||||
tools: ChatCompletionTool[],
|
||||
signal?: AbortSignal,
|
||||
): Promise<OpenAiChatIterationResult> {
|
||||
const response = await this.client.chat.completions.create({
|
||||
model: model.modelId,
|
||||
messages,
|
||||
tools,
|
||||
stream: false,
|
||||
...(signal ? { signal } : {}),
|
||||
...(model.params.maxCompletionTokens
|
||||
? { max_completion_tokens: model.params.maxCompletionTokens }
|
||||
: {}),
|
||||
|
||||
@ -80,6 +80,17 @@ export type ReleaseSessionLockMessage = (
|
||||
cb: ReleaseSessionLockCallback,
|
||||
) => void;
|
||||
|
||||
/*
|
||||
* abortWorkOrder
|
||||
*/
|
||||
|
||||
export type AbortWorkOrderCallback = (
|
||||
success: boolean,
|
||||
message?: string,
|
||||
) => void;
|
||||
|
||||
export type AbortWorkOrderMessage = (cb: AbortWorkOrderCallback) => void;
|
||||
|
||||
/*
|
||||
* sessionHeartbeat
|
||||
*/
|
||||
|
||||
@ -17,6 +17,7 @@ import {
|
||||
} from "./drone.ts";
|
||||
import { SessionUpdatedMessage } from "./web.ts";
|
||||
import {
|
||||
AbortWorkOrderMessage,
|
||||
ReleaseSessionLockMessage,
|
||||
RequestSessionLockMessage,
|
||||
RequestWorkspaceModeMessage,
|
||||
@ -50,6 +51,7 @@ export interface ClientToServerEvents {
|
||||
requestSessionLock: RequestSessionLockMessage;
|
||||
requestWorkspaceMode: RequestWorkspaceModeMessage;
|
||||
submitPrompt: SubmitPromptMessage;
|
||||
abortWorkOrder: AbortWorkOrderMessage;
|
||||
releaseSessionLock: ReleaseSessionLockMessage;
|
||||
sessionHeartbeat: SessionHeartbeatMessage;
|
||||
|
||||
@ -111,6 +113,7 @@ export interface ServerToClientEvents {
|
||||
requestWorkspaceMode: RequestWorkspaceModeMessage;
|
||||
releaseSessionLock: ReleaseSessionLockMessage;
|
||||
sessionHeartbeat: SessionHeartbeatMessage;
|
||||
abortWorkOrder: AbortWorkOrderMessage;
|
||||
processWorkOrder: ProcessWorkOrderMessage;
|
||||
crashRecoveryResponse: CrashRecoveryResponseMessage;
|
||||
requestTermination: RequestTerminationMessage;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user