feat: soc-client timeout via toolConfig, 24 unit tests, fix abort early-check and placeholder regex
Deploy LangGraph Server to Azure Web App / build-and-deploy (push) Failing after 24s
Deploy LangGraph UI to Azure Static Web Apps / build-and-deploy (push) Failing after 39s

This commit is contained in:
gongzhiyong
2026-04-12 14:57:06 +08:00
parent 38395226e5
commit b93f08f206
8 changed files with 685 additions and 20 deletions
+5 -2
View File
@@ -13,7 +13,9 @@
"build": "tsc -b && vite build",
"lint": "eslint .",
"format": "prettier --write .",
"format:check": "prettier --check ."
"format:check": "prettier --check .",
"test": "vitest run",
"test:watch": "vitest"
},
"dependencies": {
"@assistant-ui/react": "^0.8.0",
@@ -85,7 +87,8 @@
"tailwindcss": "^4.0.6",
"typescript": "~5.7.2",
"typescript-eslint": "^8.22.0",
"vite": "^6.1.0"
"vite": "^6.1.0",
"vitest": "^3.1.1"
},
"overrides": {
"react-is": "^19.0.0-rc-69d4b800-20241021"
+371
View File
@@ -213,6 +213,9 @@ importers:
vite:
specifier: ^6.1.0
version: 6.2.0(@types/node@22.13.5)(jiti@2.4.2)(lightningcss@1.29.1)(tsx@4.19.3)(yaml@2.7.0)
vitest:
specifier: ^3.1.1
version: 3.2.4(@types/debug@4.1.12)(@types/node@22.13.5)(jiti@2.4.2)(lightningcss@1.29.1)(tsx@4.19.3)(yaml@2.7.0)
packages:
@@ -685,6 +688,9 @@ packages:
'@jridgewell/sourcemap-codec@1.5.0':
resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==}
'@jridgewell/sourcemap-codec@1.5.5':
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
'@jridgewell/trace-mapping@0.3.25':
resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
@@ -1320,6 +1326,9 @@ packages:
'@types/babel__traverse@7.20.6':
resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==}
'@types/chai@5.2.3':
resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
'@types/d3-array@3.2.1':
resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==}
@@ -1350,6 +1359,9 @@ packages:
'@types/debug@4.1.12':
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
'@types/deep-eql@4.0.2':
resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
'@types/estree-jsx@1.0.5':
resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==}
@@ -1483,6 +1495,35 @@ packages:
peerDependencies:
vite: ^4.2.0 || ^5.0.0 || ^6.0.0
'@vitest/expect@3.2.4':
resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==}
'@vitest/mocker@3.2.4':
resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==}
peerDependencies:
msw: ^2.4.9
vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0
peerDependenciesMeta:
msw:
optional: true
vite:
optional: true
'@vitest/pretty-format@3.2.4':
resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==}
'@vitest/runner@3.2.4':
resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==}
'@vitest/snapshot@3.2.4':
resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==}
'@vitest/spy@3.2.4':
resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==}
'@vitest/utils@3.2.4':
resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==}
abort-controller@3.0.0:
resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
engines: {node: '>=6.5'}
@@ -1539,6 +1580,10 @@ packages:
resolution: {integrity: sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==}
engines: {node: '>=10'}
assertion-error@2.0.1:
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
engines: {node: '>=12'}
assistant-stream@0.0.21:
resolution: {integrity: sha512-tQuGIuTGtmNnHSc6hxANVjO23LPmwFBjZnOt0xOhoBQBqW3vBv6DK2QOObgT9wnfLY8i2DRDwDbw8fSvYVzdMA==}
@@ -1586,6 +1631,10 @@ packages:
resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==}
engines: {node: '>=18'}
cac@6.7.14:
resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
engines: {node: '>=8'}
call-bind-apply-helpers@1.0.2:
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
engines: {node: '>= 0.4'}
@@ -1608,6 +1657,10 @@ packages:
resolution: {integrity: sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==}
engines: {node: '>=0.8'}
chai@5.3.3:
resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==}
engines: {node: '>=18'}
chalk@4.1.2:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
engines: {node: '>=10'}
@@ -1633,6 +1686,10 @@ packages:
character-reference-invalid@2.0.1:
resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==}
check-error@2.1.3:
resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==}
engines: {node: '>= 16'}
chokidar@4.0.3:
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
engines: {node: '>= 14.16.0'}
@@ -1789,6 +1846,15 @@ packages:
supports-color:
optional: true
debug@4.4.3:
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
engines: {node: '>=6.0'}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
decamelize@1.2.0:
resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==}
engines: {node: '>=0.10.0'}
@@ -1807,6 +1873,10 @@ packages:
babel-plugin-macros:
optional: true
deep-eql@5.0.2:
resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==}
engines: {node: '>=6'}
deep-is@0.1.4:
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
@@ -1899,6 +1969,9 @@ packages:
resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
engines: {node: '>= 0.4'}
es-module-lexer@1.7.0:
resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==}
es-object-atoms@1.1.1:
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
engines: {node: '>= 0.4'}
@@ -1979,6 +2052,9 @@ packages:
estree-util-is-identifier-name@3.0.0:
resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==}
estree-walker@3.0.3:
resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
esutils@2.0.3:
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
engines: {node: '>=0.10.0'}
@@ -2002,6 +2078,10 @@ packages:
resolution: {integrity: sha512-Fqs7ChZm72y40wKjOFXBKg7nJZvQJmewP5/7LtePDdnah/+FH9Hp5sgMujSCMPXlxOAW2//1jrW9pnsY7o20vQ==}
engines: {node: '>=18'}
expect-type@1.3.0:
resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==}
engines: {node: '>=12.0.0'}
extend@3.0.2:
resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==}
@@ -2047,6 +2127,15 @@ packages:
fd-slicer@1.1.0:
resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==}
fdir@6.5.0:
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
engines: {node: '>=12.0.0'}
peerDependencies:
picomatch: ^3 || ^4
peerDependenciesMeta:
picomatch:
optional: true
fecha@4.2.3:
resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==}
@@ -2389,6 +2478,9 @@ packages:
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
js-tokens@9.0.1:
resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==}
js-yaml@4.1.0:
resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
hasBin: true
@@ -2537,6 +2629,9 @@ packages:
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
hasBin: true
loupe@3.2.1:
resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==}
lowlight@1.20.0:
resolution: {integrity: sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==}
@@ -2551,6 +2646,9 @@ packages:
peerDependencies:
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
markdown-table@3.0.4:
resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==}
@@ -2874,6 +2972,13 @@ packages:
resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
engines: {node: '>=16 || 14 >=14.18'}
pathe@2.0.3:
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
pathval@2.0.1:
resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==}
engines: {node: '>= 14.16'}
pdf-parse@2.4.5:
resolution: {integrity: sha512-mHU89HGh7v+4u2ubfnevJ03lmPgQ5WU4CxAVmTSh/sxVTEDYd1er/dKS/A6vg77NX47KTEoihq8jZBLr8Cxuwg==}
engines: {node: '>=20.16.0 <21 || >=22.3.0'}
@@ -2927,6 +3032,10 @@ packages:
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
engines: {node: '>=8.6'}
picomatch@4.0.4:
resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==}
engines: {node: '>=12'}
postcss-modules-extract-imports@3.1.0:
resolution: {integrity: sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==}
engines: {node: ^10 || ^12 || >= 14}
@@ -3231,6 +3340,9 @@ packages:
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
engines: {node: '>=8'}
siginfo@2.0.0:
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
signal-exit@4.1.0:
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
engines: {node: '>=14'}
@@ -3268,10 +3380,16 @@ packages:
stack-trace@0.0.10:
resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==}
stackback@0.0.2:
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
stacktrace-parser@0.1.11:
resolution: {integrity: sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg==}
engines: {node: '>=6'}
std-env@3.10.0:
resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==}
string-hash@1.1.3:
resolution: {integrity: sha512-kJUvRUFK49aub+a7T1nNE66EJbZBMnBgoC1UbCZ5n6bsZKBRga4KgBRTMn/pFkeCZSYtNeSyMxPDM0AXWELk2A==}
@@ -3305,6 +3423,9 @@ packages:
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
engines: {node: '>=8'}
strip-literal@3.1.0:
resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==}
strnum@1.1.2:
resolution: {integrity: sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==}
@@ -3353,6 +3474,28 @@ packages:
tiny-invariant@1.3.3:
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
tinybench@2.9.0:
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
tinyexec@0.3.2:
resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==}
tinyglobby@0.2.16:
resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==}
engines: {node: '>=12.0.0'}
tinypool@1.1.1:
resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==}
engines: {node: ^18.0.0 || >=20.0.0}
tinyrainbow@2.0.0:
resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==}
engines: {node: '>=14.0.0'}
tinyspy@4.0.4:
resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==}
engines: {node: '>=14.0.0'}
to-regex-range@5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
@@ -3539,6 +3682,11 @@ packages:
victory-vendor@36.9.2:
resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==}
vite-node@3.2.4:
resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==}
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
hasBin: true
vite@6.2.0:
resolution: {integrity: sha512-7dPxoo+WsT/64rDcwoOjk76XHj+TqNTIvHKcuMQ1k4/SeHDaQt5GFAeLYzrimZrMpn/O6DtdI03WUjdxuPM0oQ==}
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
@@ -3579,6 +3727,34 @@ packages:
yaml:
optional: true
vitest@3.2.4:
resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==}
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
hasBin: true
peerDependencies:
'@edge-runtime/vm': '*'
'@types/debug': ^4.1.12
'@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0
'@vitest/browser': 3.2.4
'@vitest/ui': 3.2.4
happy-dom: '*'
jsdom: '*'
peerDependenciesMeta:
'@edge-runtime/vm':
optional: true
'@types/debug':
optional: true
'@types/node':
optional: true
'@vitest/browser':
optional: true
'@vitest/ui':
optional: true
happy-dom:
optional: true
jsdom:
optional: true
web-namespaces@2.0.1:
resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==}
@@ -3597,6 +3773,11 @@ packages:
engines: {node: '>= 8'}
hasBin: true
why-is-node-running@2.3.0:
resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
engines: {node: '>=8'}
hasBin: true
winston-console-format@1.0.8:
resolution: {integrity: sha512-dq7t/E0D0QRi4XIOwu6HM1+5e//WPqylH88GVjKEhQVrzGFg34MCz+G7pMJcXFBen9C0kBsu5GYgbYsE2LDwKw==}
@@ -4197,6 +4378,8 @@ snapshots:
'@jridgewell/sourcemap-codec@1.5.0': {}
'@jridgewell/sourcemap-codec@1.5.5': {}
'@jridgewell/trace-mapping@0.3.25':
dependencies:
'@jridgewell/resolve-uri': 3.1.2
@@ -4817,6 +5000,11 @@ snapshots:
dependencies:
'@babel/types': 7.26.9
'@types/chai@5.2.3':
dependencies:
'@types/deep-eql': 4.0.2
assertion-error: 2.0.1
'@types/d3-array@3.2.1': {}
'@types/d3-color@3.1.3': {}
@@ -4845,6 +5033,8 @@ snapshots:
dependencies:
'@types/ms': 2.1.0
'@types/deep-eql@4.0.2': {}
'@types/estree-jsx@1.0.5':
dependencies:
'@types/estree': 1.0.6
@@ -5020,6 +5210,48 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@vitest/expect@3.2.4':
dependencies:
'@types/chai': 5.2.3
'@vitest/spy': 3.2.4
'@vitest/utils': 3.2.4
chai: 5.3.3
tinyrainbow: 2.0.0
'@vitest/mocker@3.2.4(vite@6.2.0(@types/node@22.13.5)(jiti@2.4.2)(lightningcss@1.29.1)(tsx@4.19.3)(yaml@2.7.0))':
dependencies:
'@vitest/spy': 3.2.4
estree-walker: 3.0.3
magic-string: 0.30.21
optionalDependencies:
vite: 6.2.0(@types/node@22.13.5)(jiti@2.4.2)(lightningcss@1.29.1)(tsx@4.19.3)(yaml@2.7.0)
'@vitest/pretty-format@3.2.4':
dependencies:
tinyrainbow: 2.0.0
'@vitest/runner@3.2.4':
dependencies:
'@vitest/utils': 3.2.4
pathe: 2.0.3
strip-literal: 3.1.0
'@vitest/snapshot@3.2.4':
dependencies:
'@vitest/pretty-format': 3.2.4
magic-string: 0.30.21
pathe: 2.0.3
'@vitest/spy@3.2.4':
dependencies:
tinyspy: 4.0.4
'@vitest/utils@3.2.4':
dependencies:
'@vitest/pretty-format': 3.2.4
loupe: 3.2.1
tinyrainbow: 2.0.0
abort-controller@3.0.0:
dependencies:
event-target-shim: 5.0.1
@@ -5063,6 +5295,8 @@ snapshots:
dependencies:
tslib: 2.8.1
assertion-error@2.0.1: {}
assistant-stream@0.0.21:
dependencies:
nanoid: 3.3.8
@@ -5114,6 +5348,8 @@ snapshots:
dependencies:
run-applescript: 7.0.0
cac@6.7.14: {}
call-bind-apply-helpers@1.0.2:
dependencies:
es-errors: 1.3.0
@@ -5132,6 +5368,14 @@ snapshots:
adler-32: 1.3.1
crc-32: 1.2.2
chai@5.3.3:
dependencies:
assertion-error: 2.0.1
check-error: 2.1.3
deep-eql: 5.0.2
loupe: 3.2.1
pathval: 2.0.1
chalk@4.1.2:
dependencies:
ansi-styles: 4.3.0
@@ -5151,6 +5395,8 @@ snapshots:
character-reference-invalid@2.0.1: {}
check-error@2.1.3: {}
chokidar@4.0.3:
dependencies:
readdirp: 4.1.2
@@ -5278,6 +5524,10 @@ snapshots:
dependencies:
ms: 2.1.3
debug@4.4.3:
dependencies:
ms: 2.1.3
decamelize@1.2.0: {}
decimal.js-light@2.5.1: {}
@@ -5288,6 +5538,8 @@ snapshots:
dedent@1.5.3: {}
deep-eql@5.0.2: {}
deep-is@0.1.4: {}
default-browser-id@5.0.0: {}
@@ -5361,6 +5613,8 @@ snapshots:
es-errors@1.3.0: {}
es-module-lexer@1.7.0: {}
es-object-atoms@1.1.1:
dependencies:
es-errors: 1.3.0
@@ -5489,6 +5743,10 @@ snapshots:
estree-util-is-identifier-name@3.0.0: {}
estree-walker@3.0.3:
dependencies:
'@types/estree': 1.0.6
esutils@2.0.3: {}
event-target-shim@5.0.1: {}
@@ -5514,6 +5772,8 @@ snapshots:
exit-hook@4.0.0: {}
expect-type@1.3.0: {}
extend@3.0.2: {}
extract-zip@2.0.1:
@@ -5568,6 +5828,10 @@ snapshots:
dependencies:
pend: 1.2.0
fdir@6.5.0(picomatch@4.0.4):
optionalDependencies:
picomatch: 4.0.4
fecha@4.2.3: {}
figures@6.1.0:
@@ -5922,6 +6186,8 @@ snapshots:
js-tokens@4.0.0: {}
js-tokens@9.0.1: {}
js-yaml@4.1.0:
dependencies:
argparse: 2.0.1
@@ -6048,6 +6314,8 @@ snapshots:
dependencies:
js-tokens: 4.0.0
loupe@3.2.1: {}
lowlight@1.20.0:
dependencies:
fault: 1.0.4
@@ -6063,6 +6331,10 @@ snapshots:
dependencies:
react: 19.0.0
magic-string@0.30.21:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
markdown-table@3.0.4: {}
math-intrinsics@1.1.0: {}
@@ -6605,6 +6877,10 @@ snapshots:
lru-cache: 10.4.3
minipass: 7.1.2
pathe@2.0.3: {}
pathval@2.0.1: {}
pdf-parse@2.4.5:
dependencies:
'@napi-rs/canvas': 0.1.80
@@ -6655,6 +6931,8 @@ snapshots:
picomatch@2.3.1: {}
picomatch@4.0.4: {}
postcss-modules-extract-imports@3.1.0(postcss@8.5.3):
dependencies:
postcss: 8.5.3
@@ -7023,6 +7301,8 @@ snapshots:
shebang-regex@3.0.0: {}
siginfo@2.0.0: {}
signal-exit@4.1.0: {}
simple-swizzle@0.2.2:
@@ -7050,10 +7330,14 @@ snapshots:
stack-trace@0.0.10: {}
stackback@0.0.2: {}
stacktrace-parser@0.1.11:
dependencies:
type-fest: 0.7.1
std-env@3.10.0: {}
string-hash@1.1.3: {}
string-width@4.2.3:
@@ -7089,6 +7373,10 @@ snapshots:
strip-json-comments@3.1.1: {}
strip-literal@3.1.0:
dependencies:
js-tokens: 9.0.1
strnum@1.1.2: {}
strnum@2.2.3: {}
@@ -7135,6 +7423,21 @@ snapshots:
tiny-invariant@1.3.3: {}
tinybench@2.9.0: {}
tinyexec@0.3.2: {}
tinyglobby@0.2.16:
dependencies:
fdir: 6.5.0(picomatch@4.0.4)
picomatch: 4.0.4
tinypool@1.1.1: {}
tinyrainbow@2.0.0: {}
tinyspy@4.0.4: {}
to-regex-range@5.0.1:
dependencies:
is-number: 7.0.0
@@ -7323,6 +7626,27 @@ snapshots:
d3-time: 3.1.0
d3-timer: 3.0.1
vite-node@3.2.4(@types/node@22.13.5)(jiti@2.4.2)(lightningcss@1.29.1)(tsx@4.19.3)(yaml@2.7.0):
dependencies:
cac: 6.7.14
debug: 4.4.3
es-module-lexer: 1.7.0
pathe: 2.0.3
vite: 6.2.0(@types/node@22.13.5)(jiti@2.4.2)(lightningcss@1.29.1)(tsx@4.19.3)(yaml@2.7.0)
transitivePeerDependencies:
- '@types/node'
- jiti
- less
- lightningcss
- sass
- sass-embedded
- stylus
- sugarss
- supports-color
- terser
- tsx
- yaml
vite@6.2.0(@types/node@22.13.5)(jiti@2.4.2)(lightningcss@1.29.1)(tsx@4.19.3)(yaml@2.7.0):
dependencies:
esbuild: 0.25.0
@@ -7336,6 +7660,48 @@ snapshots:
tsx: 4.19.3
yaml: 2.7.0
vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.13.5)(jiti@2.4.2)(lightningcss@1.29.1)(tsx@4.19.3)(yaml@2.7.0):
dependencies:
'@types/chai': 5.2.3
'@vitest/expect': 3.2.4
'@vitest/mocker': 3.2.4(vite@6.2.0(@types/node@22.13.5)(jiti@2.4.2)(lightningcss@1.29.1)(tsx@4.19.3)(yaml@2.7.0))
'@vitest/pretty-format': 3.2.4
'@vitest/runner': 3.2.4
'@vitest/snapshot': 3.2.4
'@vitest/spy': 3.2.4
'@vitest/utils': 3.2.4
chai: 5.3.3
debug: 4.4.3
expect-type: 1.3.0
magic-string: 0.30.21
pathe: 2.0.3
picomatch: 4.0.4
std-env: 3.10.0
tinybench: 2.9.0
tinyexec: 0.3.2
tinyglobby: 0.2.16
tinypool: 1.1.1
tinyrainbow: 2.0.0
vite: 6.2.0(@types/node@22.13.5)(jiti@2.4.2)(lightningcss@1.29.1)(tsx@4.19.3)(yaml@2.7.0)
vite-node: 3.2.4(@types/node@22.13.5)(jiti@2.4.2)(lightningcss@1.29.1)(tsx@4.19.3)(yaml@2.7.0)
why-is-node-running: 2.3.0
optionalDependencies:
'@types/debug': 4.1.12
'@types/node': 22.13.5
transitivePeerDependencies:
- jiti
- less
- lightningcss
- msw
- sass
- sass-embedded
- stylus
- sugarss
- supports-color
- terser
- tsx
- yaml
web-namespaces@2.0.1: {}
web-streams-polyfill@4.0.0-beta.3: {}
@@ -7351,6 +7717,11 @@ snapshots:
dependencies:
isexe: 2.0.0
why-is-node-running@2.3.0:
dependencies:
siginfo: 2.0.0
stackback: 0.0.2
winston-console-format@1.0.8:
dependencies:
colors: 1.4.0
@@ -0,0 +1,187 @@
import { describe, it, expect, vi } from "vitest";
// Mock config to avoid missing env var errors at import time
vi.mock("@/agent/utils/config", () => ({
config: {
kb: { url: "http://mock", searchPath: "/search", apiKey: "mock" },
gongdan: { apiBase: "http://mock", apiKey: "mock" },
jina: { apiKey: "mock" },
serper: { apiKey: "mock" },
daytona: { apiKey: "mock", apiUrl: "http://mock" },
azureOpenAI: { apiKey: "mock", endpoint: "http://mock", apiVersion: "2024", deployment: "mock" },
},
}));
import { preValidateToolCall } from "../tool-executor.js";
import type { EnterpriseState } from "../../types.js";
import { HumanMessage } from "@langchain/core/messages";
/**
* Build a minimal EnterpriseState stub for testing preValidateToolCall.
* Only `messages` and `execution_log` are inspected by the validator.
*/
function makeState(
overrides: Partial<Pick<EnterpriseState, "messages" | "execution_log">> = {},
): EnterpriseState {
return {
messages: overrides.messages ?? [new HumanMessage("hello world test")],
ui: [],
timestamp: Date.now(),
toolStatus: [],
execution_log: overrides.execution_log ?? [],
} as unknown as EnterpriseState;
}
// ---------------------------------------------------------------------------
// ticket_detail
// ---------------------------------------------------------------------------
describe("preValidateToolCall - ticket_detail", () => {
it("passes with a valid ticket_id", () => {
const result = preValidateToolCall(
"ticket_detail",
{ ticket_id: "TK-2026-123456" },
makeState(),
);
expect(result).toBeNull();
});
it.each(["xxx", "null", "{ticket_id}", "undefined", "test", "id", "unknown"])(
"blocks placeholder id: %s",
(badId) => {
const result = preValidateToolCall(
"ticket_detail",
{ ticket_id: badId },
makeState(),
);
expect(result).not.toBeNull();
expect(result).toContain("ticket_detail");
},
);
});
// ---------------------------------------------------------------------------
// kb_search
// ---------------------------------------------------------------------------
describe("preValidateToolCall - kb_search", () => {
it("blocks query shorter than 5 characters", () => {
const result = preValidateToolCall(
"kb_search",
{ query: "abc" },
makeState(),
);
expect(result).toContain("搜索词过短");
});
it("blocks duplicate same-query in the same turn", () => {
const state = makeState({
execution_log: [
{
tool: "kb_search",
status: "success",
inputSummary: "query: VPN connection issue",
summary: "found 3 results",
timestamp: Date.now(),
durationMs: 100,
resultCount: 3,
},
],
});
const result = preValidateToolCall(
"kb_search",
{ query: "VPN connection issue" },
state,
);
expect(result).toContain("已查询过相同内容");
});
it("passes with a valid long-enough query and no duplicates", () => {
const result = preValidateToolCall(
"kb_search",
{ query: "how to reset password" },
makeState(),
);
expect(result).toBeNull();
});
});
// ---------------------------------------------------------------------------
// ticket_list - page validation
// ---------------------------------------------------------------------------
describe("preValidateToolCall - ticket_list page range", () => {
it("blocks page=0 (below range)", () => {
const result = preValidateToolCall(
"ticket_list",
{ page: 0 },
makeState(),
);
expect(result).toContain("page 参数无效");
});
it("blocks page=101 (above range)", () => {
const result = preValidateToolCall(
"ticket_list",
{ page: 101 },
makeState(),
);
expect(result).toContain("page 参数无效");
});
it.each([1, 50, 100])("passes with valid page=%i", (page) => {
const result = preValidateToolCall(
"ticket_list",
{ page },
makeState(),
);
expect(result).toBeNull();
});
});
// ---------------------------------------------------------------------------
// execution_log length >= 3
// ---------------------------------------------------------------------------
describe("preValidateToolCall - tool call limit", () => {
it("blocks when execution_log.length >= 3", () => {
const state = makeState({
execution_log: [
{ tool: "kb_search", status: "success", summary: "", timestamp: 1, durationMs: 0, inputSummary: "" },
{ tool: "ticket_list", status: "success", summary: "", timestamp: 2, durationMs: 0, inputSummary: "" },
{ tool: "ticket_detail", status: "success", summary: "", timestamp: 3, durationMs: 0, inputSummary: "" },
],
});
const result = preValidateToolCall(
"kb_search",
{ query: "something long enough" },
state,
);
expect(result).toContain("达到单轮上限");
});
});
// ---------------------------------------------------------------------------
// ticket_list when message contains TK-xxxx
// ---------------------------------------------------------------------------
describe("preValidateToolCall - ticket_list with TK-xxxx in message", () => {
it("blocks ticket_list when user message contains a ticket number", () => {
const state = makeState({
messages: [new HumanMessage("请帮我查一下 TK-2026-296691 的状态")],
});
const result = preValidateToolCall(
"ticket_list",
{ page: 1 },
state,
);
expect(result).toContain("ticket_detail");
});
it("passes ticket_list when message has no ticket number", () => {
const state = makeState({
messages: [new HumanMessage("列出最近的工单")],
});
const result = preValidateToolCall(
"ticket_list",
{ page: 1 },
state,
);
expect(result).toBeNull();
});
});
@@ -185,7 +185,7 @@ async function withTimeout<T>(
* Pre-validate tool call arguments before execution.
* Returns a human-readable block reason, or null if validation passes.
*/
function preValidateToolCall(
export function preValidateToolCall(
name: string,
args: Record<string, unknown>,
state: EnterpriseState,
@@ -204,7 +204,7 @@ function preValidateToolCall(
return "ticket_detail 调用被拦截:未提供有效工单编号,请改用 ticket_list 查询工单列表";
}
// Placeholder ID detection
const PLACEHOLDER = /^(xxx|unknown|test|id|null|undefined|\d{1,2})$/i;
const PLACEHOLDER = /^(xxx|unknown|test|id|null|undefined|\d{1,2}|\{.*\})$/i;
if (PLACEHOLDER.test(id)) {
return `ticket_detail 调用被拦截:工单 ID "${id}" 看起来是占位符,请先用 ticket_list 查询真实工单号`;
}
@@ -6,6 +6,7 @@
*/
import { config } from "@/agent/utils/config";
import { getToolConfig } from "@/agent/utils/toolConfig";
// --- Knowledge Base Search ---
export async function kbSearch(query: string): Promise<{
@@ -24,7 +25,7 @@ export async function kbSearch(query: string): Promise<{
"api-key": config.kb.apiKey,
},
body: JSON.stringify({ query, top: 5, search_mode: "hybrid" }),
signal: AbortSignal.timeout(45000),
signal: AbortSignal.timeout(getToolConfig("kb_search").timeoutMs),
});
if (!resp.ok) throw new Error(`KB search failed: ${resp.status}`);
return resp.json();
@@ -47,7 +48,7 @@ export async function ticketList(
const url = `${config.gongdan.apiBase}/api/tickets?page=${page}&pageSize=${pageSize}`;
const resp = await fetch(url, {
headers: { "X-Api-Key": config.gongdan.apiKey },
signal: AbortSignal.timeout(15000),
signal: AbortSignal.timeout(getToolConfig("ticket_list").timeoutMs),
});
if (!resp.ok) throw new Error(`Ticket list failed: ${resp.status}`);
return resp.json();
@@ -59,6 +60,7 @@ export async function ticketDetail(
): Promise<Record<string, unknown>> {
const base = config.gongdan.apiBase;
const headers = { "X-Api-Key": config.gongdan.apiKey };
const tdTimeout = getToolConfig("ticket_detail").timeoutMs;
// If ticketId looks like a ticketNumber (e.g. "TK-2026-296691"), resolve UUID first.
// Strategy: try query-param filter first; if no match, fall back to full list + local find.
@@ -69,7 +71,7 @@ export async function ticketDetail(
const searchUrl = `${base}/api/tickets?${paramName}=${encodeURIComponent(ticketId)}&pageSize=50`;
const searchResp = await fetch(searchUrl, {
headers,
signal: AbortSignal.timeout(15000),
signal: AbortSignal.timeout(tdTimeout),
});
if (searchResp.ok) {
const searchData = await searchResp.json();
@@ -91,7 +93,7 @@ export async function ticketDetail(
const listUrl = `${base}/api/tickets?pageSize=100`;
const listResp = await fetch(listUrl, {
headers,
signal: AbortSignal.timeout(15000),
signal: AbortSignal.timeout(tdTimeout),
});
if (listResp.ok) {
const listData = await listResp.json();
@@ -111,7 +113,7 @@ export async function ticketDetail(
let url = `${base}/api/tickets/detail/${resolvedId}`;
let resp = await fetch(url, {
headers,
signal: AbortSignal.timeout(15000),
signal: AbortSignal.timeout(tdTimeout),
});
// If /detail/ returns 404, fall back to /api/tickets/{id}
@@ -119,7 +121,7 @@ export async function ticketDetail(
url = `${base}/api/tickets/${resolvedId}`;
resp = await fetch(url, {
headers,
signal: AbortSignal.timeout(15000),
signal: AbortSignal.timeout(tdTimeout),
});
}
@@ -147,7 +149,7 @@ export async function webSearch(query: string): Promise<{
method: "POST",
headers,
body: JSON.stringify({ q: query, num: 5 }),
signal: AbortSignal.timeout(12000),
signal: AbortSignal.timeout(getToolConfig("web_search").timeoutMs),
});
if (!searchResp.ok) throw new Error(`Jina search failed: ${searchResp.status}`);
const searchData = await searchResp.json();
@@ -159,7 +161,7 @@ export async function webSearch(query: string): Promise<{
if (!r.url) return "";
const readResp = await fetch(`https://r.jina.ai/${r.url}`, {
headers,
signal: AbortSignal.timeout(8000),
signal: AbortSignal.timeout(getToolConfig("web_search").timeoutMs),
});
if (!readResp.ok) return "";
const data = await readResp.json();
@@ -200,7 +202,7 @@ export async function googleSearch(query: string): Promise<{
"Content-Type": "application/json",
},
body: JSON.stringify({ q: query, num: 10, gl: "cn", hl: "zh-cn" }),
signal: AbortSignal.timeout(10000),
signal: AbortSignal.timeout(getToolConfig("google_search").timeoutMs),
});
if (!resp.ok) throw new Error(`Serper search failed: ${resp.status}`);
const data = await resp.json();
@@ -229,7 +231,7 @@ export async function webRead(url: string): Promise<{ content: string; title: st
Authorization: `Bearer ${config.jina.apiKey}`,
Accept: "application/json",
},
signal: AbortSignal.timeout(10000),
signal: AbortSignal.timeout(getToolConfig("web_search").timeoutMs),
});
if (!resp.ok) throw new Error(`Jina read failed: ${resp.status}`);
const data = await resp.json();
@@ -252,7 +254,7 @@ export async function jinaRerank(query: string, documents: string[], topN = 5):
model: "jina-reranker-v2-base-multilingual",
top_n: topN,
}),
signal: AbortSignal.timeout(10000),
signal: AbortSignal.timeout(getToolConfig("web_search").timeoutMs),
});
if (!resp.ok) throw new Error(`Jina rerank failed: ${resp.status}`);
return resp.json();
@@ -264,13 +266,10 @@ export async function jinaRerank(query: string, documents: string[], topN = 5):
const SANDBOX_MAX_CODE_LENGTH = 10_000;
/** Maximum output length (characters) before truncation */
const SANDBOX_MAX_OUTPUT_LENGTH = 3_000;
/** Default code execution timeout (ms) */
const SANDBOX_DEFAULT_EXEC_TIMEOUT_MS = 15_000;
export async function sandboxRun(
code: string,
language = "python",
timeoutMs = SANDBOX_DEFAULT_EXEC_TIMEOUT_MS,
timeoutMs = getToolConfig("code_execute").timeoutMs,
): Promise<{
exit_code: number;
stdout: string;
@@ -295,7 +294,7 @@ export async function sandboxRun(
method: "POST",
headers,
body: JSON.stringify({ autoStopInterval: 5, autoDeleteInterval: 0 }),
signal: AbortSignal.timeout(30000),
signal: AbortSignal.timeout(getToolConfig("code_execute").timeoutMs * 2),
});
} catch (e) {
const isTimeout = e instanceof DOMException && e.name === "AbortError";
@@ -0,0 +1,86 @@
import { describe, it, expect, vi } from "vitest";
import { executeWithRetry } from "../retry.js";
describe("executeWithRetry", () => {
it("returns result on first success with retryCount=0 (no retries triggered)", async () => {
const onAttempt = vi.fn();
const result = await executeWithRetry(
() => Promise.resolve("ok"),
2,
{ backoffMs: 10, onAttempt },
);
expect(result).toBe("ok");
expect(onAttempt).not.toHaveBeenCalled();
});
it("retries once then succeeds (retryCount=1)", async () => {
let call = 0;
const onAttempt = vi.fn();
const result = await executeWithRetry(
() => {
call++;
if (call === 1) return Promise.reject(new Error("fail1"));
return Promise.resolve("ok2");
},
2,
{ backoffMs: 10, onAttempt },
);
expect(result).toBe("ok2");
expect(onAttempt).toHaveBeenCalledTimes(1);
expect(onAttempt).toHaveBeenCalledWith(1);
});
it("throws the last error when all attempts fail", async () => {
let call = 0;
await expect(
executeWithRetry(
() => {
call++;
return Promise.reject(new Error(`fail${call}`));
},
2, // 2 retries = 3 total attempts
{ backoffMs: 10 },
),
).rejects.toThrow("fail3");
});
it("rejects immediately when abortSignal is already aborted", async () => {
const controller = new AbortController();
controller.abort();
const fn = vi.fn(() => Promise.reject(new Error("should-retry")));
await expect(
executeWithRetry(fn, 3, {
backoffMs: 10,
abortSignal: controller.signal,
}),
).rejects.toThrow();
// fn is called once (the initial attempt), then during the backoff wait
// the abort fires and rejects with RunCancelled before any retry
expect(fn).toHaveBeenCalledTimes(1);
});
it("rejects with RunCancelled when abortSignal fires during backoff", async () => {
const controller = new AbortController();
let call = 0;
const fn = () => {
call++;
if (call === 1) {
// Abort during the backoff delay after first failure
setTimeout(() => controller.abort(), 5);
return Promise.reject(new Error("fail1"));
}
return Promise.resolve("should-not-reach");
};
await expect(
executeWithRetry(fn, 3, {
backoffMs: 5000, // long backoff so abort fires first
abortSignal: controller.signal,
}),
).rejects.toThrow("RunCancelled");
});
});
+5
View File
@@ -26,6 +26,11 @@ export async function executeWithRetry<T>(
`[retry] attempt ${attempt}/${maxAttempts} failed, retrying in ${delay}ms`,
);
await new Promise<void>((resolve, reject) => {
// Check if signal already aborted before adding listener
if (options?.abortSignal?.aborted) {
reject(new Error("RunCancelled"));
return;
}
const t = setTimeout(resolve, delay);
options?.abortSignal?.addEventListener("abort", () => {
clearTimeout(t);
+14
View File
@@ -0,0 +1,14 @@
import { defineConfig } from "vitest/config";
import path from "path";
export default defineConfig({
test: {
globals: true,
environment: "node",
},
resolve: {
alias: {
"@": path.resolve(__dirname, "src"),
},
},
});