fix: abort runs between tool calls

This commit is contained in:
Peter Steinberger
2026-01-10 01:26:20 +01:00
parent a0a64a625e
commit 5898304fa0
6 changed files with 216 additions and 1 deletions

View File

@@ -853,6 +853,7 @@ export async function compactEmbeddedPiSession(params: {
sessionKey: params.sessionKey ?? params.sessionId,
agentDir,
config: params.config,
abortSignal: runAbortController.signal,
// No currentChannelId/currentThreadTs for compaction - not in message context
});
const machineName = await getMachineDisplayName();
@@ -1045,6 +1046,7 @@ export async function runEmbeddedPiAgent(params: {
const enqueueGlobal =
params.enqueue ??
((task, opts) => enqueueCommandInLane(globalLane, task, opts));
const runAbortController = new AbortController();
return enqueueCommandInLane(sessionLane, () =>
enqueueGlobal(async () => {
const started = Date.now();
@@ -1223,6 +1225,7 @@ export async function runEmbeddedPiAgent(params: {
sessionKey: params.sessionKey ?? params.sessionId,
agentDir,
config: params.config,
abortSignal: runAbortController.signal,
currentChannelId: params.currentChannelId,
currentThreadTs: params.currentThreadTs,
replyToMode: params.replyToMode,
@@ -1326,6 +1329,7 @@ export async function runEmbeddedPiAgent(params: {
const abortRun = (isTimeout = false) => {
aborted = true;
if (isTimeout) timedOut = true;
runAbortController.abort();
void session.abort();
};
let subscription: ReturnType<typeof subscribeEmbeddedPiSession>;

View File

@@ -503,6 +503,48 @@ export const __testing = {
cleanToolSchemaForGemini,
} as const;
function throwAbortError(): never {
const err = new Error("Aborted");
err.name = "AbortError";
throw err;
}
function combineAbortSignals(
a?: AbortSignal,
b?: AbortSignal,
): AbortSignal | undefined {
if (!a && !b) return undefined;
if (a && !b) return a;
if (b && !a) return b;
if (a?.aborted) return a;
if (b?.aborted) return b;
if (typeof AbortSignal.any === "function") {
return AbortSignal.any([a as AbortSignal, b as AbortSignal]);
}
const controller = new AbortController();
const onAbort = () => controller.abort();
a?.addEventListener("abort", onAbort, { once: true });
b?.addEventListener("abort", onAbort, { once: true });
return controller.signal;
}
function wrapToolWithAbortSignal(
tool: AnyAgentTool,
abortSignal?: AbortSignal,
): AnyAgentTool {
if (!abortSignal) return tool;
const execute = tool.execute;
if (!execute) return tool;
return {
...tool,
execute: async (toolCallId, params, signal, onUpdate) => {
const combined = combineAbortSignals(signal, abortSignal);
if (combined?.aborted) throwAbortError();
return await execute(toolCallId, params, combined, onUpdate);
},
};
}
export function createClawdbotCodingTools(options?: {
bash?: BashToolDefaults & ProcessToolDefaults;
messageProvider?: string;
@@ -511,6 +553,7 @@ export function createClawdbotCodingTools(options?: {
sessionKey?: string;
agentDir?: string;
config?: ClawdbotConfig;
abortSignal?: AbortSignal;
/** Current channel ID for auto-threading (Slack). */
currentChannelId?: string;
/** Current thread timestamp for auto-threading (Slack). */
@@ -607,8 +650,11 @@ export function createClawdbotCodingTools(options?: {
// Always normalize tool JSON Schemas before handing them to pi-agent/pi-ai.
// Without this, some providers (notably OpenAI) will reject root-level union schemas.
const normalized = subagentFiltered.map(normalizeToolParameters);
const withAbort = options?.abortSignal
? normalized.map((tool) => wrapToolWithAbortSignal(tool, options.abortSignal))
: normalized;
// Anthropic blocks specific lowercase tool names (bash, read, write, edit) with OAuth tokens.
// Always use capitalized versions for compatibility with both OAuth and regular API keys.
return renameBlockedToolsForOAuth(normalized);
return renameBlockedToolsForOAuth(withAbort);
}