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:
Rob Colbert 2026-05-12 12:25:17 -04:00
parent c635209201
commit 4780b79148
14 changed files with 266 additions and 35 deletions

3
.gitignore vendored
View File

@ -1,4 +1,5 @@
gadget*log gadget-code.*.log
gadget-drone.*.log
logfetch logfetch
.gadget .gadget

View File

@ -49,7 +49,9 @@ const ChatTurn = memo(function ChatTurn({ turn }: ChatTurnProps) {
? "text-green-500" ? "text-green-500"
: turn.status === "error" : turn.status === "error"
? "text-red-500" ? "text-red-500"
: "text-yellow-500" : turn.status === "aborted"
? "text-yellow-500"
: "text-yellow-500"
} }
> >
{statusLabel} {statusLabel}
@ -88,6 +90,17 @@ const ChatTurn = memo(function ChatTurn({ turn }: ChatTurnProps) {
</div> </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 */} {/* User Prompt */}
<div className="max-w-[80%] ml-0 mb-4"> <div className="max-w-[80%] ml-0 mb-4">
<div className="bg-[#4a0000] text-white rounded p-4"> <div className="bg-[#4a0000] text-white rounded p-4">

View File

@ -408,7 +408,7 @@ export interface ChatTurn {
provider: string | AiProvider; provider: string | AiProvider;
llm: string; llm: string;
mode: ChatSessionMode; mode: ChatSessionMode;
status: "processing" | "finished" | "error"; status: "processing" | "finished" | "aborted" | "error";
prompts: ChatTurnPrompts; prompts: ChatTurnPrompts;
blocks: ChatTurnBlock[]; blocks: ChatTurnBlock[];
errorMessage?: string; errorMessage?: string;

View File

@ -42,6 +42,7 @@ export interface ServerToClientEvents {
export interface ClientToServerEvents { export interface ClientToServerEvents {
submitPrompt: (content: string) => void; submitPrompt: (content: string) => void;
abortWorkOrder: (cb: (success: boolean, message?: string) => void) => void;
requestSessionLock: ( requestSessionLock: (
registration: any, registration: any,
project: 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( requestSessionLock(
registration: any, registration: any,
project: any, project: any,

View File

@ -50,6 +50,7 @@ export default function ChatSessionView() {
const [turns, setTurns] = useState<ChatTurn[]>([]); const [turns, setTurns] = useState<ChatTurn[]>([]);
const [promptInput, setPromptInput] = useState(''); const [promptInput, setPromptInput] = useState('');
const [isProcessing, setIsProcessing] = useState(false); const [isProcessing, setIsProcessing] = useState(false);
const [isAborting, setIsAborting] = useState(false);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState(''); const [error, setError] = useState('');
const [sessionLocked, setSessionLocked] = useState(true); const [sessionLocked, setSessionLocked] = useState(true);
@ -81,6 +82,8 @@ export default function ChatSessionView() {
const updateRafRef = useRef<number | null>(null); const updateRafRef = useRef<number | null>(null);
const currentTurnIdRef = useRef<string | null>(null); const currentTurnIdRef = useRef<string | null>(null);
const streamingStateRef = useRef<Map<string, StreamingState>>(new Map()); 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 subagentStateRef = useRef<Map<string, SubagentStreamState>>(new Map());
const sessionRef = useRef<ChatSession | null>(null); const sessionRef = useRef<ChatSession | null>(null);
const projectRef = useRef<Project | null>(null); const projectRef = useRef<Project | null>(null);
@ -534,6 +537,16 @@ export default function ChatSessionView() {
scheduleUpdate(); scheduleUpdate();
}, [mergePendingUpdate, 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) => { const handleWorkOrderComplete = useCallback((turnId: string, success: boolean, message?: string) => {
// Backend has already flushed and persisted all streaming content // Backend has already flushed and persisted all streaming content
// Just clean up frontend streaming state and update status // Just clean up frontend streaming state and update status
@ -551,19 +564,38 @@ export default function ChatSessionView() {
subagentStateRef.current.delete(agentId); subagentStateRef.current.delete(agentId);
} }
setTurns(prevTurns => if (success && message === 'aborted') {
prevTurns.map(turn => setTurns(prevTurns =>
turn._id === turnId prevTurns.map(turn =>
? { ...turn, status: success ? 'finished' : 'error', errorMessage: message && !success ? message : turn.errorMessage } turn._id === turnId
: turn ? { ...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); setIsProcessing(false);
currentTurnIdRef.current = null; currentTurnIdRef.current = null;
if (!success) { }, [showToast]);
setError(message || 'Work order failed');
}
}, []);
const handleWorkspaceModeChanged = useCallback((mode: string) => { const handleWorkspaceModeChanged = useCallback((mode: string) => {
setWorkspaceMode(mode as WorkspaceMode); setWorkspaceMode(mode as WorkspaceMode);
@ -712,16 +744,6 @@ export default function ChatSessionView() {
}); });
}, [updateSubagentBlock]); }, [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) => { const handleWorkspaceModeChange = async (mode: WorkspaceMode) => {
if (!session || !project) return; 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 = () => { const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}; };
@ -1037,13 +1117,24 @@ export default function ChatSessionView() {
rows={3} rows={3}
disabled={promptDisabled} disabled={promptDisabled}
/> />
<button {isProcessing ? (
type="submit" <button
disabled={promptDisabled || !promptInput.trim()} type="button"
className="px-6 py-2 bg-brand text-white rounded hover:bg-red-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed" onClick={handleCancel}
> disabled={isAborting}
{isProcessing ? 'Processing...' : 'Send'} className="px-6 py-2 bg-red-600 text-white rounded hover:bg-red-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
</button> >
{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> </form>
</div> </div>
</div> </div>

View File

@ -19,6 +19,7 @@ import {
ChatTurnDocument, ChatTurnDocument,
WorkspaceMode, WorkspaceMode,
SubmitPromptCallback, SubmitPromptCallback,
AbortWorkOrderCallback,
} from "@gadget/api"; } from "@gadget/api";
import ChatSession from "../models/chat-session.ts"; import ChatSession from "../models/chat-session.ts";
@ -55,6 +56,7 @@ export class CodeSession extends SocketSession {
this.onRequestWorkspaceMode.bind(this), this.onRequestWorkspaceMode.bind(this),
); );
this.socket.on("submitPrompt", this.onSubmitPrompt.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("releaseSessionLock", this.onReleaseSessionLock.bind(this));
this.socket.on("sessionHeartbeat", this.onSessionHeartbeat.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 * Called when the IDE sends a releaseSessionLock event to release a
* previously-acquired session lock on a gadget-drone instance. * previously-acquired session lock on a gadget-drone instance.

View File

@ -288,9 +288,14 @@ export class DroneSession extends SocketSession {
const turn = await ChatTurn.findById(turnId); const turn = await ChatTurn.findById(turnId);
if (turn) { if (turn) {
turn.status = success ? ChatTurnStatus.Finished : ChatTurnStatus.Error; if (success && message === "aborted") {
if (!success && message) { turn.status = ChatTurnStatus.Aborted;
turn.errorMessage = message; 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(); await turn.save();
} }

View File

@ -248,6 +248,10 @@ class GadgetDrone extends GadgetProcess {
this.onReleaseSessionLock.bind(this), this.onReleaseSessionLock.bind(this),
); );
this.socket.on("sessionHeartbeat", this.onSessionHeartbeat.bind(this)); this.socket.on("sessionHeartbeat", this.onSessionHeartbeat.bind(this));
this.socket.on(
"abortWorkOrder",
this.onAbortWorkOrder.bind(this),
);
this.socket.on( this.socket.on(
"requestTermination", "requestTermination",
this.onRequestTermination.bind(this), 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> { async onRequestTermination(cb: (success: boolean) => void): Promise<void> {
this.log.info("requestTermination received from platform", { this.log.info("requestTermination received from platform", {
registrationId: this.registration?._id, registrationId: this.registration?._id,

View File

@ -79,6 +79,7 @@ class AgentService extends GadgetService {
private currentWorkOrder: IAgentWorkOrder | null = null; private currentWorkOrder: IAgentWorkOrder | null = null;
private currentSocket: DroneSocket | null = null; private currentSocket: DroneSocket | null = null;
private currentToolCallId: string | null = null; private currentToolCallId: string | null = null;
private abortController: AbortController | null = null;
get name(): string { get name(): string {
return "AgentService"; return "AgentService";
@ -143,6 +144,7 @@ class AgentService extends GadgetService {
): Promise<void> { ): Promise<void> {
this.currentWorkOrder = workOrder; this.currentWorkOrder = workOrder;
this.currentSocket = socket; this.currentSocket = socket;
this.abortController = new AbortController();
const { turn } = workOrder; const { turn } = workOrder;
let toolCallCount = 0; let toolCallCount = 0;
@ -204,6 +206,7 @@ class AgentService extends GadgetService {
const chatOptions: IAiChatOptions = { const chatOptions: IAiChatOptions = {
context: messages, context: messages,
tools: this.getToolsForMode(turn.mode), tools: this.getToolsForMode(turn.mode),
signal: this.abortController.signal,
}; };
let response: IAiChatResponse; let response: IAiChatResponse;
@ -302,6 +305,11 @@ class AgentService extends GadgetService {
socket.emit("workOrderComplete", turn._id, true); socket.emit("workOrderComplete", turn._id, true);
} catch (cause) { } 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); const msg = cause instanceof Error ? cause.message : String(cause);
this.log.error("agent loop failed, sending workOrderComplete(false)", { this.log.error("agent loop failed, sending workOrderComplete(false)", {
turnId: turn._id, turnId: turn._id,
@ -309,9 +317,21 @@ class AgentService extends GadgetService {
}); });
socket.emit("workOrderComplete", turn._id, false, msg); socket.emit("workOrderComplete", turn._id, false, msg);
throw cause; 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[] { buildSessionContext(workOrder: IAgentWorkOrder): IContextChatMessage[] {
const session = workOrder.turn.session as IChatSession; const session = workOrder.turn.session as IChatSession;
if (!session.user) { if (!session.user) {
@ -596,6 +616,7 @@ class AgentService extends GadgetService {
const chatOptions: IAiChatOptions = { const chatOptions: IAiChatOptions = {
context: messages, context: messages,
tools, tools,
signal: this.abortController?.signal,
}; };
this.log.info("subagent loop iteration", { this.log.info("subagent loop iteration", {

View File

@ -46,6 +46,7 @@ export interface IAiInferenceStats {
export interface IAiGenerateOptions { export interface IAiGenerateOptions {
prompt: string; prompt: string;
systemPrompt?: string; systemPrompt?: string;
signal?: AbortSignal;
} }
export interface IAiGenerateResponse { export interface IAiGenerateResponse {
@ -89,6 +90,7 @@ export interface IAiChatOptions {
userPrompt?: string; userPrompt?: string;
context?: IContextChatMessage[]; context?: IContextChatMessage[];
tools?: IAiTool[]; tools?: IAiTool[];
signal?: AbortSignal;
} }
export interface IAiChatResponse { export interface IAiChatResponse {

View File

@ -144,11 +144,16 @@ export class OllamaAiApi extends AiApi {
modelId: model.modelId, modelId: model.modelId,
}); });
if (options.signal?.aborted) {
throw new DOMException("The operation was aborted", "AbortError");
}
const response = await this.client.generate({ const response = await this.client.generate({
model: model.modelId, model: model.modelId,
prompt: options.prompt, prompt: options.prompt,
system: options.systemPrompt, system: options.systemPrompt,
stream: true, stream: true,
...(options.signal ? { signal: options.signal } : {}),
options: { options: {
num_ctx: model.params.numCtx, num_ctx: model.params.numCtx,
num_predict: model.params.numPredict, num_predict: model.params.numPredict,
@ -161,6 +166,10 @@ export class OllamaAiApi extends AiApi {
}; };
let lastChunk; let lastChunk;
for await (const chunk of response) { for await (const chunk of response) {
if (options.signal?.aborted) {
throw new DOMException("The operation was aborted", "AbortError");
}
lastChunk = chunk; lastChunk = chunk;
if (chunk.thinking) { if (chunk.thinking) {
@ -216,6 +225,10 @@ export class OllamaAiApi extends AiApi {
modelId: model.modelId, modelId: model.modelId,
}); });
if (options.signal?.aborted) {
throw new DOMException("The operation was aborted", "AbortError");
}
const messages: OllamaMessage[] = []; const messages: OllamaMessage[] = [];
if (options.systemPrompt) { if (options.systemPrompt) {
@ -268,6 +281,7 @@ export class OllamaAiApi extends AiApi {
stream: true, stream: true,
think: model.params.reasoning, think: model.params.reasoning,
tools: ollamaTools, tools: ollamaTools,
...(options.signal ? { signal: options.signal } : {}),
options: { options: {
num_ctx: model.params.numCtx, num_ctx: model.params.numCtx,
num_predict: model.params.numPredict, num_predict: model.params.numPredict,
@ -280,6 +294,10 @@ export class OllamaAiApi extends AiApi {
const toolCalls: IToolCall[] = []; const toolCalls: IToolCall[] = [];
for await (const chunk of response) { for await (const chunk of response) {
if (options.signal?.aborted) {
throw new DOMException("The operation was aborted", "AbortError");
}
lastChunk = chunk; lastChunk = chunk;
if (chunk.message.thinking) { if (chunk.message.thinking) {

View File

@ -192,6 +192,10 @@ export class OpenAiApi extends AiApi {
modelId: model.modelId, modelId: model.modelId,
}); });
if (options.signal?.aborted) {
throw new DOMException("The operation was aborted", "AbortError");
}
const startTime = Date.now(); const startTime = Date.now();
const response = await this.client.chat.completions.create({ const response = await this.client.chat.completions.create({
model: model.modelId, model: model.modelId,
@ -202,6 +206,7 @@ export class OpenAiApi extends AiApi {
{ role: "user" as const, content: options.prompt }, { role: "user" as const, content: options.prompt },
], ],
stream: true, stream: true,
...(options.signal ? { signal: options.signal } : {}),
...(model.params.maxCompletionTokens ...(model.params.maxCompletionTokens
? { max_completion_tokens: model.params.maxCompletionTokens } ? { max_completion_tokens: model.params.maxCompletionTokens }
: {}), : {}),
@ -219,6 +224,10 @@ export class OpenAiApi extends AiApi {
let accumulatedThinking = ""; let accumulatedThinking = "";
for await (const chunk of response) { for await (const chunk of response) {
if (options.signal?.aborted) {
throw new DOMException("The operation was aborted", "AbortError");
}
const delta = chunk.choices[0]?.delta; const delta = chunk.choices[0]?.delta;
if (delta) { if (delta) {
if (delta.content) { if (delta.content) {
@ -282,6 +291,7 @@ export class OpenAiApi extends AiApi {
messages, messages,
tools, tools,
streamCallback, streamCallback,
options.signal,
); );
await this.log.debug("OpenAI chat stream iteration finished", { await this.log.debug("OpenAI chat stream iteration finished", {
@ -295,7 +305,7 @@ export class OpenAiApi extends AiApi {
}); });
if (this.isEmptyIteration(iteration)) { if (this.isEmptyIteration(iteration)) {
iteration = await this.readNonStreamingChatCompletion(model, messages, tools); iteration = await this.readNonStreamingChatCompletion(model, messages, tools, options.signal);
if (streamCallback && iteration.response) { if (streamCallback && iteration.response) {
await streamCallback({ type: "response", data: iteration.response }); await streamCallback({ type: "response", data: iteration.response });
} }
@ -370,12 +380,18 @@ export class OpenAiApi extends AiApi {
messages: ChatCompletionMessageParam[], messages: ChatCompletionMessageParam[],
tools: ChatCompletionTool[], tools: ChatCompletionTool[],
streamCallback?: IAiResponseStreamFn, streamCallback?: IAiResponseStreamFn,
signal?: AbortSignal,
): Promise<OpenAiChatIterationResult> { ): Promise<OpenAiChatIterationResult> {
if (signal?.aborted) {
throw new DOMException("The operation was aborted", "AbortError");
}
const response = await this.client.chat.completions.create({ const response = await this.client.chat.completions.create({
model: model.modelId, model: model.modelId,
messages, messages,
tools, tools,
stream: true, stream: true,
...(signal ? { signal } : {}),
...(model.params.maxCompletionTokens ...(model.params.maxCompletionTokens
? { max_completion_tokens: model.params.maxCompletionTokens } ? { max_completion_tokens: model.params.maxCompletionTokens }
: {}), : {}),
@ -398,6 +414,10 @@ export class OpenAiApi extends AiApi {
const toolCallMap = new Map<number, StreamingToolCallAccumulator>(); const toolCallMap = new Map<number, StreamingToolCallAccumulator>();
for await (const chunk of response) { for await (const chunk of response) {
if (signal?.aborted) {
throw new DOMException("The operation was aborted", "AbortError");
}
chunkCount++; chunkCount++;
finishReason = chunk.choices[0]?.finish_reason ?? finishReason; finishReason = chunk.choices[0]?.finish_reason ?? finishReason;
const delta = chunk.choices[0]?.delta; const delta = chunk.choices[0]?.delta;
@ -439,12 +459,14 @@ export class OpenAiApi extends AiApi {
model: IAiModelConfig, model: IAiModelConfig,
messages: ChatCompletionMessageParam[], messages: ChatCompletionMessageParam[],
tools: ChatCompletionTool[], tools: ChatCompletionTool[],
signal?: AbortSignal,
): Promise<OpenAiChatIterationResult> { ): Promise<OpenAiChatIterationResult> {
const response = await this.client.chat.completions.create({ const response = await this.client.chat.completions.create({
model: model.modelId, model: model.modelId,
messages, messages,
tools, tools,
stream: false, stream: false,
...(signal ? { signal } : {}),
...(model.params.maxCompletionTokens ...(model.params.maxCompletionTokens
? { max_completion_tokens: model.params.maxCompletionTokens } ? { max_completion_tokens: model.params.maxCompletionTokens }
: {}), : {}),

View File

@ -80,6 +80,17 @@ export type ReleaseSessionLockMessage = (
cb: ReleaseSessionLockCallback, cb: ReleaseSessionLockCallback,
) => void; ) => void;
/*
* abortWorkOrder
*/
export type AbortWorkOrderCallback = (
success: boolean,
message?: string,
) => void;
export type AbortWorkOrderMessage = (cb: AbortWorkOrderCallback) => void;
/* /*
* sessionHeartbeat * sessionHeartbeat
*/ */

View File

@ -17,6 +17,7 @@ import {
} from "./drone.ts"; } from "./drone.ts";
import { SessionUpdatedMessage } from "./web.ts"; import { SessionUpdatedMessage } from "./web.ts";
import { import {
AbortWorkOrderMessage,
ReleaseSessionLockMessage, ReleaseSessionLockMessage,
RequestSessionLockMessage, RequestSessionLockMessage,
RequestWorkspaceModeMessage, RequestWorkspaceModeMessage,
@ -50,6 +51,7 @@ export interface ClientToServerEvents {
requestSessionLock: RequestSessionLockMessage; requestSessionLock: RequestSessionLockMessage;
requestWorkspaceMode: RequestWorkspaceModeMessage; requestWorkspaceMode: RequestWorkspaceModeMessage;
submitPrompt: SubmitPromptMessage; submitPrompt: SubmitPromptMessage;
abortWorkOrder: AbortWorkOrderMessage;
releaseSessionLock: ReleaseSessionLockMessage; releaseSessionLock: ReleaseSessionLockMessage;
sessionHeartbeat: SessionHeartbeatMessage; sessionHeartbeat: SessionHeartbeatMessage;
@ -111,6 +113,7 @@ export interface ServerToClientEvents {
requestWorkspaceMode: RequestWorkspaceModeMessage; requestWorkspaceMode: RequestWorkspaceModeMessage;
releaseSessionLock: ReleaseSessionLockMessage; releaseSessionLock: ReleaseSessionLockMessage;
sessionHeartbeat: SessionHeartbeatMessage; sessionHeartbeat: SessionHeartbeatMessage;
abortWorkOrder: AbortWorkOrderMessage;
processWorkOrder: ProcessWorkOrderMessage; processWorkOrder: ProcessWorkOrderMessage;
crashRecoveryResponse: CrashRecoveryResponseMessage; crashRecoveryResponse: CrashRecoveryResponseMessage;
requestTermination: RequestTerminationMessage; requestTermination: RequestTerminationMessage;