Skip to content

Commit 0ab66ea

Browse files
fix(js/prompt): Fix for AbortSignal value of "this" issue (#3394)
1 parent 57ca001 commit 0ab66ea

File tree

2 files changed

+70
-0
lines changed

2 files changed

+70
-0
lines changed

js/ai/src/prompt.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,12 @@ function definePromptAsync<
331331
...renderOptions?.config,
332332
},
333333
});
334+
335+
// Fix for issue #3348: Preserve AbortSignal object
336+
// AbortSignal needs its prototype chain and shouldn't be processed by stripUndefinedProps
337+
if (renderOptions?.abortSignal) {
338+
opts.abortSignal = renderOptions.abortSignal;
339+
}
334340
// if config is empty and it was not explicitly passed in, we delete it, don't want {}
335341
if (Object.keys(opts.config).length === 0 && !renderOptions?.config) {
336342
delete opts.config;

js/ai/tests/prompt/prompt_test.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import assert from 'node:assert';
2121
import { beforeEach, describe, it } from 'node:test';
2222
import { Document } from '../../src/document.js';
2323
import type { GenerateOptions } from '../../src/index.js';
24+
import { defineModel } from '../../src/model.js';
2425
import {
2526
definePrompt,
2627
type PromptConfig,
@@ -45,6 +46,26 @@ describe('prompt', () => {
4546
},
4647
async () => 'a'
4748
);
49+
50+
// Define a special model to test AbortSignal preservation
51+
defineModel(
52+
registry,
53+
{
54+
name: 'abortTestModel',
55+
apiVersion: 'v2',
56+
},
57+
async (request, opts) => {
58+
// Store the abortSignal for verification
59+
(defineModel as any).__test__lastAbortSignal = request.abortSignal;
60+
return {
61+
message: {
62+
role: 'model',
63+
content: [{ text: 'AbortSignal preserved correctly' }],
64+
},
65+
finishReason: 'stop',
66+
};
67+
}
68+
);
4869
});
4970

5071
let basicTests: {
@@ -821,6 +842,49 @@ describe('prompt', () => {
821842
toJsonSchema({ schema: schema1 })
822843
);
823844
});
845+
846+
it('preserves AbortSignal objects through the rendering pipeline', async () => {
847+
// Test AbortSignal.timeout()
848+
const timeoutSignal = AbortSignal.timeout(1000);
849+
const prompt = definePrompt(registry, {
850+
name: 'abortTestPrompt',
851+
model: 'abortTestModel',
852+
prompt: 'test message',
853+
});
854+
855+
const rendered = await prompt.render(undefined, {
856+
abortSignal: timeoutSignal,
857+
});
858+
859+
// Verify the AbortSignal is preserved in the rendered options
860+
assert.ok(rendered.abortSignal, 'AbortSignal should be preserved');
861+
assert.strictEqual(
862+
rendered.abortSignal,
863+
timeoutSignal,
864+
'Should be the exact same AbortSignal instance'
865+
);
866+
assert.ok(
867+
rendered.abortSignal instanceof AbortSignal,
868+
'Should be an AbortSignal instance'
869+
);
870+
871+
// Test manual AbortController
872+
const controller = new AbortController();
873+
const rendered2 = await prompt.render(undefined, {
874+
abortSignal: controller.signal,
875+
});
876+
877+
assert.ok(rendered2.abortSignal, 'Manual AbortSignal should be preserved');
878+
assert.strictEqual(
879+
rendered2.abortSignal,
880+
controller.signal,
881+
'Should be the exact same manual AbortSignal instance'
882+
);
883+
assert.ok(
884+
rendered2.abortSignal instanceof AbortSignal,
885+
'Manual AbortSignal should be an AbortSignal instance'
886+
);
887+
});
824888
});
825889

826890
function stripUndefined(input: any) {

0 commit comments

Comments
 (0)