diff --git a/bun.lock b/bun.lock index 5c4685bf..9df19deb 100644 --- a/bun.lock +++ b/bun.lock @@ -729,7 +729,7 @@ "@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.9.0", "", { "peerDependencies": { "unenv": "2.0.0-rc.24", "workerd": "^1.20251202.0" }, "optionalPeers": ["workerd"] }, "sha512-99nEvuOTCGGGRNaIat8UVVXJ27aZK+U09SYDp0kVjQLwC9wyxcrQ28IqLwrQq2DjWLmBI1+UalGJzdPqYgPlRw=="], - "@cloudflare/vite-plugin": ["@cloudflare/vite-plugin@1.20.2", "", { "dependencies": { "@cloudflare/unenv-preset": "2.9.0", "@remix-run/node-fetch-server": "^0.8.0", "defu": "^6.1.4", "get-port": "^7.1.0", "miniflare": "4.20260111.0", "picocolors": "^1.1.1", "tinyglobby": "^0.2.12", "unenv": "2.0.0-rc.24", "wrangler": "4.59.0", "ws": "8.18.0" }, "peerDependencies": { "vite": "^6.1.0 || ^7.0.0" } }, "sha512-OwlNobAshlfaoJdOnoIN2vRm1zYFzwodfZJOB+j/3PKNT6Qy+0Y+krzP3RAIUxAT4VSPVLizKfC3t48LMNrIfw=="], + "@cloudflare/vite-plugin": ["@cloudflare/vite-plugin@1.20.3", "", { "dependencies": { "@cloudflare/unenv-preset": "2.9.0", "@remix-run/node-fetch-server": "^0.8.0", "defu": "^6.1.4", "get-port": "^7.1.0", "miniflare": "4.20260111.0", "picocolors": "^1.1.1", "tinyglobby": "^0.2.12", "unenv": "2.0.0-rc.24", "wrangler": "4.59.1", "ws": "8.18.0" }, "peerDependencies": { "vite": "^6.1.0 || ^7.0.0" } }, "sha512-o6ePNfGpu2AKCi7bs32fOl121qFvdyi2fSblF6xID7aHFosqEfZAgCUaJ86LvXJWcPeUl+B0sFII67N5st1rBg=="], "@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260111.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-UGAjrGLev2/CMLZy7b+v1NIXA4Hupc/QJBFlJwMqldywMcJ/iEqvuUYYuVI2wZXuXeWkgmgFP87oFDQsg78YTQ=="], @@ -1503,15 +1503,15 @@ "@tanstack/react-query": ["@tanstack/react-query@5.90.16", "", { "dependencies": { "@tanstack/query-core": "5.90.16" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-bpMGOmV4OPmif7TNMteU/Ehf/hoC0Kf98PDc0F4BZkFrEapRMEqI/V6YS0lyzwSV6PQpY1y4xxArUIfBW5LVxQ=="], - "@tanstack/react-router": ["@tanstack/react-router@1.147.3", "", { "dependencies": { "@tanstack/history": "1.145.7", "@tanstack/react-store": "^0.8.0", "@tanstack/router-core": "1.147.1", "isbot": "^5.1.22", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-Fp9DoszYiIJclwxU43kyP/cqcWD418DPmV6yhmIOuVedsSMnfh2g7uRQ+bOoaWn996JjuU9yt/x48h66aCQSQA=="], + "@tanstack/react-router": ["@tanstack/react-router@1.149.3", "", { "dependencies": { "@tanstack/history": "1.145.7", "@tanstack/react-store": "^0.8.0", "@tanstack/router-core": "1.149.3", "isbot": "^5.1.22", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-yklZ2LSXLGfhW4PXu2N98yhGk8qtlkUbFRV42np0rx46s50wB5sXRkjdnqyGuDG/dldaBIi76M6vWg84Pmb4+A=="], - "@tanstack/react-router-devtools": ["@tanstack/react-router-devtools@1.149.0", "", { "dependencies": { "@tanstack/router-devtools-core": "1.149.0" }, "peerDependencies": { "@tanstack/react-router": "^1.147.3", "@tanstack/router-core": "^1.147.1", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" }, "optionalPeers": ["@tanstack/router-core"] }, "sha512-QJ6epMhRKTS8WrBmcMFjK1v+jDaimMQuySCSNA8NR1ZROKv3xx0gY8AjyVVgQ1h78HSXXRMYH3aql2kWYjc31g=="], + "@tanstack/react-router-devtools": ["@tanstack/react-router-devtools@1.149.3", "", { "dependencies": { "@tanstack/router-devtools-core": "1.149.3" }, "peerDependencies": { "@tanstack/react-router": "^1.149.3", "@tanstack/router-core": "^1.149.3", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" }, "optionalPeers": ["@tanstack/router-core"] }, "sha512-QH16WA0NkfZSxku8fHy0CFm42MJ1mXeDnCAsaIZXKypv935MQzsXEvwn6ZZDkH8qP8eCQBoYlRVZmWiIr+9Omw=="], "@tanstack/react-store": ["@tanstack/react-store@0.8.0", "", { "dependencies": { "@tanstack/store": "0.8.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-1vG9beLIuB7q69skxK9r5xiLN3ztzIPfSQSs0GfeqWGO2tGIyInZx0x1COhpx97RKaONSoAb8C3dxacWksm1ow=="], - "@tanstack/router-core": ["@tanstack/router-core@1.147.1", "", { "dependencies": { "@tanstack/history": "1.145.7", "@tanstack/store": "^0.8.0", "cookie-es": "^2.0.0", "seroval": "^1.4.1", "seroval-plugins": "^1.4.0", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-yf8o3CNgJVGO5JnIqiTe0y2eChxEM0w7TrEs1VSumL/zz2bQroYGNr1mOXJ2VeN+7YfJJwjEqq71P5CzWwMzRg=="], + "@tanstack/router-core": ["@tanstack/router-core@1.149.3", "", { "dependencies": { "@tanstack/history": "1.145.7", "@tanstack/store": "^0.8.0", "cookie-es": "^2.0.0", "seroval": "^1.4.1", "seroval-plugins": "^1.4.0", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-obXmQ2hElxqjQ9cpABjXOvR/aQG+uG9ALEcVvyqP1ae57Fb3VhOuynmc2k/eVgx/bKKvxe2cqj4wCG04O0i5Zg=="], - "@tanstack/router-devtools-core": ["@tanstack/router-devtools-core@1.149.0", "", { "dependencies": { "clsx": "^2.1.1", "goober": "^2.1.16", "tiny-invariant": "^1.3.3" }, "peerDependencies": { "@tanstack/router-core": "^1.147.1", "csstype": "^3.0.10" }, "optionalPeers": ["csstype"] }, "sha512-dy9xb8U9VWAavqKM0sTFhAs2ufVs3d/cGSbqczIgBcAKCjjbsAng1gV4ezPXmfF1pa+2MW6n7SViXsxxvtCRiw=="], + "@tanstack/router-devtools-core": ["@tanstack/router-devtools-core@1.149.3", "", { "dependencies": { "clsx": "^2.1.1", "goober": "^2.1.16", "tiny-invariant": "^1.3.3" }, "peerDependencies": { "@tanstack/router-core": "^1.149.3", "csstype": "^3.0.10" }, "optionalPeers": ["csstype"] }, "sha512-hgGPqqs/yD2XgmyTdmwBH6FrXnMbcsNWLup7nHPp/NGod9mtGKqSR2gBpicjZTBpaX/ihX29GG1s0l5MKmpQXA=="], "@tanstack/store": ["@tanstack/store@0.8.0", "", {}, "sha512-Om+BO0YfMZe//X2z0uLF2j+75nQga6TpTJgLJQBiq85aOyZNIhkCgleNcud2KQg4k4v9Y9l+Uhru3qWMPGTOzQ=="], @@ -1559,7 +1559,7 @@ "@types/mysql": ["@types/mysql@2.15.27", "", { "dependencies": { "@types/node": "*" } }, "sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA=="], - "@types/node": ["@types/node@24.10.7", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-+054pVMzVTmRQV8BhpGv3UyfZ2Llgl8rdpDTon+cUH9+na0ncBVXj3wTUKh14+Kiz18ziM3b4ikpP5/Pc0rQEQ=="], + "@types/node": ["@types/node@24.10.8", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-r0bBaXu5Swb05doFYO2kTWHMovJnNVbCsII0fhesM8bNRlLhXIuckley4a2DaD+vOdmm5G+zGkQZAPZsF80+YQ=="], "@types/node-fetch": ["@types/node-fetch@2.6.13", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.4" } }, "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw=="], @@ -1613,7 +1613,7 @@ "agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="], - "ai": ["ai@6.0.31", "", { "dependencies": { "@ai-sdk/gateway": "3.0.13", "@ai-sdk/provider": "3.0.2", "@ai-sdk/provider-utils": "4.0.5", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-aAn62jsDueAK7oiY4jeqJcA4zFctDqVHGyEaUDaWxEXzz4kbMdoByfYlYZhO1V3nhkeVoI8qNyFfiZusAubQLQ=="], + "ai": ["ai@6.0.33", "", { "dependencies": { "@ai-sdk/gateway": "3.0.13", "@ai-sdk/provider": "3.0.2", "@ai-sdk/provider-utils": "4.0.5", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-bVokbmy2E2QF6Efl+5hOJx5MRWoacZ/CZY/y1E+VcewknvGlgaiCzMu8Xgddz6ArFJjiMFNUPHKxAhIePE4rmg=="], "ai-v5": ["ai@5.0.97", "", { "dependencies": { "@ai-sdk/gateway": "2.0.12", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.17", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-8zBx0b/owis4eJI2tAlV8a1Rv0BANmLxontcAelkLNwEHhgfgXeKpDkhNB6OgV+BJSwboIUDkgd9312DdJnCOQ=="], @@ -2497,7 +2497,7 @@ "workerd": ["workerd@1.20260111.0", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260111.0", "@cloudflare/workerd-darwin-arm64": "1.20260111.0", "@cloudflare/workerd-linux-64": "1.20260111.0", "@cloudflare/workerd-linux-arm64": "1.20260111.0", "@cloudflare/workerd-windows-64": "1.20260111.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-ov6Pt4k6d/ALfJja/EIHohT9IrY/f6GAa0arWEPat2qekp78xHbVM7jSxNWAMbaE7ZmnQQIFEGD1ZhAWZmQKIg=="], - "wrangler": ["wrangler@4.59.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.4.1", "@cloudflare/unenv-preset": "2.9.0", "blake3-wasm": "2.1.5", "esbuild": "0.27.0", "miniflare": "4.20260111.0", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20260111.0" }, "optionalDependencies": { "fsevents": "~2.3.2" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20260111.0" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-YYTYELFHsWfkx4oT+SB6fV8r/4mzR/ySQrcSoGavdHsXNux7ge6UNscMifWnqyyB76meCVTkgsXrRONGoV7bEQ=="], + "wrangler": ["wrangler@4.59.1", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.4.1", "@cloudflare/unenv-preset": "2.9.0", "blake3-wasm": "2.1.5", "esbuild": "0.27.0", "miniflare": "4.20260111.0", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20260111.0" }, "optionalDependencies": { "fsevents": "~2.3.2" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20260111.0" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-5DddGSNxHd6dOjREWTDQdovQlZ1Lh80NNRXZFQ4/CrK3fNyVIBj9tqCs9pmXMNrKQ/AnKNeYzEs/l1kr8rHhOg=="], "wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], @@ -2589,6 +2589,8 @@ "@deco-cx/warp-node/undici": ["undici@6.23.0", "", {}, "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g=="], + "@deco-cx/warp-node/ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], + "@deco/mcp/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], "@discordjs/rest/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="], @@ -2857,8 +2859,6 @@ "bl/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], - "bun-types/@types/node": ["@types/node@25.0.7", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-C/er7DlIZgRJO7WtTdYovjIFzGsz0I95UlMyR9anTb4aCpBSRWe5Jc1/RvLKUfzmOxHPGjSE5+63HgLtndxU4w=="], - "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], "cloudflare/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], @@ -2963,15 +2963,13 @@ "pino-pretty/secure-json-parse": ["secure-json-parse@4.1.0", "", {}, "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA=="], - "protobufjs/@types/node": ["@types/node@25.0.7", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-C/er7DlIZgRJO7WtTdYovjIFzGsz0I95UlMyR9anTb4aCpBSRWe5Jc1/RvLKUfzmOxHPGjSE5+63HgLtndxU4w=="], - "readonly-sql/@decocms/runtime": ["@decocms/runtime@0.25.1", "", { "dependencies": { "@cloudflare/workers-types": "^4.20250617.0", "@deco/mcp": "npm:@jsr/deco__mcp@0.5.5", "@mastra/cloudflare-d1": "^0.13.4", "@mastra/core": "^0.20.2", "@modelcontextprotocol/sdk": "^1.19.1", "bidc": "0.0.3", "drizzle-orm": "^0.44.5", "jose": "^6.0.11", "mime-db": "1.52.0", "zod": "^3.25.76", "zod-from-json-schema": "^0.0.5", "zod-to-json-schema": "^3.24.4" } }, "sha512-G1J09NpHkuOcBQMPDi7zJDwtNweH33/39sOsR/mpA+sRWn2W3CX51FXeB5dp06oAmCe9BoBpYnyvb896hSQ+Jg=="], "readonly-sql/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.20.2", "", { "dependencies": { "ajv": "^6.12.6", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-6rqTdFt67AAAzln3NOKsXRmv5ZzPkgbfaebKBqUbts7vK1GZudqnrun5a8d3M/h955cam9RHZ6Jb4Y1XhnmFPg=="], "readonly-sql/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], - "registry/@types/node": ["@types/node@22.19.5", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-HfF8+mYcHPcPypui3w3mvzuIErlNOh2OAG+BCeBZCEwyiD5ls2SiCwEyT47OELtf7M3nHxBdu0FsmzdKxkN52Q=="], + "registry/@types/node": ["@types/node@22.19.6", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-qm+G8HuG6hOHQigsi7VGuLjUVu6TtBo/F05zvX04Mw2uCg9Dv0Qxy3Qw7j41SidlTcl5D/5yg0SEZqOB+EqZnQ=="], "replicate/@decocms/runtime": ["@decocms/runtime@0.25.1", "", { "dependencies": { "@cloudflare/workers-types": "^4.20250617.0", "@deco/mcp": "npm:@jsr/deco__mcp@0.5.5", "@mastra/cloudflare-d1": "^0.13.4", "@mastra/core": "^0.20.2", "@modelcontextprotocol/sdk": "^1.19.1", "bidc": "0.0.3", "drizzle-orm": "^0.44.5", "jose": "^6.0.11", "mime-db": "1.52.0", "zod": "^3.25.76", "zod-from-json-schema": "^0.0.5", "zod-to-json-schema": "^3.24.4" } }, "sha512-G1J09NpHkuOcBQMPDi7zJDwtNweH33/39sOsR/mpA+sRWn2W3CX51FXeB5dp06oAmCe9BoBpYnyvb896hSQ+Jg=="], @@ -3153,6 +3151,8 @@ "content-scraper/deco-cli/@supabase/supabase-js": ["@supabase/supabase-js@2.50.0", "", { "dependencies": { "@supabase/auth-js": "2.70.0", "@supabase/functions-js": "2.4.4", "@supabase/node-fetch": "2.6.15", "@supabase/postgrest-js": "1.19.4", "@supabase/realtime-js": "2.11.10", "@supabase/storage-js": "2.7.1" } }, "sha512-M1Gd5tPaaghYZ9OjeO1iORRqbTWFEz/cF3pPubRnMPzA+A8SiUsXXWDP+DWsASZcjEcVEcVQIAF38i5wrijYOg=="], + "content-scraper/deco-cli/ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], + "content-scraper/deco-cli/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], "data-for-seo/@decocms/runtime/@mastra/core": ["@mastra/core@0.20.2", "", { "dependencies": { "@a2a-js/sdk": "~0.2.4", "@ai-sdk/anthropic-v5": "npm:@ai-sdk/anthropic@2.0.23", "@ai-sdk/google-v5": "npm:@ai-sdk/google@2.0.17", "@ai-sdk/openai-compatible-v5": "npm:@ai-sdk/openai-compatible@1.0.19", "@ai-sdk/openai-v5": "npm:@ai-sdk/openai@2.0.42", "@ai-sdk/provider": "^1.1.3", "@ai-sdk/provider-utils": "^2.2.8", "@ai-sdk/provider-utils-v5": "npm:@ai-sdk/provider-utils@3.0.10", "@ai-sdk/provider-v5": "npm:@ai-sdk/provider@2.0.0", "@ai-sdk/ui-utils": "^1.2.11", "@ai-sdk/xai-v5": "npm:@ai-sdk/xai@2.0.23", "@isaacs/ttlcache": "^1.4.1", "@mastra/schema-compat": "0.11.4", "@openrouter/ai-sdk-provider-v5": "npm:@openrouter/ai-sdk-provider@1.2.0", "@opentelemetry/api": "^1.9.0", "@opentelemetry/auto-instrumentations-node": "^0.62.1", "@opentelemetry/core": "^2.0.1", "@opentelemetry/exporter-trace-otlp-grpc": "^0.203.0", "@opentelemetry/exporter-trace-otlp-http": "^0.203.0", "@opentelemetry/otlp-exporter-base": "^0.203.0", "@opentelemetry/otlp-transformer": "^0.203.0", "@opentelemetry/resources": "^2.0.1", "@opentelemetry/sdk-metrics": "^2.0.1", "@opentelemetry/sdk-node": "^0.203.0", "@opentelemetry/sdk-trace-base": "^2.0.1", "@opentelemetry/sdk-trace-node": "^2.0.1", "@opentelemetry/semantic-conventions": "^1.36.0", "@sindresorhus/slugify": "^2.2.1", "ai": "^4.3.19", "ai-v5": "npm:ai@5.0.60", "date-fns": "^3.6.0", "dotenv": "^16.6.1", "hono": "^4.9.7", "hono-openapi": "^0.4.8", "js-tiktoken": "^1.0.20", "json-schema": "^0.4.0", "json-schema-to-zod": "^2.6.1", "p-map": "^7.0.3", "pino": "^9.7.0", "pino-pretty": "^13.0.0", "radash": "^12.1.1", "sift": "^17.1.3", "xstate": "^5.20.1", "zod-to-json-schema": "^3.24.6" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-RbwuLwOVrcLbbjLFEBSlGTBA3mzGAy4bXp4JeXg2miJWDR/7WbXtxKIU+sTZGw5LpzlvvEFtj7JtHI1l+gKMVg=="], @@ -3871,8 +3871,6 @@ "apify/@decocms/runtime/@mastra/core/ai-v5/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.10", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.5" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-T1gZ76gEIwffep6MWI0QNy9jgoybUHE7TRaHB5k54K8mF91ciGFlbtCGxDYhMH3nCRergKwYFIDeFF0hJSIQHQ=="], - "content-scraper/deco-cli/@supabase/supabase-js/@supabase/realtime-js/ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], - "data-for-seo/@decocms/runtime/@mastra/core/@ai-sdk/anthropic-v5/@ai-sdk/provider": ["@ai-sdk/provider@2.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="], "data-for-seo/@decocms/runtime/@mastra/core/@ai-sdk/anthropic-v5/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.10", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.5" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-T1gZ76gEIwffep6MWI0QNy9jgoybUHE7TRaHB5k54K8mF91ciGFlbtCGxDYhMH3nCRergKwYFIDeFF0hJSIQHQ=="], diff --git a/deco-llm/server/db/postgres.ts b/deco-llm/server/db/postgres.ts new file mode 100644 index 00000000..37c16e4f --- /dev/null +++ b/deco-llm/server/db/postgres.ts @@ -0,0 +1,33 @@ +/** + * PostgreSQL Database Module + * + * Generic SQL runner using the DATABASE binding. + */ + +import type { Env } from "../main.ts"; + +/** + * Run a SQL query using the DATABASE binding + * @param env - The environment containing the DATABASE binding + * @param sql - SQL query with ? placeholders + * @param params - Parameters to substitute for ? placeholders + * @returns The query results as an array of rows + */ +export async function runSQL( + env: Env, + sql: string, + params: unknown[] = [], +): Promise { + // Defensive: some DATABASE bindings may interpolate params into SQL literals. + // Ensure single quotes inside string params don't break the query. + const sanitizedParams = params.map((p) => { + if (typeof p === "string") return p.replaceAll("'", "''"); + return p; + }); + const response = + await env.MESH_REQUEST_CONTEXT?.state?.DATABASE.DATABASES_RUN_SQL({ + sql, + params: sanitizedParams, + }); + return (response.result[0]?.results ?? []) as T[]; +} diff --git a/deco-llm/server/db/schemas/threads.ts b/deco-llm/server/db/schemas/threads.ts new file mode 100644 index 00000000..ede52510 --- /dev/null +++ b/deco-llm/server/db/schemas/threads.ts @@ -0,0 +1,63 @@ +import type { Env } from "../../main.ts"; +import { runSQL } from "../postgres.ts"; + +/** + * Ensure the threads and messages tables exist, creating them if necessary + */ +export async function ensureThreadsTables(env: Env) { + try { + // Create threads table + await runSQL( + env, + ` + CREATE TABLE IF NOT EXISTS threads ( + id TEXT PRIMARY KEY, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + metadata TEXT, + title TEXT, + status TEXT DEFAULT 'active' + ) + `, + ); + + // Create messages table + await runSQL( + env, + ` + CREATE TABLE IF NOT EXISTS messages ( + id TEXT PRIMARY KEY, + thread_id TEXT NOT NULL, + role TEXT NOT NULL, + content TEXT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + metadata TEXT, + tool_calls TEXT, + tokens_used INTEGER DEFAULT 0, + model TEXT, + finish_reason TEXT, + FOREIGN KEY (thread_id) REFERENCES threads(id) ON DELETE CASCADE + ) + `, + ); + + // Create indexes for better query performance + await runSQL( + env, + `CREATE INDEX IF NOT EXISTS idx_messages_thread_created_at ON messages(thread_id, created_at)`, + ); + + await runSQL( + env, + `CREATE INDEX IF NOT EXISTS idx_threads_status ON threads(status)`, + ); + + await runSQL( + env, + `CREATE INDEX IF NOT EXISTS idx_threads_updated_at ON threads(updated_at DESC)`, + ); + } catch (error) { + console.error("Error ensuring threads tables exist:", error); + throw error; + } +} diff --git a/deco-llm/server/main.ts b/deco-llm/server/main.ts index 82fabc2e..129063d7 100644 --- a/deco-llm/server/main.ts +++ b/deco-llm/server/main.ts @@ -12,10 +12,17 @@ import { serve } from "@decocms/mcps-shared/serve"; import { tools } from "@decocms/openrouter/tools"; import { BindingOf, type DefaultEnv, withRuntime } from "@decocms/runtime"; import { z } from "zod"; +import { ensureThreadsTables } from "./db/schemas/threads.ts"; +import { + createRetrieveThreadDataTool, + createSaveThreadDataTool, +} from "./tools/threads.ts"; +import { createLLMDoGenerateWithThreadTool } from "./tools/llm-with-thread.ts"; import { calculatePreAuthAmount, toMicrodollars } from "./usage"; export const StateSchema = z.object({ WALLET: BindingOf("@deco/wallet"), + DATABASE: BindingOf("@deco/postgres"), }); /** @@ -57,7 +64,8 @@ const runtime = withRuntime< Registry >({ tools: (env) => { - return tools(env, { + // Combine LLM tools with thread management tools + const llmTools = tools(env, { start: async (modelInfo, params) => { const amount = calculatePreAuthAmount(modelInfo, params); @@ -91,12 +99,26 @@ const runtime = withRuntime< }; }, }); + + // Add thread management tools + const threadTools = [ + createSaveThreadDataTool(env), + createRetrieveThreadDataTool(env), + createLLMDoGenerateWithThreadTool(env), + ]; + + return [...llmTools, ...threadTools]; }, configuration: { + onChange: async (env) => { + // Ensure threads tables exist on configuration change + await ensureThreadsTables(env); + }, state: StateSchema, scopes: [ "WALLET::PRE_AUTHORIZE_AMOUNT", "WALLET::COMMIT_PRE_AUTHORIZED_AMOUNT", + "DATABASE::DATABASES_RUN_SQL", ], }, }); diff --git a/deco-llm/server/tools/llm-with-thread.ts b/deco-llm/server/tools/llm-with-thread.ts new file mode 100644 index 00000000..388bd4ec --- /dev/null +++ b/deco-llm/server/tools/llm-with-thread.ts @@ -0,0 +1,276 @@ +/** + * LLM Generate With Thread Tool + * + * Generates LLM responses using thread context from the database. + * Automatically retrieves thread history, appends new user message, + * calls LLM_DO_GENERATE, and persists the conversation turn. + */ + +import { + LANGUAGE_MODEL_BINDING, + type LanguageModelGenerateOutputSchema, +} from "@decocms/bindings/llm"; +import { createPrivateTool } from "@decocms/runtime/tools"; +import { z } from "zod"; +import { tools } from "@decocms/openrouter/tools"; +import type { Env } from "../main.ts"; +import { + createRetrieveThreadDataTool, + createSaveThreadDataTool, +} from "./threads.ts"; +import { LanguageModelCallOptionsSchema } from "@decocms/bindings/llm"; + +// Find the GENERATE binding schema +const GENERATE_BINDING = LANGUAGE_MODEL_BINDING.find( + (b: { name: string }) => b.name === "LLM_DO_GENERATE", +); + +if (!GENERATE_BINDING?.inputSchema || !GENERATE_BINDING?.outputSchema) { + throw new Error("LLM_DO_GENERATE binding not found or missing schemas"); +} + +// Input schema: same as LLM_DO_GENERATE but with optional threadId +const LLMDoGenerateWithThreadInputSchema = z + .object({ + modelId: z.string().describe("The ID of the model"), + callOptions: LanguageModelCallOptionsSchema, // LanguageModelCallOptionsSchema + threadId: z.string().optional().describe("Optional thread ID to use for context"), + }) + .passthrough(); + +// Output schema: same as LLM_DO_GENERATE +const LLMDoGenerateWithThreadOutputSchema = GENERATE_BINDING.outputSchema; + +/** + * Convert thread messages (from database) to LanguageModelMessageSchema format + */ +function convertThreadMessagesToPrompt( + messages: Array<{ role: string; content: string }>, +): Array<{ role: string; content: Array<{ type: string; text: string }> }> { + return messages.map((msg) => ({ + role: msg.role, + content: [ + { + type: "text", + text: msg.content, + }, + ], + providerOptions: undefined, + })); +} + +/** + * Extract text content from user message content array + */ +function extractUserContent( + userMessage: { content: Array> }, +): string { + const textParts = userMessage.content + .filter((part: Record) => { + return part.type === "text" && typeof part.text === "string"; + }) + .map((part: Record) => { + return String(part.text ?? ""); + }); + return textParts.join("\n"); +} + +/** + * Extract text content from assistant generation output + */ +function extractAssistantContent( + generation: z.infer, +): string { + const textParts = generation.content + .filter((part: Record) => { + return part.type === "text" && typeof part.text === "string"; + }) + .map((part: Record) => { + return String(part.text ?? ""); + }); + return textParts.join("\n"); +} + +/** + * Extract tool calls from assistant generation output + */ +function extractToolCalls( + generation: z.infer, +): Array> { + return generation.content + .filter((part: Record) => { + return part.type === "tool-call"; + }) + .map((part: Record) => { + return { + toolCallId: part.toolCallId, + toolName: part.toolName, + input: part.input, + }; + }); +} + +/** + * LLM_DO_GENERATE_WITH_THREAD - Generate LLM response with thread context + * + * Retrieves thread history, appends new user message, generates response, + * and persists the conversation turn to the database. + */ +export const createLLMDoGenerateWithThreadTool = (env: Env) => + createPrivateTool({ + id: "LLM_DO_GENERATE_WITH_THREAD", + description: + "Generate a language model response using thread context from the database. " + + "Retrieves existing thread messages, appends the new user message, generates a response, " + + "and automatically saves the conversation turn (user + assistant) to the thread.", + inputSchema: LLMDoGenerateWithThreadInputSchema, + outputSchema: LLMDoGenerateWithThreadOutputSchema, + execute: async ({ + context, + }: { + context: z.infer; + }) => { + const { modelId, callOptions, threadId: providedThreadId } = context; + + // Step 1: Determine thread ID + const finalThreadId = providedThreadId ?? crypto.randomUUID(); + + // Step 2: Retrieve thread messages (or empty array if thread doesn't exist) + let threadMessages: Array<{ role: string; content: string }> = []; + try { + const retrieveTool = createRetrieveThreadDataTool(env); + const threadData = await retrieveTool.execute({ + context: { threadId: finalThreadId }, + }); + threadMessages = threadData.messages; + } catch (error) { + // Thread doesn't exist - continue with empty array + if ( + error instanceof Error && + error.message.includes("not found") + ) { + threadMessages = []; + } else { + throw error; + } + } + + // Step 3: Get last message from callOptions.prompt and validate it's a user message + const prompt = callOptions.prompt as Array<{ + role: string; + content: Array>; + }>; + if (!Array.isArray(prompt) || prompt.length === 0) { + throw new Error("callOptions.prompt must be a non-empty array"); + } + + const lastUserMessage = prompt[prompt.length - 1] as { + role: string; + content: Array>; + }; + if (lastUserMessage.role !== "user") { + throw new Error( + "The last message in callOptions.prompt must have role 'user'", + ); + } + + // Step 4: Convert thread messages to prompt format and merge with new user message + const threadPrompt = convertThreadMessagesToPrompt(threadMessages); + const mergedPrompt = [...threadPrompt, lastUserMessage]; + + // Step 5: Call LLM_DO_GENERATE with merged prompt using the existing tool + env.MESH_REQUEST_CONTEXT.ensureAuthenticated(); + + // Get LLM tools with hooks (same as in main.ts) + const llmTools = tools(env, { + start: async (modelInfo, params) => { + const { calculatePreAuthAmount, toMicrodollars } = await import("../usage.ts"); + const amount = calculatePreAuthAmount(modelInfo, params); + + const { id } = + await env.MESH_REQUEST_CONTEXT.state.WALLET.PRE_AUTHORIZE_AMOUNT({ + amount, + metadata: { + modelId: modelInfo.id, + params: params, + }, + }); + return { + end: async (usage: unknown) => { + interface OpenRouterUsageReport { + providerMetadata: { + openrouter: { + usage: { + cost: number; + }; + }; + }; + } + const usageReport = usage as OpenRouterUsageReport; + if ( + !usageReport?.providerMetadata?.openrouter?.usage?.cost + ) { + throw new Error("Usage cost not found"); + } + const vendorId = "deco"; // Default vendor ID + await env.MESH_REQUEST_CONTEXT.state.WALLET.COMMIT_PRE_AUTHORIZED_AMOUNT( + { + identifier: id, + contractId: + env.MESH_REQUEST_CONTEXT.connectionId ?? + env.MESH_REQUEST_CONTEXT.state.WALLET.value, + vendorId, + amount: toMicrodollars( + usageReport.providerMetadata.openrouter.usage.cost, + ), + }, + ); + }, + }; + }, + }); + + // Find LLM_DO_GENERATE tool + const generateTool = llmTools.find((tool) => tool.id === "LLM_DO_GENERATE"); + if (!generateTool) { + throw new Error("LLM_DO_GENERATE tool not found"); + } + + // Call LLM_DO_GENERATE with merged prompt + const generation = (await generateTool.execute({ + context: { + modelId, + callOptions: { + ...callOptions, + prompt: mergedPrompt, + }, + }, + })) as z.infer; + + // Step 6: Extract content and save to thread + const userContent = extractUserContent( + lastUserMessage as { content: Array> }, + ); + const assistantContent = extractAssistantContent(generation); + const toolCalls = extractToolCalls(generation); + const tokensUsed = generation.usage?.totalTokens ?? 0; + + // Save the conversation turn + const saveTool = createSaveThreadDataTool(env); + await saveTool.execute({ + context: { + threadId: finalThreadId, + userContent, + assistantContent, + assistantMetadata: { generation }, + toolCalls: toolCalls.length > 0 ? toolCalls : undefined, + tokensUsed, + model: modelId, + finishReason: generation.finishReason, + }, + }); + + // Step 7: Return the generation result (same as LLM_DO_GENERATE) + return generation; + }, + }); diff --git a/deco-llm/server/tools/threads.ts b/deco-llm/server/tools/threads.ts new file mode 100644 index 00000000..f4702742 --- /dev/null +++ b/deco-llm/server/tools/threads.ts @@ -0,0 +1,283 @@ +/** + * Thread Management Tools + * + * Tools for saving and retrieving thread conversations with LLM context. + * Ensures messages are always saved in pairs (user -> assistant) to maintain + * valid conversation state for LLM APIs. + */ + +import { createPrivateTool } from "@decocms/runtime/tools"; +import { z } from "zod"; +import type { Env } from "../main.ts"; +import { runSQL } from "../db/postgres.ts"; + +// ============================================================================ +// Types +// ============================================================================ + +interface ThreadRow { + id: string; + created_at: string; + updated_at: string; + metadata: string | null; + title: string | null; + status: string; +} + +interface MessageRow { + id: string; + thread_id: string; + role: string; + content: string; + created_at: string; + metadata: string | null; + tool_calls: string | null; + tokens_used: number; + model: string | null; + finish_reason: string | null; +} + +// ============================================================================ +// Tool: SAVE_THREAD_DATA +// ============================================================================ + +const SaveThreadDataInputSchema = z.object({ + threadId: z.string().optional(), + title: z.string().optional(), + status: z.enum(["active", "archived", "deleted"]).optional(), + threadMetadata: z.record(z.string(), z.unknown()).optional(), + userContent: z.string().describe("The user's message content"), + assistantContent: z.string().describe("The assistant's response content"), + assistantMetadata: z.record(z.string(), z.unknown()).optional(), + toolCalls: z.array(z.unknown()).optional(), + tokensUsed: z.number().optional(), + model: z.string().optional(), + finishReason: z.string().optional(), +}); + +const SaveThreadDataOutputSchema = z.object({ + threadId: z.string(), + userMessageId: z.string(), + assistantMessageId: z.string(), +}); + +/** + * SAVE_THREAD_DATA - Saves a conversation turn (user + assistant pair) to a thread + * + * Always saves messages in pairs to maintain valid LLM conversation state. + * Creates a new thread if threadId is not provided. + */ +export const createSaveThreadDataTool = (env: Env) => + createPrivateTool({ + id: "SAVE_THREAD_DATA", + description: + "Save a conversation turn (user message + assistant response) to a thread. " + + "Creates a new thread if threadId is not provided. Always saves messages in pairs " + + "to ensure valid conversation state for LLM APIs.", + inputSchema: SaveThreadDataInputSchema, + outputSchema: SaveThreadDataOutputSchema, + execute: async ({ context }: { context: z.infer }) => { + const { + threadId: providedThreadId, + title, + status = "active", + threadMetadata, + userContent, + assistantContent, + assistantMetadata, + toolCalls, + tokensUsed, + model, + finishReason, + } = context; + + // Generate IDs + const finalThreadId = providedThreadId ?? crypto.randomUUID(); + const userMessageId = crypto.randomUUID(); + const assistantMessageId = crypto.randomUUID(); + + // Serialize metadata and tool calls to JSON strings + const threadMetadataJson = threadMetadata + ? JSON.stringify(threadMetadata) + : null; + const assistantMetadataJson = assistantMetadata + ? JSON.stringify(assistantMetadata) + : null; + const toolCallsJson = toolCalls ? JSON.stringify(toolCalls) : null; + + // Execute atomic transaction: upsert thread + insert 2 messages + // Using a single SQL statement with CTE to ensure atomicity + await runSQL( + env, + ` + WITH thread_upsert AS ( + INSERT INTO threads (id, title, status, metadata, created_at, updated_at) + VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) + ON CONFLICT (id) DO UPDATE SET + updated_at = CURRENT_TIMESTAMP, + title = CASE WHEN EXCLUDED.title IS NOT NULL THEN EXCLUDED.title ELSE threads.title END, + status = CASE WHEN EXCLUDED.status IS NOT NULL THEN EXCLUDED.status ELSE threads.status END, + metadata = CASE WHEN EXCLUDED.metadata IS NOT NULL THEN EXCLUDED.metadata ELSE threads.metadata END + RETURNING id + ) + INSERT INTO messages (id, thread_id, role, content, metadata, tool_calls, tokens_used, model, finish_reason, created_at) + VALUES + (?, (SELECT id FROM thread_upsert), 'user', ?, NULL, NULL, 0, NULL, NULL, CURRENT_TIMESTAMP), + (?, (SELECT id FROM thread_upsert), 'assistant', ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) + `, + [ + finalThreadId, + title ?? null, + status, + threadMetadataJson, + userMessageId, + userContent, + assistantMessageId, + assistantContent, + assistantMetadataJson, + toolCallsJson, + tokensUsed ?? 0, + model ?? null, + finishReason ?? null, + ], + ); + + return { + threadId: finalThreadId, + userMessageId, + assistantMessageId, + }; + }, + }); + +// ============================================================================ +// Tool: RETRIEVE_THREAD_DATA +// ============================================================================ + +const RetrieveThreadDataInputSchema = z.object({ + threadId: z.string().describe("The ID of the thread to retrieve"), + limitPairs: z.number().optional().describe("Maximum number of message pairs to return"), + offsetPairs: z.number().optional().describe("Number of message pairs to skip"), +}); + +const RetrieveThreadDataOutputSchema = z.object({ + thread: z.object({ + id: z.string(), + created_at: z.string(), + updated_at: z.string(), + metadata: z.record(z.string(), z.unknown()).nullable(), + title: z.string().nullable(), + status: z.string(), + }), + messages: z.array( + z.object({ + id: z.string(), + role: z.string(), + content: z.string(), + created_at: z.string(), + metadata: z.record(z.string(), z.unknown()).nullable(), + tool_calls: z.array(z.unknown()).nullable(), + tokens_used: z.number(), + model: z.string().nullable(), + finish_reason: z.string().nullable(), + }), + ), +}); + +/** + * RETRIEVE_THREAD_DATA - Retrieves a thread and its messages + * + * Returns messages ordered by creation time, ensuring they start with 'user' + * and alternate in pairs. If there's an odd number of messages (legacy data), + * the last message is discarded to maintain pair integrity. + */ +export const createRetrieveThreadDataTool = (env: Env) => + createPrivateTool({ + id: "RETRIEVE_THREAD_DATA", + description: + "Retrieve a thread and all its messages ordered by creation time. " + + "Messages are returned in pairs (user -> assistant) suitable for LLM context. " + + "If there's an odd number of messages, the last one is discarded.", + inputSchema: RetrieveThreadDataInputSchema, + outputSchema: RetrieveThreadDataOutputSchema, + execute: async ({ context }: { context: z.infer }) => { + const { threadId, limitPairs, offsetPairs = 0 } = context; + + // Fetch thread + const threads = await runSQL( + env, + `SELECT * FROM threads WHERE id = ?`, + [threadId], + ); + + if (threads.length === 0) { + throw new Error(`Thread with id ${threadId} not found`); + } + + const thread = threads[0]; + + // Fetch messages ordered by creation time + let messagesQuery = ` + SELECT * FROM messages + WHERE thread_id = ? + ORDER BY created_at ASC + `; + + const queryParams: unknown[] = [threadId]; + + // Apply pagination if specified (limitPairs means limit message pairs, so multiply by 2) + if (limitPairs !== undefined) { + messagesQuery += ` LIMIT ?`; + queryParams.push(limitPairs * 2); + } + + if (offsetPairs > 0) { + messagesQuery += ` OFFSET ?`; + queryParams.push(offsetPairs * 2); + } + + const messageRows = await runSQL(env, messagesQuery, queryParams); + + // Parse JSON fields and transform to output format + const messages = messageRows.map((row) => ({ + id: row.id, + role: row.role, + content: row.content, + created_at: row.created_at, + metadata: row.metadata ? (JSON.parse(row.metadata) as Record) : null, + tool_calls: row.tool_calls ? (JSON.parse(row.tool_calls) as unknown[]) : null, + tokens_used: row.tokens_used, + model: row.model, + finish_reason: row.finish_reason, + })); + + // Ensure messages start with 'user' and are in pairs + // If first message is not 'user', discard it + let validMessages = messages; + if (validMessages.length > 0 && validMessages[0].role !== "user") { + validMessages = validMessages.slice(1); + } + + // Ensure even number of messages (discard last if odd) + if (validMessages.length % 2 !== 0) { + validMessages = validMessages.slice(0, -1); + } + + // Parse thread metadata + const threadMetadata = thread.metadata + ? (JSON.parse(thread.metadata) as Record) + : null; + + return { + thread: { + id: thread.id, + created_at: thread.created_at, + updated_at: thread.updated_at, + metadata: threadMetadata, + title: thread.title, + status: thread.status, + }, + messages: validMessages, + }; + }, + });