fix(tools): reject cancellation after async interception

This commit is contained in:
Rudy Celekli committed 2026-10-14 14:17:54 -04:00
1 parent 0796efe603
commit fffbe67b66
2 files changed
+47 -2

No files matched your search

+37
View File
@@ -39,3 +39,40 @@ test('cancelled catalog calls cannot execute or return late results', async () =
await assert.rejects(pending, { name: 'AbortError' });
assert.equal(calls, 1);
});
test('cancellation during an awaited interceptor cannot publish its late result', async () => {
const controller = new AbortController();
const reason = new Error('interceptor caller canceled');
let calls = 0;
const action = defineTool({
name: 'fixture_intercepted_action',
kind: 'action',
title: 'Fixture',
description: 'Offline fixture',
inputSchema: { type: 'object' },
run: async () => {
calls++;
return { summary: 'completed action', data: {} };
},
});
const catalog = composeCatalog({
tools: [action],
interceptors: [
async (call, next) => {
const result = await next(call);
await Promise.resolve();
controller.abort(reason);
return result;
},
],
});
await assert.rejects(
catalog.call(
'fixture_intercepted_action',
{},
{ signal: controller.signal },
),
(error) => error === reason,
);
assert.equal(calls, 1, 'completed actions are not rolled back or rerun');
});
+10 -2
View File
@@ -169,13 +169,21 @@ export function composeCatalog({
);
const index = new Map(available.map((tool) => [tool.name, tool]));
const invoke = (name, args, signal, parent) => {
const invoke = async (name, args, signal, parent) => {
signal?.throwIfAborted();
const tool = index.get(name);
if (!tool)
return Promise.reject(
new ToolError('unsupported', `No tool named ${name} is available`),
);
return chain({ tool, args, signal, ...(parent ? { parent } : {}) });
const result = await chain({
tool,
args,
signal,
...(parent ? { parent } : {}),
});
signal?.throwIfAborted();
return result;
};
const execute = async ({ tool, args, signal }) => {
signal?.throwIfAborted();