feat: runnersok
This commit is contained in:
438
backend/package-lock.json
generated
438
backend/package-lock.json
generated
@@ -10,10 +10,12 @@
|
||||
"dependencies": {
|
||||
"@ai-sdk/openai": "^1.0.0",
|
||||
"ai": "^4.0.0",
|
||||
"better-sqlite3": "^12.8.0",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.21.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/better-sqlite3": "^7.6.13",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/node": "^22.0.0",
|
||||
@@ -561,6 +563,16 @@
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/better-sqlite3": {
|
||||
"version": "7.6.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz",
|
||||
"integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/body-parser": {
|
||||
"version": "1.19.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
|
||||
@@ -720,6 +732,60 @@
|
||||
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/base64-js": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/better-sqlite3": {
|
||||
"version": "12.8.0",
|
||||
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.8.0.tgz",
|
||||
"integrity": "sha512-RxD2Vd96sQDjQr20kdP+F+dK/1OUNiVOl200vKBZY8u0vTwysfolF6Hq+3ZK2+h8My9YvZhHsF+RSGZW2VYrPQ==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bindings": "^1.5.0",
|
||||
"prebuild-install": "^7.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20.x || 22.x || 23.x || 24.x || 25.x"
|
||||
}
|
||||
},
|
||||
"node_modules/bindings": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
|
||||
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"file-uri-to-path": "1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bl": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
|
||||
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer": "^5.5.0",
|
||||
"inherits": "^2.0.4",
|
||||
"readable-stream": "^3.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "1.20.4",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
|
||||
@@ -744,6 +810,30 @@
|
||||
"npm": "1.2.8000 || >= 1.4.16"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
||||
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.1.13"
|
||||
}
|
||||
},
|
||||
"node_modules/bytes": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||
@@ -794,6 +884,12 @@
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/chownr": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
|
||||
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/content-disposition": {
|
||||
"version": "0.5.4",
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
|
||||
@@ -856,6 +952,30 @@
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/decompress-response": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
|
||||
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mimic-response": "^3.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/deep-extend": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
|
||||
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/depd": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||
@@ -884,6 +1004,15 @@
|
||||
"npm": "1.2.8000 || >= 1.4.16"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/diff-match-patch": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz",
|
||||
@@ -919,6 +1048,15 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/end-of-stream": {
|
||||
"version": "1.4.5",
|
||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
|
||||
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"once": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
@@ -1006,6 +1144,15 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/expand-template": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
|
||||
"integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
|
||||
"license": "(MIT OR WTFPL)",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "4.22.1",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
|
||||
@@ -1052,6 +1199,12 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/file-uri-to-path": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
|
||||
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/finalhandler": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
|
||||
@@ -1088,6 +1241,12 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/fs-constants": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
|
||||
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
@@ -1162,6 +1321,12 @@
|
||||
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/github-from-package": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
|
||||
"integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
@@ -1230,12 +1395,38 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ieee754": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ini": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
|
||||
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ipaddr.js": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||
@@ -1337,6 +1528,33 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mimic-response": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
|
||||
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/mkdirp-classic": {
|
||||
"version": "0.5.3",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
|
||||
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
@@ -1361,6 +1579,12 @@
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/napi-build-utils": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
|
||||
"integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/negotiator": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
||||
@@ -1370,6 +1594,18 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/node-abi": {
|
||||
"version": "3.89.0",
|
||||
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.89.0.tgz",
|
||||
"integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"semver": "^7.3.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
@@ -1403,6 +1639,15 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/parseurl": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||
@@ -1418,6 +1663,33 @@
|
||||
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/prebuild-install": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
|
||||
"integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
|
||||
"deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.0",
|
||||
"expand-template": "^2.0.3",
|
||||
"github-from-package": "0.0.0",
|
||||
"minimist": "^1.2.3",
|
||||
"mkdirp-classic": "^0.5.3",
|
||||
"napi-build-utils": "^2.0.0",
|
||||
"node-abi": "^3.3.0",
|
||||
"pump": "^3.0.0",
|
||||
"rc": "^1.2.7",
|
||||
"simple-get": "^4.0.0",
|
||||
"tar-fs": "^2.0.0",
|
||||
"tunnel-agent": "^0.6.0"
|
||||
},
|
||||
"bin": {
|
||||
"prebuild-install": "bin.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-addr": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
@@ -1431,6 +1703,16 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/pump": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
|
||||
"integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"end-of-stream": "^1.1.0",
|
||||
"once": "^1.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.14.2",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
|
||||
@@ -1470,6 +1752,21 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/rc": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
|
||||
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
|
||||
"license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
|
||||
"dependencies": {
|
||||
"deep-extend": "^0.6.0",
|
||||
"ini": "~1.3.0",
|
||||
"minimist": "^1.2.0",
|
||||
"strip-json-comments": "~2.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"rc": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "19.2.4",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
|
||||
@@ -1480,6 +1777,20 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/readable-stream": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
"util-deprecate": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve-pkg-maps": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
|
||||
@@ -1522,6 +1833,18 @@
|
||||
"integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.7.4",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
|
||||
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/send": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
|
||||
@@ -1645,6 +1968,51 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/simple-concat": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
|
||||
"integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/simple-get": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
|
||||
"integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"decompress-response": "^6.0.0",
|
||||
"once": "^1.3.1",
|
||||
"simple-concat": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/statuses": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||
@@ -1654,6 +2022,24 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/string_decoder": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-json-comments": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
|
||||
"integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/swr": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/swr/-/swr-2.4.1.tgz",
|
||||
@@ -1667,6 +2053,34 @@
|
||||
"react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tar-fs": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz",
|
||||
"integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chownr": "^1.1.1",
|
||||
"mkdirp-classic": "^0.5.2",
|
||||
"pump": "^3.0.0",
|
||||
"tar-stream": "^2.1.4"
|
||||
}
|
||||
},
|
||||
"node_modules/tar-stream": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
|
||||
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bl": "^4.0.3",
|
||||
"end-of-stream": "^1.4.1",
|
||||
"fs-constants": "^1.0.0",
|
||||
"inherits": "^2.0.3",
|
||||
"readable-stream": "^3.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/throttleit": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/throttleit/-/throttleit-2.1.0.tgz",
|
||||
@@ -1708,6 +2122,18 @@
|
||||
"fsevents": "~2.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/tunnel-agent": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
|
||||
"integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"safe-buffer": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/type-is": {
|
||||
"version": "1.6.18",
|
||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
|
||||
@@ -1760,6 +2186,12 @@
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/utils-merge": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
||||
@@ -1778,6 +2210,12 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "3.25.76",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||
|
||||
@@ -14,12 +14,14 @@
|
||||
"node": ">=20"
|
||||
},
|
||||
"dependencies": {
|
||||
"ai": "^4.0.0",
|
||||
"@ai-sdk/openai": "^1.0.0",
|
||||
"ai": "^4.0.0",
|
||||
"better-sqlite3": "^12.8.0",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.21.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/better-sqlite3": "^7.6.13",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/node": "^22.0.0",
|
||||
|
||||
69
backend/src/db/connection.ts
Normal file
69
backend/src/db/connection.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* SQLite database connection singleton.
|
||||
* Uses better-sqlite3 for synchronous, fast access.
|
||||
* DB_PATH env var controls location; defaults to ./data/zui.db
|
||||
*/
|
||||
|
||||
import Database from 'better-sqlite3'
|
||||
import path from 'node:path'
|
||||
import fs from 'node:fs'
|
||||
|
||||
const DB_PATH = process.env.DB_PATH || path.resolve('data', 'zui.db')
|
||||
|
||||
let db: Database.Database | null = null
|
||||
|
||||
export function getDb(): Database.Database {
|
||||
if (db) return db
|
||||
|
||||
// Ensure parent directory exists
|
||||
const dir = path.dirname(DB_PATH)
|
||||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })
|
||||
|
||||
db = new Database(DB_PATH)
|
||||
db.pragma('journal_mode = WAL')
|
||||
db.pragma('foreign_keys = ON')
|
||||
|
||||
initSchema(db)
|
||||
return db
|
||||
}
|
||||
|
||||
function initSchema(db: Database.Database): void {
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS runs (
|
||||
id TEXT PRIMARY KEY,
|
||||
recollectionId TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
graphSnapshot TEXT NOT NULL,
|
||||
createdAt TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updatedAt TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
error TEXT
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_runs_recollection
|
||||
ON runs(recollectionId, createdAt DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS run_steps (
|
||||
id TEXT PRIMARY KEY,
|
||||
runId TEXT NOT NULL REFERENCES runs(id) ON DELETE CASCADE,
|
||||
nodeId TEXT NOT NULL,
|
||||
nodeType TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
input TEXT,
|
||||
output TEXT,
|
||||
error TEXT,
|
||||
startedAt TEXT,
|
||||
endedAt TEXT,
|
||||
sortOrder INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_run_steps_run
|
||||
ON run_steps(runId, sortOrder);
|
||||
`)
|
||||
}
|
||||
|
||||
export function closeDb(): void {
|
||||
if (db) {
|
||||
db.close()
|
||||
db = null
|
||||
}
|
||||
}
|
||||
@@ -61,6 +61,8 @@ app.use(express.json())
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
import { handleAgentRequest, handleAgentStreamRequest } from './routes/agentRoutes.js'
|
||||
import { handleCreateRun, handleRunStream, handleGetRun, handleListRuns } from './routes/runRoutes.js'
|
||||
import { getDb } from './db/connection.js'
|
||||
|
||||
/** POST /api/agent - Run AI agent */
|
||||
app.post('/api/agent', handleAgentRequest)
|
||||
@@ -68,6 +70,22 @@ app.post('/api/agent', handleAgentRequest)
|
||||
/** POST /api/agent/stream - Stream AI agent response */
|
||||
app.post('/api/agent/stream', handleAgentStreamRequest)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Run Routes
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** POST /api/runs - Create and queue a graph execution run */
|
||||
app.post('/api/runs', handleCreateRun)
|
||||
|
||||
/** GET /api/runs/:id/stream - SSE stream of run execution events */
|
||||
app.get('/api/runs/:id/stream', handleRunStream)
|
||||
|
||||
/** GET /api/runs/:id - Get run details with steps */
|
||||
app.get('/api/runs/:id', handleGetRun)
|
||||
|
||||
/** GET /api/recollections/:recollectionId/runs - List runs for a recollection */
|
||||
app.get('/api/recollections/:recollectionId/runs', handleListRuns)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Health Check Endpoint
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -91,6 +109,9 @@ app.use((err: Error, req: express.Request, res: express.Response, next: express.
|
||||
// Start Server
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Initialize database on startup
|
||||
getDb()
|
||||
|
||||
app.listen(PORT, '0.0.0.0', () => {
|
||||
console.log(`Backend listening on port ${PORT} (CORS: ${CORS_ORIGIN})`)
|
||||
})
|
||||
|
||||
67
backend/src/models/run.ts
Normal file
67
backend/src/models/run.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Run and RunStep domain types for graph execution.
|
||||
*/
|
||||
|
||||
export type RunStatus = 'pending' | 'running' | 'completed' | 'failed'
|
||||
|
||||
export type Run = {
|
||||
id: string
|
||||
recollectionId: string
|
||||
status: RunStatus
|
||||
graphSnapshot: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
error?: string | null
|
||||
}
|
||||
|
||||
export type RunStep = {
|
||||
id: string
|
||||
runId: string
|
||||
nodeId: string
|
||||
nodeType: string
|
||||
status: RunStatus
|
||||
input?: string | null
|
||||
output?: string | null
|
||||
error?: string | null
|
||||
startedAt?: string | null
|
||||
endedAt?: string | null
|
||||
sortOrder: number
|
||||
}
|
||||
|
||||
/** SSE event types for run streaming */
|
||||
export type RunEventType =
|
||||
| 'connected'
|
||||
| 'run/started'
|
||||
| 'run/completed'
|
||||
| 'run/failed'
|
||||
| 'step/started'
|
||||
| 'step/completed'
|
||||
| 'step/failed'
|
||||
| 'step/chunk'
|
||||
| 'ping'
|
||||
|
||||
export type RunEvent = {
|
||||
type: RunEventType
|
||||
data: Record<string, unknown>
|
||||
}
|
||||
|
||||
/** Graph snapshot types (subset of frontend StoredGraphState) */
|
||||
export type GraphNode = {
|
||||
id: string
|
||||
type?: string
|
||||
data?: Record<string, unknown>
|
||||
position?: { x: number; y: number }
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export type GraphEdge = {
|
||||
id: string
|
||||
source: string
|
||||
target: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export type GraphSnapshot = {
|
||||
nodes: GraphNode[]
|
||||
edges: GraphEdge[]
|
||||
}
|
||||
62
backend/src/repositories/runRepository.ts
Normal file
62
backend/src/repositories/runRepository.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* Data access layer for runs and run_steps.
|
||||
*/
|
||||
|
||||
import { getDb } from '../db/connection.js'
|
||||
import type { Run, RunStep, RunStatus } from '../models/run.js'
|
||||
|
||||
export function createRun(run: Run): void {
|
||||
const db = getDb()
|
||||
db.prepare(`
|
||||
INSERT INTO runs (id, recollectionId, status, graphSnapshot, createdAt, updatedAt, error)
|
||||
VALUES (@id, @recollectionId, @status, @graphSnapshot, @createdAt, @updatedAt, @error)
|
||||
`).run(run)
|
||||
}
|
||||
|
||||
export function getRun(id: string): Run | undefined {
|
||||
const db = getDb()
|
||||
return db.prepare('SELECT * FROM runs WHERE id = ?').get(id) as Run | undefined
|
||||
}
|
||||
|
||||
export function updateRunStatus(id: string, status: RunStatus, error?: string): void {
|
||||
const db = getDb()
|
||||
db.prepare(`
|
||||
UPDATE runs SET status = ?, error = ?, updatedAt = datetime('now') WHERE id = ?
|
||||
`).run(status, error ?? null, id)
|
||||
}
|
||||
|
||||
export function getRunsByRecollection(recollectionId: string, limit = 50, offset = 0): Run[] {
|
||||
const db = getDb()
|
||||
return db.prepare(`
|
||||
SELECT * FROM runs WHERE recollectionId = ? ORDER BY createdAt DESC LIMIT ? OFFSET ?
|
||||
`).all(recollectionId, limit, offset) as Run[]
|
||||
}
|
||||
|
||||
export function createRunStep(step: RunStep): void {
|
||||
const db = getDb()
|
||||
db.prepare(`
|
||||
INSERT INTO run_steps (id, runId, nodeId, nodeType, status, input, output, error, startedAt, endedAt, sortOrder)
|
||||
VALUES (@id, @runId, @nodeId, @nodeType, @status, @input, @output, @error, @startedAt, @endedAt, @sortOrder)
|
||||
`).run(step)
|
||||
}
|
||||
|
||||
export function updateRunStep(id: string, updates: Partial<Pick<RunStep, 'status' | 'output' | 'error' | 'startedAt' | 'endedAt'>>): void {
|
||||
const db = getDb()
|
||||
const fields: string[] = []
|
||||
const values: unknown[] = []
|
||||
|
||||
if (updates.status !== undefined) { fields.push('status = ?'); values.push(updates.status) }
|
||||
if (updates.output !== undefined) { fields.push('output = ?'); values.push(updates.output) }
|
||||
if (updates.error !== undefined) { fields.push('error = ?'); values.push(updates.error) }
|
||||
if (updates.startedAt !== undefined) { fields.push('startedAt = ?'); values.push(updates.startedAt) }
|
||||
if (updates.endedAt !== undefined) { fields.push('endedAt = ?'); values.push(updates.endedAt) }
|
||||
|
||||
if (fields.length === 0) return
|
||||
values.push(id)
|
||||
db.prepare(`UPDATE run_steps SET ${fields.join(', ')} WHERE id = ?`).run(...values)
|
||||
}
|
||||
|
||||
export function getRunSteps(runId: string): RunStep[] {
|
||||
const db = getDb()
|
||||
return db.prepare('SELECT * FROM run_steps WHERE runId = ? ORDER BY sortOrder').all(runId) as RunStep[]
|
||||
}
|
||||
127
backend/src/routes/runRoutes.ts
Normal file
127
backend/src/routes/runRoutes.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
/**
|
||||
* Run routes: create runs, stream execution, list history.
|
||||
*/
|
||||
|
||||
import crypto from 'node:crypto'
|
||||
import type { Request, Response } from 'express'
|
||||
import type { Run, RunEvent } from '../models/run.js'
|
||||
import * as runRepo from '../repositories/runRepository.js'
|
||||
import { executeRun } from '../services/graphRunnerService.js'
|
||||
|
||||
/**
|
||||
* POST /api/runs
|
||||
* Body: { recollectionId, graph: { nodes, edges } }
|
||||
* Creates a new run and starts execution, returning the run ID immediately.
|
||||
* Client should connect to GET /api/runs/:id/stream for live updates.
|
||||
*/
|
||||
export async function handleCreateRun(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { recollectionId, graph } = req.body
|
||||
|
||||
if (!recollectionId || typeof recollectionId !== 'string') {
|
||||
res.status(400).json({ error: 'recollectionId is required' })
|
||||
return
|
||||
}
|
||||
if (!graph || !Array.isArray(graph.nodes) || !Array.isArray(graph.edges)) {
|
||||
res.status(400).json({ error: 'graph with nodes and edges is required' })
|
||||
return
|
||||
}
|
||||
|
||||
const run: Run = {
|
||||
id: crypto.randomUUID(),
|
||||
recollectionId,
|
||||
status: 'pending',
|
||||
graphSnapshot: JSON.stringify(graph),
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
error: null,
|
||||
}
|
||||
|
||||
runRepo.createRun(run)
|
||||
res.status(201).json({ id: run.id, status: run.status })
|
||||
} catch (err) {
|
||||
console.error('Create run error:', err)
|
||||
res.status(500).json({ error: (err as Error).message ?? 'Failed to create run' })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/runs/:id/stream
|
||||
* SSE endpoint that executes the run and streams events in real time.
|
||||
*/
|
||||
export async function handleRunStream(req: Request, res: Response): Promise<void> {
|
||||
const { id } = req.params
|
||||
|
||||
const run = runRepo.getRun(id)
|
||||
if (!run) {
|
||||
res.status(404).json({ error: 'Run not found' })
|
||||
return
|
||||
}
|
||||
|
||||
if (run.status !== 'pending') {
|
||||
res.status(409).json({ error: `Run already ${run.status}` })
|
||||
return
|
||||
}
|
||||
|
||||
// Set up SSE headers
|
||||
res.setHeader('Content-Type', 'text/event-stream')
|
||||
res.setHeader('Cache-Control', 'no-cache')
|
||||
res.setHeader('Connection', 'keep-alive')
|
||||
res.setHeader('X-Accel-Buffering', 'no')
|
||||
res.flushHeaders()
|
||||
|
||||
const emit = (event: RunEvent) => {
|
||||
res.write(`event: ${event.type}\ndata: ${JSON.stringify(event.data)}\n\n`)
|
||||
}
|
||||
|
||||
emit({ type: 'connected', data: { runId: id } })
|
||||
|
||||
// Keepalive ping
|
||||
const pingInterval = setInterval(() => {
|
||||
emit({ type: 'ping', data: { timestamp: Date.now() } })
|
||||
}, 15_000)
|
||||
|
||||
req.on('close', () => {
|
||||
clearInterval(pingInterval)
|
||||
})
|
||||
|
||||
try {
|
||||
await executeRun(id, emit)
|
||||
} catch (err) {
|
||||
const errorMsg = err instanceof Error ? err.message : String(err)
|
||||
emit({ type: 'run/failed', data: { runId: id, error: errorMsg } })
|
||||
runRepo.updateRunStatus(id, 'failed', errorMsg)
|
||||
} finally {
|
||||
clearInterval(pingInterval)
|
||||
res.end()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/runs/:id
|
||||
* Returns run details with steps.
|
||||
*/
|
||||
export async function handleGetRun(req: Request, res: Response): Promise<void> {
|
||||
const { id } = req.params
|
||||
const run = runRepo.getRun(id)
|
||||
if (!run) {
|
||||
res.status(404).json({ error: 'Run not found' })
|
||||
return
|
||||
}
|
||||
|
||||
const steps = runRepo.getRunSteps(id)
|
||||
res.json({ ...run, steps })
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/recollections/:recollectionId/runs
|
||||
* Returns run history for a recollection.
|
||||
*/
|
||||
export async function handleListRuns(req: Request, res: Response): Promise<void> {
|
||||
const { recollectionId } = req.params
|
||||
const limit = Math.min(Number(req.query.limit) || 50, 200)
|
||||
const offset = Number(req.query.offset) || 0
|
||||
|
||||
const runs = runRepo.getRunsByRecollection(recollectionId, limit, offset)
|
||||
res.json({ runs })
|
||||
}
|
||||
237
backend/src/services/graphRunnerService.ts
Normal file
237
backend/src/services/graphRunnerService.ts
Normal file
@@ -0,0 +1,237 @@
|
||||
/**
|
||||
* Graph execution engine.
|
||||
* Topologically sorts graph nodes (Kahn's algorithm) and executes them in order.
|
||||
* Emits SSE events via a callback for real-time streaming to clients.
|
||||
*/
|
||||
|
||||
import crypto from 'node:crypto'
|
||||
import type { GraphSnapshot, GraphNode, GraphEdge, RunEvent, RunStep, RunStatus } from '../models/run.js'
|
||||
import * as runRepo from '../repositories/runRepository.js'
|
||||
import { buildAgentRequest } from './agentService.js'
|
||||
|
||||
type EmitFn = (event: RunEvent) => void
|
||||
|
||||
/**
|
||||
* Topological sort using Kahn's algorithm.
|
||||
* Returns ordered node IDs or throws on cycle detection.
|
||||
*/
|
||||
function topologicalSort(nodes: GraphNode[], edges: GraphEdge[]): string[] {
|
||||
const nodeIds = new Set(nodes.map((n) => n.id))
|
||||
const inDegree = new Map<string, number>()
|
||||
const adjacency = new Map<string, string[]>()
|
||||
|
||||
for (const id of nodeIds) {
|
||||
inDegree.set(id, 0)
|
||||
adjacency.set(id, [])
|
||||
}
|
||||
|
||||
for (const edge of edges) {
|
||||
if (!nodeIds.has(edge.source) || !nodeIds.has(edge.target)) continue
|
||||
adjacency.get(edge.source)!.push(edge.target)
|
||||
inDegree.set(edge.target, (inDegree.get(edge.target) ?? 0) + 1)
|
||||
}
|
||||
|
||||
const queue: string[] = []
|
||||
for (const [id, deg] of inDegree) {
|
||||
if (deg === 0) queue.push(id)
|
||||
}
|
||||
|
||||
const sorted: string[] = []
|
||||
while (queue.length > 0) {
|
||||
const current = queue.shift()!
|
||||
sorted.push(current)
|
||||
for (const neighbor of adjacency.get(current) ?? []) {
|
||||
const newDeg = (inDegree.get(neighbor) ?? 1) - 1
|
||||
inDegree.set(neighbor, newDeg)
|
||||
if (newDeg === 0) queue.push(neighbor)
|
||||
}
|
||||
}
|
||||
|
||||
if (sorted.length !== nodeIds.size) {
|
||||
throw new Error('Cycle detected in graph')
|
||||
}
|
||||
|
||||
return sorted
|
||||
}
|
||||
|
||||
/** Node types that produce output when executed */
|
||||
const EXECUTABLE_TYPES = new Set(['agent', 'config', 'render'])
|
||||
|
||||
/** Node types that are pure inputs (no execution needed) */
|
||||
const INPUT_TYPES = new Set(['variable', 'data', 'function'])
|
||||
|
||||
/**
|
||||
* Execute a single node based on its type.
|
||||
* Returns the output string or null for input-only nodes.
|
||||
*/
|
||||
async function executeNode(
|
||||
node: GraphNode,
|
||||
_edges: GraphEdge[],
|
||||
_nodes: GraphNode[],
|
||||
_outputs: Map<string, string>,
|
||||
emit: EmitFn
|
||||
): Promise<string | null> {
|
||||
const nodeType = node.type ?? 'unknown'
|
||||
|
||||
if (INPUT_TYPES.has(nodeType)) {
|
||||
// Input nodes: extract their value as output
|
||||
const data = node.data ?? {}
|
||||
if (nodeType === 'variable') {
|
||||
const v = data.value
|
||||
return v === undefined || v === null ? '' : String(v)
|
||||
}
|
||||
if (nodeType === 'data') {
|
||||
const rows = (data.rows as Record<string, string>[]) ?? []
|
||||
return JSON.stringify(rows)
|
||||
}
|
||||
if (nodeType === 'function') {
|
||||
return (data.body as string) ?? ''
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
if (nodeType === 'agent') {
|
||||
// Agent nodes: call the AI service
|
||||
const configContents: string[] = []
|
||||
for (const edge of _edges) {
|
||||
if (edge.target !== node.id) continue
|
||||
const output = _outputs.get(edge.source)
|
||||
if (output) configContents.push(output)
|
||||
}
|
||||
|
||||
const prompt = configContents.join('\n\n---\n\n') || 'No prompt provided.'
|
||||
const connection = (node.data?.connection as Record<string, unknown>) ?? undefined
|
||||
|
||||
const built = buildAgentRequest({
|
||||
prompt,
|
||||
connection: connection as any,
|
||||
})
|
||||
|
||||
if ('error' in built) {
|
||||
throw new Error(built.error)
|
||||
}
|
||||
|
||||
// Use streaming for agent nodes
|
||||
const { streamText } = await import('ai')
|
||||
const result = streamText({
|
||||
model: built.openai as any,
|
||||
prompt: built.fullPrompt,
|
||||
})
|
||||
|
||||
let fullText = ''
|
||||
for await (const chunk of (await result).textStream) {
|
||||
fullText += chunk
|
||||
emit({
|
||||
type: 'step/chunk',
|
||||
data: { nodeId: node.id, chunk },
|
||||
})
|
||||
}
|
||||
|
||||
return fullText
|
||||
}
|
||||
|
||||
if (nodeType === 'config') {
|
||||
// Config nodes: return their template content as-is during execution
|
||||
// (Nunjucks resolution happens on the frontend; backend treats as passthrough)
|
||||
const content = (node.data?.content as string) ?? ''
|
||||
return content
|
||||
}
|
||||
|
||||
if (nodeType === 'render') {
|
||||
// Render nodes: collect input from connected source nodes
|
||||
const inputs: string[] = []
|
||||
for (const edge of _edges) {
|
||||
if (edge.target !== node.id) continue
|
||||
const output = _outputs.get(edge.source)
|
||||
if (output) inputs.push(output)
|
||||
}
|
||||
return inputs.join('\n\n')
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the full graph execution for a given run ID.
|
||||
* Emits SSE events for each step.
|
||||
*/
|
||||
export async function executeRun(runId: string, emit: EmitFn): Promise<void> {
|
||||
const run = runRepo.getRun(runId)
|
||||
if (!run) throw new Error(`Run not found: ${runId}`)
|
||||
|
||||
let graph: GraphSnapshot
|
||||
try {
|
||||
graph = JSON.parse(run.graphSnapshot) as GraphSnapshot
|
||||
} catch {
|
||||
throw new Error('Invalid graph snapshot')
|
||||
}
|
||||
|
||||
const { nodes, edges } = graph
|
||||
|
||||
// Sort nodes topologically
|
||||
const sortedIds = topologicalSort(nodes, edges)
|
||||
const nodeMap = new Map(nodes.map((n) => [n.id, n]))
|
||||
const outputs = new Map<string, string>()
|
||||
|
||||
// Create run_steps records
|
||||
const steps: RunStep[] = sortedIds.map((nodeId, i) => {
|
||||
const node = nodeMap.get(nodeId)!
|
||||
return {
|
||||
id: crypto.randomUUID(),
|
||||
runId,
|
||||
nodeId,
|
||||
nodeType: node.type ?? 'unknown',
|
||||
status: 'pending' as RunStatus,
|
||||
input: null,
|
||||
output: null,
|
||||
error: null,
|
||||
startedAt: null,
|
||||
endedAt: null,
|
||||
sortOrder: i,
|
||||
}
|
||||
})
|
||||
|
||||
for (const step of steps) {
|
||||
runRepo.createRunStep(step)
|
||||
}
|
||||
|
||||
// Mark run as running
|
||||
runRepo.updateRunStatus(runId, 'running')
|
||||
const stepNodeIds = steps.map((s) => s.nodeId)
|
||||
emit({ type: 'run/started', data: { runId, totalSteps: steps.length, nodeIds: stepNodeIds } })
|
||||
|
||||
// Execute each node in order
|
||||
for (const step of steps) {
|
||||
const node = nodeMap.get(step.nodeId)
|
||||
if (!node) continue
|
||||
|
||||
const now = new Date().toISOString()
|
||||
runRepo.updateRunStep(step.id, { status: 'running', startedAt: now })
|
||||
emit({ type: 'step/started', data: { stepId: step.id, nodeId: step.nodeId, nodeType: step.nodeType } })
|
||||
|
||||
try {
|
||||
const output = await executeNode(node, edges, nodes, outputs, emit)
|
||||
if (output !== null) {
|
||||
outputs.set(node.id, output)
|
||||
}
|
||||
|
||||
const endedAt = new Date().toISOString()
|
||||
runRepo.updateRunStep(step.id, { status: 'completed', output: output ?? '', endedAt })
|
||||
emit({ type: 'step/completed', data: { stepId: step.id, nodeId: step.nodeId, output: output?.slice(0, 500) ?? '' } })
|
||||
} catch (err) {
|
||||
const endedAt = new Date().toISOString()
|
||||
const errorMsg = err instanceof Error ? err.message : String(err)
|
||||
runRepo.updateRunStep(step.id, { status: 'failed', error: errorMsg, endedAt })
|
||||
emit({ type: 'step/failed', data: { stepId: step.id, nodeId: step.nodeId, error: errorMsg } })
|
||||
|
||||
// Fail the entire run
|
||||
runRepo.updateRunStatus(runId, 'failed', errorMsg)
|
||||
emit({ type: 'run/failed', data: { runId, error: errorMsg } })
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Mark run as completed
|
||||
runRepo.updateRunStatus(runId, 'completed')
|
||||
emit({ type: 'run/completed', data: { runId } })
|
||||
}
|
||||
Reference in New Issue
Block a user