From a505e441ad5e9f567e9e2d6e9bbde01205350d2f Mon Sep 17 00:00:00 2001 From: gongzhiyong Date: Sat, 11 Apr 2026 12:31:55 +0800 Subject: [PATCH] feat: file upload + context compression + fix maxTokens MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add file-service.ts: Azure Blob upload + PDF/Excel/image parsing - Add truncate-messages.ts: context compression for long conversations - Fix maxTokens: flash 500→2000, auto 2048→4096, pro 4096→8192 - Integrate truncateMessages in general-input and router nodes - Add @azure/storage-blob, pdf-parse, xlsx dependencies Co-Authored-By: Claude Sonnet 4.6 --- langgraph/package.json | 4 + langgraph/pnpm-lock.yaml | 455 ++++++++++++++++++ .../agent/supervisor/nodes/general-input.ts | 4 +- .../src/agent/supervisor/nodes/router.ts | 6 +- langgraph/src/agent/utils/create-llm.ts | 6 +- langgraph/src/agent/utils/file-service.ts | 105 ++++ .../src/agent/utils/truncate-messages.ts | 87 ++++ 7 files changed, 661 insertions(+), 6 deletions(-) create mode 100644 langgraph/src/agent/utils/file-service.ts create mode 100644 langgraph/src/agent/utils/truncate-messages.ts diff --git a/langgraph/package.json b/langgraph/package.json index 6d7ecd6..2f83c24 100644 --- a/langgraph/package.json +++ b/langgraph/package.json @@ -19,6 +19,7 @@ "@assistant-ui/react": "^0.8.0", "@assistant-ui/react-markdown": "^0.8.0", "@assistant-ui/react-syntax-highlighter": "^0.7.2", + "@azure/storage-blob": "^12.31.0", "@faker-js/faker": "^9.5.1", "@langchain/anthropic": "^0.3.18", "@langchain/core": "^0.3.45", @@ -46,6 +47,7 @@ "katex": "^0.16.21", "lucide-react": "^0.476.0", "next-themes": "^0.4.4", + "pdf-parse": "^2.4.5", "prettier": "^3.5.2", "react": "^19.0.0", "react-dom": "^19.0.0", @@ -62,11 +64,13 @@ "use-query-params": "^2.2.1", "use-stick-to-bottom": "^1.0.46", "uuid": "^11.0.5", + "xlsx": "^0.18.5", "zod": "^3.23.8" }, "devDependencies": { "@eslint/js": "^9.19.0", "@types/node": "^22.13.5", + "@types/pdf-parse": "^1.1.5", "@types/react": "^19.0.8", "@types/react-dom": "^19.0.3", "@types/react-syntax-highlighter": "^15.5.13", diff --git a/langgraph/pnpm-lock.yaml b/langgraph/pnpm-lock.yaml index e338554..94be835 100644 --- a/langgraph/pnpm-lock.yaml +++ b/langgraph/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: '@assistant-ui/react-syntax-highlighter': specifier: ^0.7.2 version: 0.7.10(@assistant-ui/react-markdown@0.8.0(@assistant-ui/react@0.8.0(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@assistant-ui/react@0.8.0(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react-syntax-highlighter@15.5.13)(@types/react@19.0.10)(react-syntax-highlighter@15.6.1(react@19.0.0))(react@19.0.0) + '@azure/storage-blob': + specifier: ^12.31.0 + version: 12.31.0 '@faker-js/faker': specifier: ^9.5.1 version: 9.5.1 @@ -98,6 +101,9 @@ importers: next-themes: specifier: ^0.4.4 version: 0.4.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + pdf-parse: + specifier: ^2.4.5 + version: 2.4.5 prettier: specifier: ^3.5.2 version: 3.5.2 @@ -146,6 +152,9 @@ importers: uuid: specifier: ^11.0.5 version: 11.1.0 + xlsx: + specifier: ^0.18.5 + version: 0.18.5 zod: specifier: ^3.23.8 version: 3.24.2 @@ -156,6 +165,9 @@ importers: '@types/node': specifier: ^22.13.5 version: 22.13.5 + '@types/pdf-parse': + specifier: ^1.1.5 + version: 1.1.5 '@types/react': specifier: ^19.0.8 version: 19.0.10 @@ -258,6 +270,61 @@ packages: '@types/react-dom': optional: true + '@azure/abort-controller@2.1.2': + resolution: {integrity: sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==} + engines: {node: '>=18.0.0'} + + '@azure/core-auth@1.10.1': + resolution: {integrity: sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg==} + engines: {node: '>=20.0.0'} + + '@azure/core-client@1.10.1': + resolution: {integrity: sha512-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w==} + engines: {node: '>=20.0.0'} + + '@azure/core-http-compat@2.4.0': + resolution: {integrity: sha512-f1P96IB399YiN2ARYHP7EpZi3Bf3wH4SN2lGzrw7JVwm7bbsVYtf2iKSBwTywD2P62NOPZGHFSZi+6jjb75JuA==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@azure/core-client': ^1.10.0 + '@azure/core-rest-pipeline': ^1.22.0 + + '@azure/core-lro@2.7.2': + resolution: {integrity: sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw==} + engines: {node: '>=18.0.0'} + + '@azure/core-paging@1.6.2': + resolution: {integrity: sha512-YKWi9YuCU04B55h25cnOYZHxXYtEvQEbKST5vqRga7hWY9ydd3FZHdeQF8pyh+acWZvppw13M/LMGx0LABUVMA==} + engines: {node: '>=18.0.0'} + + '@azure/core-rest-pipeline@1.23.0': + resolution: {integrity: sha512-Evs1INHo+jUjwHi1T6SG6Ua/LHOQBCLuKEEE6efIpt4ZOoNonaT1kP32GoOcdNDbfqsD2445CPri3MubBy5DEQ==} + engines: {node: '>=20.0.0'} + + '@azure/core-tracing@1.3.1': + resolution: {integrity: sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ==} + engines: {node: '>=20.0.0'} + + '@azure/core-util@1.13.1': + resolution: {integrity: sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A==} + engines: {node: '>=20.0.0'} + + '@azure/core-xml@1.5.1': + resolution: {integrity: sha512-xcNRHqCoSp4AunOALEae6A8f3qATb83gSrm31Iqb01OzblvC3/W/bfXozcq78EzIdzZzuH1bZ2NvRR0TdX709w==} + engines: {node: '>=20.0.0'} + + '@azure/logger@1.3.0': + resolution: {integrity: sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA==} + engines: {node: '>=20.0.0'} + + '@azure/storage-blob@12.31.0': + resolution: {integrity: sha512-DBgNv10aCSxopt92DkTDD0o9xScXeBqPKGmR50FPZQaEcH4JLQ+GEOGEDv19V5BMkB7kxr+m4h6il/cCDPvmHg==} + engines: {node: '>=20.0.0'} + + '@azure/storage-common@12.3.0': + resolution: {integrity: sha512-/OFHhy86aG5Pe8dP5tsp+BuJ25JOAl9yaMU3WZbkeoiFMHFtJ7tu5ili7qEdBXNW9G5lDB19trwyI6V49F/8iQ==} + engines: {node: '>=20.0.0'} + '@babel/code-frame@7.26.2': resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} engines: {node: '>=6.9.0'} @@ -700,6 +767,70 @@ packages: peerDependencies: '@langchain/core': '>=0.3.39 <0.4.0' + '@napi-rs/canvas-android-arm64@0.1.80': + resolution: {integrity: sha512-sk7xhN/MoXeuExlggf91pNziBxLPVUqF2CAVnB57KLG/pz7+U5TKG8eXdc3pm0d7Od0WreB6ZKLj37sX9muGOQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@napi-rs/canvas-darwin-arm64@0.1.80': + resolution: {integrity: sha512-O64APRTXRUiAz0P8gErkfEr3lipLJgM6pjATwavZ22ebhjYl/SUbpgM0xcWPQBNMP1n29afAC/Us5PX1vg+JNQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@napi-rs/canvas-darwin-x64@0.1.80': + resolution: {integrity: sha512-FqqSU7qFce0Cp3pwnTjVkKjjOtxMqRe6lmINxpIZYaZNnVI0H5FtsaraZJ36SiTHNjZlUB69/HhxNDT1Aaa9vA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@napi-rs/canvas-linux-arm-gnueabihf@0.1.80': + resolution: {integrity: sha512-eyWz0ddBDQc7/JbAtY4OtZ5SpK8tR4JsCYEZjCE3dI8pqoWUC8oMwYSBGCYfsx2w47cQgQCgMVRVTFiiO38hHQ==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@napi-rs/canvas-linux-arm64-gnu@0.1.80': + resolution: {integrity: sha512-qwA63t8A86bnxhuA/GwOkK3jvb+XTQaTiVML0vAWoHyoZYTjNs7BzoOONDgTnNtr8/yHrq64XXzUoLqDzU+Uuw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@napi-rs/canvas-linux-arm64-musl@0.1.80': + resolution: {integrity: sha512-1XbCOz/ymhj24lFaIXtWnwv/6eFHXDrjP0jYkc6iHQ9q8oXKzUX1Lc6bu+wuGiLhGh2GS/2JlfORC5ZcXimRcg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@napi-rs/canvas-linux-riscv64-gnu@0.1.80': + resolution: {integrity: sha512-XTzR125w5ZMs0lJcxRlS1K3P5RaZ9RmUsPtd1uGt+EfDyYMu4c6SEROYsxyatbbu/2+lPe7MPHOO/0a0x7L/gw==} + engines: {node: '>= 10'} + cpu: [riscv64] + os: [linux] + + '@napi-rs/canvas-linux-x64-gnu@0.1.80': + resolution: {integrity: sha512-BeXAmhKg1kX3UCrJsYbdQd3hIMDH/K6HnP/pG2LuITaXhXBiNdh//TVVVVCBbJzVQaV5gK/4ZOCMrQW9mvuTqA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@napi-rs/canvas-linux-x64-musl@0.1.80': + resolution: {integrity: sha512-x0XvZWdHbkgdgucJsRxprX/4o4sEed7qo9rCQA9ugiS9qE2QvP0RIiEugtZhfLH3cyI+jIRFJHV4Fuz+1BHHMg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@napi-rs/canvas-win32-x64-msvc@0.1.80': + resolution: {integrity: sha512-Z8jPsM6df5V8B1HrCHB05+bDiCxjE9QA//3YrkKIdVDEwn5RKaqOxCJDRJkl48cJbylcrJbW4HxZbTte8juuPg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@napi-rs/canvas@0.1.80': + resolution: {integrity: sha512-DxuT1ClnIPts1kQx8FBmkk4BQDTfI5kIzywAaMjQSXfNnra5UFU9PwurXrl+Je3bJ6BGsp/zmshVVFbCmyI+ww==} + engines: {node: '>= 10'} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -1252,6 +1383,9 @@ packages: '@types/node@22.13.5': resolution: {integrity: sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg==} + '@types/pdf-parse@1.1.5': + resolution: {integrity: sha512-kBfrSXsloMnUJOKi25s3+hRmkycHfLK6A09eRGqF/N8BkQoPUmaCr+q8Cli5FnfohEz/rsv82zAiPz/LXtOGhA==} + '@types/prismjs@1.26.5': resolution: {integrity: sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==} @@ -1336,6 +1470,10 @@ packages: peerDependencies: typescript: '*' + '@typespec/ts-http-runtime@0.3.5': + resolution: {integrity: sha512-yURCknZhvywvQItHMMmFSo+fq5arCUIyz/CVk7jD89MSai7dkaX8ufjCWp3NttLojoTVbcE72ri+be/TnEbMHw==} + engines: {node: '>=20.0.0'} + '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} @@ -1359,6 +1497,14 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + adler-32@1.3.1: + resolution: {integrity: sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==} + engines: {node: '>=0.8'} + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + agentkeepalive@4.6.0: resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} engines: {node: '>= 8.0.0'} @@ -1458,6 +1604,10 @@ packages: ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + cfb@1.2.2: + resolution: {integrity: sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==} + engines: {node: '>=0.8'} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -1501,6 +1651,10 @@ packages: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} + codepage@1.15.0: + resolution: {integrity: sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==} + engines: {node: '>=0.8'} + color-convert@1.9.3: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} @@ -1562,6 +1716,11 @@ packages: resolution: {integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==} engines: {node: '>=12.13'} + crc-32@1.2.2: + resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} + engines: {node: '>=0.8'} + hasBin: true + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -1831,6 +1990,10 @@ packages: eventemitter3@4.0.7: resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + execa@9.5.2: resolution: {integrity: sha512-EHlpxMCpHWSAh1dgS6bVeoLAXGnJNdR93aabr4QCGbzOM73o5XmRfM/e5FUqsw3aagP8S8XEWUWFAxnRBnAF0Q==} engines: {node: ^18.19.0 || >=20.5.0} @@ -1864,10 +2027,17 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-xml-builder@1.1.4: + resolution: {integrity: sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==} + fast-xml-parser@4.5.3: resolution: {integrity: sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==} hasBin: true + fast-xml-parser@5.5.11: + resolution: {integrity: sha512-QL0eb0YbSTVWF6tTf1+LEMSgtCEjBYPpnAjoLC8SscESlAjXEIRJ7cHtLG0pLeDFaZLa4VKZLArtA/60ZS7vyA==} + hasBin: true + fastq@1.19.1: resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} @@ -1925,6 +2095,10 @@ packages: resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==} engines: {node: '>= 12.20'} + frac@1.1.2: + resolution: {integrity: sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==} + engines: {node: '>=0.8'} + fraction.js@4.3.7: resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} @@ -2079,6 +2253,14 @@ packages: html-url-attributes@3.0.1: resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + human-signals@8.0.0: resolution: {integrity: sha512-/1/GPCpDUCCYwlERiYjxoczfP0zfvZMU/OWgQPMya9AbAE24vseigFdhAMObpc8Q4lc/kjutPfUddDYyAmejnA==} engines: {node: '>=18.18.0'} @@ -2676,6 +2858,10 @@ packages: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} + path-expression-matcher@1.5.0: + resolution: {integrity: sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==} + engines: {node: '>=14.0.0'} + path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} @@ -2688,6 +2874,15 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} + pdf-parse@2.4.5: + resolution: {integrity: sha512-mHU89HGh7v+4u2ubfnevJ03lmPgQ5WU4CxAVmTSh/sxVTEDYd1er/dKS/A6vg77NX47KTEoihq8jZBLr8Cxuwg==} + engines: {node: '>=20.16.0 <21 || >=22.3.0'} + hasBin: true + + pdfjs-dist@5.4.296: + resolution: {integrity: sha512-DlOzet0HO7OEnmUmB6wWGJrrdvbyJKftI1bhMitK7O2N8W2gc757yyYBbINy9IDafXAV9wmKr9t7xsTaNKRG5Q==} + engines: {node: '>=20.16.0 || >=22.3.0'} + pend@1.2.0: resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} @@ -3066,6 +3261,10 @@ packages: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} + ssf@0.11.2: + resolution: {integrity: sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==} + engines: {node: '>=0.8'} + stack-trace@0.0.10: resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} @@ -3109,6 +3308,9 @@ packages: strnum@1.1.2: resolution: {integrity: sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==} + strnum@2.2.3: + resolution: {integrity: sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg==} + style-to-object@1.0.8: resolution: {integrity: sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g==} @@ -3406,10 +3608,18 @@ packages: resolution: {integrity: sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==} engines: {node: '>= 12.0.0'} + wmf@1.0.2: + resolution: {integrity: sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==} + engines: {node: '>=0.8'} + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + word@0.3.0: + resolution: {integrity: sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==} + engines: {node: '>=0.8'} + wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -3421,6 +3631,11 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + xlsx@0.18.5: + resolution: {integrity: sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==} + engines: {node: '>=0.8'} + hasBin: true + xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} @@ -3556,6 +3771,119 @@ snapshots: - immer - use-sync-external-store + '@azure/abort-controller@2.1.2': + dependencies: + tslib: 2.8.1 + + '@azure/core-auth@1.10.1': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-util': 1.13.1 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/core-client@1.10.1': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-auth': 1.10.1 + '@azure/core-rest-pipeline': 1.23.0 + '@azure/core-tracing': 1.3.1 + '@azure/core-util': 1.13.1 + '@azure/logger': 1.3.0 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/core-http-compat@2.4.0(@azure/core-client@1.10.1)(@azure/core-rest-pipeline@1.23.0)': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-client': 1.10.1 + '@azure/core-rest-pipeline': 1.23.0 + + '@azure/core-lro@2.7.2': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-util': 1.13.1 + '@azure/logger': 1.3.0 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/core-paging@1.6.2': + dependencies: + tslib: 2.8.1 + + '@azure/core-rest-pipeline@1.23.0': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-auth': 1.10.1 + '@azure/core-tracing': 1.3.1 + '@azure/core-util': 1.13.1 + '@azure/logger': 1.3.0 + '@typespec/ts-http-runtime': 0.3.5 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/core-tracing@1.3.1': + dependencies: + tslib: 2.8.1 + + '@azure/core-util@1.13.1': + dependencies: + '@azure/abort-controller': 2.1.2 + '@typespec/ts-http-runtime': 0.3.5 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/core-xml@1.5.1': + dependencies: + fast-xml-parser: 5.5.11 + tslib: 2.8.1 + + '@azure/logger@1.3.0': + dependencies: + '@typespec/ts-http-runtime': 0.3.5 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/storage-blob@12.31.0': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-auth': 1.10.1 + '@azure/core-client': 1.10.1 + '@azure/core-http-compat': 2.4.0(@azure/core-client@1.10.1)(@azure/core-rest-pipeline@1.23.0) + '@azure/core-lro': 2.7.2 + '@azure/core-paging': 1.6.2 + '@azure/core-rest-pipeline': 1.23.0 + '@azure/core-tracing': 1.3.1 + '@azure/core-util': 1.13.1 + '@azure/core-xml': 1.5.1 + '@azure/logger': 1.3.0 + '@azure/storage-common': 12.3.0(@azure/core-client@1.10.1) + events: 3.3.0 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/storage-common@12.3.0(@azure/core-client@1.10.1)': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-auth': 1.10.1 + '@azure/core-http-compat': 2.4.0(@azure/core-client@1.10.1)(@azure/core-rest-pipeline@1.23.0) + '@azure/core-rest-pipeline': 1.23.0 + '@azure/core-tracing': 1.3.1 + '@azure/core-util': 1.13.1 + '@azure/logger': 1.3.0 + events: 3.3.0 + tslib: 2.8.1 + transitivePeerDependencies: + - '@azure/core-client' + - supports-color + '@babel/code-frame@7.26.2': dependencies: '@babel/helper-validator-identifier': 7.25.9 @@ -4026,6 +4354,49 @@ snapshots: - encoding - ws + '@napi-rs/canvas-android-arm64@0.1.80': + optional: true + + '@napi-rs/canvas-darwin-arm64@0.1.80': + optional: true + + '@napi-rs/canvas-darwin-x64@0.1.80': + optional: true + + '@napi-rs/canvas-linux-arm-gnueabihf@0.1.80': + optional: true + + '@napi-rs/canvas-linux-arm64-gnu@0.1.80': + optional: true + + '@napi-rs/canvas-linux-arm64-musl@0.1.80': + optional: true + + '@napi-rs/canvas-linux-riscv64-gnu@0.1.80': + optional: true + + '@napi-rs/canvas-linux-x64-gnu@0.1.80': + optional: true + + '@napi-rs/canvas-linux-x64-musl@0.1.80': + optional: true + + '@napi-rs/canvas-win32-x64-msvc@0.1.80': + optional: true + + '@napi-rs/canvas@0.1.80': + optionalDependencies: + '@napi-rs/canvas-android-arm64': 0.1.80 + '@napi-rs/canvas-darwin-arm64': 0.1.80 + '@napi-rs/canvas-darwin-x64': 0.1.80 + '@napi-rs/canvas-linux-arm-gnueabihf': 0.1.80 + '@napi-rs/canvas-linux-arm64-gnu': 0.1.80 + '@napi-rs/canvas-linux-arm64-musl': 0.1.80 + '@napi-rs/canvas-linux-riscv64-gnu': 0.1.80 + '@napi-rs/canvas-linux-x64-gnu': 0.1.80 + '@napi-rs/canvas-linux-x64-musl': 0.1.80 + '@napi-rs/canvas-win32-x64-msvc': 0.1.80 + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -4511,6 +4882,10 @@ snapshots: dependencies: undici-types: 6.20.0 + '@types/pdf-parse@1.1.5': + dependencies: + '@types/node': 22.13.5 + '@types/prismjs@1.26.5': {} '@types/react-dom@19.0.4(@types/react@19.0.10)': @@ -4624,6 +4999,14 @@ snapshots: transitivePeerDependencies: - supports-color + '@typespec/ts-http-runtime@0.3.5': + dependencies: + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + '@ungap/structured-clone@1.3.0': {} '@vitejs/plugin-react@4.3.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))': @@ -4647,6 +5030,10 @@ snapshots: acorn@8.14.0: {} + adler-32@1.3.1: {} + + agent-base@7.1.4: {} + agentkeepalive@4.6.0: dependencies: humanize-ms: 1.2.1 @@ -4740,6 +5127,11 @@ snapshots: ccount@2.0.1: {} + cfb@1.2.2: + dependencies: + adler-32: 1.3.1 + crc-32: 1.2.2 + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -4773,6 +5165,8 @@ snapshots: clsx@2.1.1: {} + codepage@1.15.0: {} + color-convert@1.9.3: dependencies: color-name: 1.1.3 @@ -4828,6 +5222,8 @@ snapshots: dependencies: is-what: 4.1.16 + crc-32@1.2.2: {} + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -5099,6 +5495,8 @@ snapshots: eventemitter3@4.0.7: {} + events@3.3.0: {} + execa@9.5.2: dependencies: '@sindresorhus/merge-streams': 4.0.0 @@ -5144,10 +5542,20 @@ snapshots: fast-levenshtein@2.0.6: {} + fast-xml-builder@1.1.4: + dependencies: + path-expression-matcher: 1.5.0 + fast-xml-parser@4.5.3: dependencies: strnum: 1.1.2 + fast-xml-parser@5.5.11: + dependencies: + fast-xml-builder: 1.1.4 + path-expression-matcher: 1.5.0 + strnum: 2.2.3 + fastq@1.19.1: dependencies: reusify: 1.1.0 @@ -5209,6 +5617,8 @@ snapshots: node-domexception: 1.0.0 web-streams-polyfill: 4.0.0-beta.3 + frac@1.1.2: {} + fraction.js@4.3.7: {} framer-motion@12.4.9(react-dom@19.0.0(react@19.0.0))(react@19.0.0): @@ -5403,6 +5813,20 @@ snapshots: html-url-attributes@3.0.1: {} + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.0 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.0 + transitivePeerDependencies: + - supports-color + human-signals@8.0.0: {} humanize-ms@1.2.1: @@ -6170,6 +6594,8 @@ snapshots: path-exists@4.0.0: {} + path-expression-matcher@1.5.0: {} + path-key@3.1.1: {} path-key@4.0.0: {} @@ -6179,6 +6605,15 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.2 + pdf-parse@2.4.5: + dependencies: + '@napi-rs/canvas': 0.1.80 + pdfjs-dist: 5.4.296 + + pdfjs-dist@5.4.296: + optionalDependencies: + '@napi-rs/canvas': 0.1.80 + pend@1.2.0: {} pg-cloudflare@1.3.0: @@ -6609,6 +7044,10 @@ snapshots: split2@4.2.0: {} + ssf@0.11.2: + dependencies: + frac: 1.1.2 + stack-trace@0.0.10: {} stacktrace-parser@0.1.11: @@ -6652,6 +7091,8 @@ snapshots: strnum@1.1.2: {} + strnum@2.2.3: {} + style-to-object@1.0.8: dependencies: inline-style-parser: 0.2.4 @@ -6936,8 +7377,12 @@ snapshots: triple-beam: 1.4.1 winston-transport: 4.9.0 + wmf@1.0.2: {} + word-wrap@1.2.5: {} + word@0.3.0: {} + wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 @@ -6952,6 +7397,16 @@ snapshots: wrappy@1.0.2: {} + xlsx@0.18.5: + dependencies: + adler-32: 1.3.1 + cfb: 1.2.2 + codepage: 1.15.0 + crc-32: 1.2.2 + ssf: 0.11.2 + wmf: 1.0.2 + word: 0.3.0 + xtend@4.0.2: {} yallist@3.1.1: {} diff --git a/langgraph/src/agent/supervisor/nodes/general-input.ts b/langgraph/src/agent/supervisor/nodes/general-input.ts index f848ab8..ada60e3 100644 --- a/langgraph/src/agent/supervisor/nodes/general-input.ts +++ b/langgraph/src/agent/supervisor/nodes/general-input.ts @@ -1,6 +1,7 @@ import { SupervisorState, SupervisorUpdate } from "../types"; import { ALL_TOOL_DESCRIPTIONS } from "../index"; import { createLlm, type ModelMode } from "@/agent/utils/create-llm"; +import { truncateMessages } from "@/agent/utils/truncate-messages"; import { LangGraphRunnableConfig } from "@langchain/langgraph"; export async function generalInput( @@ -17,12 +18,13 @@ Otherwise, just answer as normal.`; const modelMode = ((config.configurable?.modelMode as string) ?? "auto") as ModelMode; const llm = createLlm({ modelMode }); + const truncated = truncateMessages(state.messages); const response = await llm.invoke([ { role: "system", content: GENERAL_INPUT_SYSTEM_PROMPT, }, - ...state.messages, + ...truncated, ]); return { diff --git a/langgraph/src/agent/supervisor/nodes/router.ts b/langgraph/src/agent/supervisor/nodes/router.ts index d510410..54ce6f2 100644 --- a/langgraph/src/agent/supervisor/nodes/router.ts +++ b/langgraph/src/agent/supervisor/nodes/router.ts @@ -3,6 +3,7 @@ import { ALL_TOOL_DESCRIPTIONS } from "../index"; import { SupervisorState, SupervisorUpdate } from "../types"; import { formatMessages } from "@/agent/utils/format-messages"; import { createLlm } from "@/agent/utils/create-llm"; +import { truncateMessages } from "@/agent/utils/truncate-messages"; export async function router( state: SupervisorState, @@ -43,8 +44,9 @@ Routing rules: IMPORTANT: "写代码" (write code to execute) → coder, NOT writer. "写文档/写报告" (write a document) → writer. The key distinction is whether the user wants code EXECUTED or a document CREATED.`; - const allMessagesButLast = state.messages.slice(0, -1); - const lastMessage = state.messages.at(-1); + const truncated = truncateMessages(state.messages); + const allMessagesButLast = truncated.slice(0, -1); + const lastMessage = truncated.at(-1); const formattedPreviousMessages = formatMessages(allMessagesButLast); const formattedLastMessage = lastMessage ? formatMessages([lastMessage]) : ""; diff --git a/langgraph/src/agent/utils/create-llm.ts b/langgraph/src/agent/utils/create-llm.ts index 8cae5b9..cf882c4 100644 --- a/langgraph/src/agent/utils/create-llm.ts +++ b/langgraph/src/agent/utils/create-llm.ts @@ -6,9 +6,9 @@ const MODEL_PRESETS: Record< ModelMode, { temperature: number; maxTokens: number } > = { - flash: { temperature: 0.2, maxTokens: 500 }, - pro: { temperature: 0.3, maxTokens: 4096 }, - auto: { temperature: 0.2, maxTokens: 2048 }, + flash: { temperature: 0.2, maxTokens: 2000 }, + pro: { temperature: 0.3, maxTokens: 8192 }, + auto: { temperature: 0.2, maxTokens: 4096 }, }; /** diff --git a/langgraph/src/agent/utils/file-service.ts b/langgraph/src/agent/utils/file-service.ts new file mode 100644 index 0000000..2a3aad4 --- /dev/null +++ b/langgraph/src/agent/utils/file-service.ts @@ -0,0 +1,105 @@ +import { + BlobServiceClient, + StorageSharedKeyCredential, +} from "@azure/storage-blob"; +import pdfParse from "pdf-parse"; +import * as XLSX from "xlsx"; +import { randomUUID } from "crypto"; + +const CONTAINER_NAME = "chat-uploads"; + +export type ParsedFile = + | { type: "image"; content: string; url: string } + | { type: "document"; content: string; url: string }; + +function getBlobServiceClient(): BlobServiceClient { + const connectionString = process.env.AZURE_STORAGE_CONNECTION_STRING; + if (!connectionString) { + throw new Error( + "AZURE_STORAGE_CONNECTION_STRING environment variable is not set", + ); + } + return BlobServiceClient.fromConnectionString(connectionString); +} + +/** + * Upload a buffer to Azure Blob Storage and return the public URL. + */ +export async function uploadToBlob( + buffer: Buffer, + filename: string, + mimeType: string, +): Promise { + const blobServiceClient = getBlobServiceClient(); + const containerClient = blobServiceClient.getContainerClient(CONTAINER_NAME); + + // Create container if it doesn't exist (with public blob access for image URLs) + await containerClient.createIfNotExists({ access: "blob" }); + + // Generate a unique blob name to avoid collisions + const ext = filename.includes(".") ? filename.split(".").pop() : ""; + const blobName = `${randomUUID()}${ext ? `.${ext}` : ""}`; + + const blockBlobClient = containerClient.getBlockBlobClient(blobName); + await blockBlobClient.uploadData(buffer, { + blobHTTPHeaders: { blobContentType: mimeType }, + }); + + return blockBlobClient.url; +} + +const IMAGE_MIME_TYPES = new Set([ + "image/jpeg", + "image/png", + "image/gif", + "image/webp", +]); + +/** + * Parse a file buffer into a structured result. + * - Images: upload to Blob, return URL (for multimodal LLM) + * - PDF: extract text with pdf-parse + * - Excel: convert to CSV with xlsx + * - Other: treat as text + */ +export async function parseFile( + buffer: Buffer, + mimeType: string, + filename: string, +): Promise { + // Images — upload and return URL for vision + if (IMAGE_MIME_TYPES.has(mimeType)) { + const url = await uploadToBlob(buffer, filename, mimeType); + return { type: "image", content: "", url }; + } + + // PDF + if (mimeType === "application/pdf") { + const data = await pdfParse(buffer); + const url = await uploadToBlob(buffer, filename, mimeType); + return { type: "document", content: data.text, url }; + } + + // Excel + if ( + mimeType === + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" || + mimeType === "application/vnd.ms-excel" + ) { + const workbook = XLSX.read(buffer, { type: "buffer" }); + const csvParts: string[] = []; + for (const sheetName of workbook.SheetNames) { + const sheet = workbook.Sheets[sheetName]; + csvParts.push(`--- Sheet: ${sheetName} ---`); + csvParts.push(XLSX.utils.sheet_to_csv(sheet)); + } + const content = csvParts.join("\n"); + const url = await uploadToBlob(buffer, filename, mimeType); + return { type: "document", content, url }; + } + + // Other text files + const content = buffer.toString("utf-8"); + const url = await uploadToBlob(buffer, filename, mimeType); + return { type: "document", content, url }; +} diff --git a/langgraph/src/agent/utils/truncate-messages.ts b/langgraph/src/agent/utils/truncate-messages.ts new file mode 100644 index 0000000..54016be --- /dev/null +++ b/langgraph/src/agent/utils/truncate-messages.ts @@ -0,0 +1,87 @@ +import { BaseMessage, SystemMessage, HumanMessage } from "@langchain/core/messages"; + +const MAX_RECENT_MESSAGES = 20; + +/** + * Estimate token count using a simple character-based heuristic. + * ~4 chars per token for English, ~2 chars per token for Chinese. + * We use ~3 as a rough average for mixed content. + */ +function estimateTokens(message: BaseMessage): number { + const content = typeof message.content === "string" + ? message.content + : JSON.stringify(message.content); + return Math.ceil(content.length / 3); +} + +/** + * Truncate messages to fit within a token budget. + * Strategy: + * - Always keep system messages (if any) + * - Keep the most recent N messages + * - If the history exceeds maxTokens, drop older messages and inject a summary note + */ +export function truncateMessages( + messages: BaseMessage[], + maxTokens = 60000, +): BaseMessage[] { + if (messages.length === 0) return messages; + + // Separate system messages from the rest + const systemMessages = messages.filter((m) => m instanceof SystemMessage); + const nonSystemMessages = messages.filter((m) => !(m instanceof SystemMessage)); + + // Calculate total tokens + const totalTokens = messages.reduce((sum, m) => sum + estimateTokens(m), 0); + + // If within budget, return as-is + if (totalTokens <= maxTokens) { + return messages; + } + + // Keep only recent messages + const recentMessages = nonSystemMessages.slice(-MAX_RECENT_MESSAGES); + + // Check if recent messages alone fit + const recentTokens = + systemMessages.reduce((sum, m) => sum + estimateTokens(m), 0) + + recentMessages.reduce((sum, m) => sum + estimateTokens(m), 0); + + if (recentTokens <= maxTokens) { + const droppedCount = nonSystemMessages.length - recentMessages.length; + if (droppedCount > 0) { + const summaryNote = new HumanMessage( + `[System note: ${droppedCount} earlier messages were truncated to save context space.]`, + ); + return [...systemMessages, summaryNote, ...recentMessages]; + } + return [...systemMessages, ...recentMessages]; + } + + // Still too large — progressively trim from the front of recent messages + const result: BaseMessage[] = [...systemMessages]; + let budget = maxTokens - systemMessages.reduce((sum, m) => sum + estimateTokens(m), 0); + + const trimmed: BaseMessage[] = []; + for (let i = recentMessages.length - 1; i >= 0; i--) { + const tokens = estimateTokens(recentMessages[i]); + if (budget - tokens >= 0) { + trimmed.unshift(recentMessages[i]); + budget -= tokens; + } else { + break; + } + } + + const droppedCount = nonSystemMessages.length - trimmed.length; + if (droppedCount > 0) { + result.push( + new HumanMessage( + `[System note: ${droppedCount} earlier messages were truncated to save context space.]`, + ), + ); + } + result.push(...trimmed); + + return result; +}