From 799c644d8e417aad2bcbade5edc840e9e87a310b Mon Sep 17 00:00:00 2001 From: decobot Date: Wed, 14 Jan 2026 13:05:47 -0300 Subject: [PATCH] added threads --- bun.lock | 85 ++----- deco-llm/server/db/postgres.ts | 33 +++ deco-llm/server/db/schemas/threads.ts | 63 +++++ deco-llm/server/main.ts | 24 +- deco-llm/server/tools/llm-with-thread.ts | 276 ++++++++++++++++++++++ deco-llm/server/tools/threads.ts | 283 +++++++++++++++++++++++ 6 files changed, 695 insertions(+), 69 deletions(-) create mode 100644 deco-llm/server/db/postgres.ts create mode 100644 deco-llm/server/db/schemas/threads.ts create mode 100644 deco-llm/server/tools/llm-with-thread.ts create mode 100644 deco-llm/server/tools/threads.ts diff --git a/bun.lock b/bun.lock index d5396fc7..758f8d7c 100644 --- a/bun.lock +++ b/bun.lock @@ -187,14 +187,15 @@ "name": "hyperdx", "version": "1.0.0", "dependencies": { - "@decocms/bindings": "1.0.1-alpha.23", - "@decocms/runtime": "1.0.0-alpha.41", + "@decocms/bindings": "^1.0.8", + "@decocms/mcps-shared": "workspace:*", + "@decocms/runtime": "^1.1.3", "zod": "^3.24.3", }, "devDependencies": { "@cloudflare/workers-types": "^4.20251014.0", "@modelcontextprotocol/sdk": "1.20.2", - "@types/node": "^25.0.3", + "@types/node": "^24.10.0", "bun-types": "^1.3.5", "deco-cli": "^0.28.0", "typescript": "^5.7.2", @@ -740,7 +741,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=="], @@ -1496,15 +1497,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=="], @@ -1552,7 +1553,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=="], @@ -1604,7 +1605,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=="], @@ -2476,7 +2477,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=="], @@ -2568,6 +2569,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=="], "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], @@ -2786,36 +2789,8 @@ "@ts-morph/common/minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="], - "@types/body-parser/@types/node": ["@types/node@25.0.7", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-C/er7DlIZgRJO7WtTdYovjIFzGsz0I95UlMyR9anTb4aCpBSRWe5Jc1/RvLKUfzmOxHPGjSE5+63HgLtndxU4w=="], - "@types/bun/bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="], - "@types/bunyan/@types/node": ["@types/node@25.0.7", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-C/er7DlIZgRJO7WtTdYovjIFzGsz0I95UlMyR9anTb4aCpBSRWe5Jc1/RvLKUfzmOxHPGjSE5+63HgLtndxU4w=="], - - "@types/connect/@types/node": ["@types/node@25.0.7", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-C/er7DlIZgRJO7WtTdYovjIFzGsz0I95UlMyR9anTb4aCpBSRWe5Jc1/RvLKUfzmOxHPGjSE5+63HgLtndxU4w=="], - - "@types/cors/@types/node": ["@types/node@25.0.7", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-C/er7DlIZgRJO7WtTdYovjIFzGsz0I95UlMyR9anTb4aCpBSRWe5Jc1/RvLKUfzmOxHPGjSE5+63HgLtndxU4w=="], - - "@types/express-serve-static-core/@types/node": ["@types/node@25.0.7", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-C/er7DlIZgRJO7WtTdYovjIFzGsz0I95UlMyR9anTb4aCpBSRWe5Jc1/RvLKUfzmOxHPGjSE5+63HgLtndxU4w=="], - - "@types/memcached/@types/node": ["@types/node@25.0.7", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-C/er7DlIZgRJO7WtTdYovjIFzGsz0I95UlMyR9anTb4aCpBSRWe5Jc1/RvLKUfzmOxHPGjSE5+63HgLtndxU4w=="], - - "@types/mysql/@types/node": ["@types/node@25.0.7", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-C/er7DlIZgRJO7WtTdYovjIFzGsz0I95UlMyR9anTb4aCpBSRWe5Jc1/RvLKUfzmOxHPGjSE5+63HgLtndxU4w=="], - - "@types/node-fetch/@types/node": ["@types/node@25.0.7", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-C/er7DlIZgRJO7WtTdYovjIFzGsz0I95UlMyR9anTb4aCpBSRWe5Jc1/RvLKUfzmOxHPGjSE5+63HgLtndxU4w=="], - - "@types/oracledb/@types/node": ["@types/node@25.0.7", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-C/er7DlIZgRJO7WtTdYovjIFzGsz0I95UlMyR9anTb4aCpBSRWe5Jc1/RvLKUfzmOxHPGjSE5+63HgLtndxU4w=="], - - "@types/pg/@types/node": ["@types/node@25.0.7", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-C/er7DlIZgRJO7WtTdYovjIFzGsz0I95UlMyR9anTb4aCpBSRWe5Jc1/RvLKUfzmOxHPGjSE5+63HgLtndxU4w=="], - - "@types/send/@types/node": ["@types/node@25.0.7", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-C/er7DlIZgRJO7WtTdYovjIFzGsz0I95UlMyR9anTb4aCpBSRWe5Jc1/RvLKUfzmOxHPGjSE5+63HgLtndxU4w=="], - - "@types/serve-static/@types/node": ["@types/node@25.0.7", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-C/er7DlIZgRJO7WtTdYovjIFzGsz0I95UlMyR9anTb4aCpBSRWe5Jc1/RvLKUfzmOxHPGjSE5+63HgLtndxU4w=="], - - "@types/tedious/@types/node": ["@types/node@25.0.7", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-C/er7DlIZgRJO7WtTdYovjIFzGsz0I95UlMyR9anTb4aCpBSRWe5Jc1/RvLKUfzmOxHPGjSE5+63HgLtndxU4w=="], - - "@types/ws/@types/node": ["@types/node@25.0.7", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-C/er7DlIZgRJO7WtTdYovjIFzGsz0I95UlMyR9anTb4aCpBSRWe5Jc1/RvLKUfzmOxHPGjSE5+63HgLtndxU4w=="], - "ai-v5/@ai-sdk/gateway": ["@ai-sdk/gateway@2.0.12", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.17", "@vercel/oidc": "3.0.5" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-W+cB1sOWvPcz9qiIsNtD+HxUrBUva2vWv2K1EFukuImX+HA0uZx3EyyOjhYQ9gtf/teqEG80M6OvJ7xx/VLV2A=="], "ai-v5/@ai-sdk/provider": ["@ai-sdk/provider@2.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="], @@ -2832,8 +2807,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=="], @@ -2876,14 +2849,8 @@ "gemini-pro-vision/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], - "hyperdx/@decocms/bindings": ["@decocms/bindings@1.0.1-alpha.23", "", { "dependencies": { "@modelcontextprotocol/sdk": "1.20.2", "zod": "^3.25.76", "zod-from-json-schema": "^0.0.5" } }, "sha512-CVvfLeQPzD0RwkIznl4CPg8yLcdz5O2bgMmaL+lyHe+azO2qkGpQkTp1pX5fDhdr7jvWtjTcThUNoiCno58cSw=="], - - "hyperdx/@decocms/runtime": ["@decocms/runtime@1.0.0-alpha.41", "", { "dependencies": { "@ai-sdk/provider": "^2.0.0", "@cloudflare/workers-types": "^4.20250617.0", "@deco/mcp": "npm:@jsr/deco__mcp@0.7.8", "@decocms/bindings": "1.0.1-alpha.23", "@modelcontextprotocol/sdk": "1.20.2", "hono": "^4.10.7", "jose": "^6.0.11", "zod": "^3.25.76", "zod-to-json-schema": "3.25.0" } }, "sha512-zpGy4bMoly/uQV8jNOeaAsIbXs5rS+ksXGqvIU9EqS3cH/aP2Dab158sy12OH+ixvlAZab5bd7yBij0viy+T6g=="], - "hyperdx/@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=="], - "hyperdx/@types/node": ["@types/node@25.0.7", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-C/er7DlIZgRJO7WtTdYovjIFzGsz0I95UlMyR9anTb4aCpBSRWe5Jc1/RvLKUfzmOxHPGjSE5+63HgLtndxU4w=="], - "hyperdx/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], "inquirer-search-checkbox/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="], @@ -2946,15 +2913,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=="], @@ -3118,8 +3083,6 @@ "@opentelemetry/sdk-trace-web/@opentelemetry/sdk-trace-base/@opentelemetry/resources": ["@opentelemetry/resources@1.25.1", "", { "dependencies": { "@opentelemetry/core": "1.25.1", "@opentelemetry/semantic-conventions": "1.25.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-pkZT+iFYIZsVn6+GzM0kSX+u3MSLCY9md+lIJOoKl/P+gJFfxJte/60Usdp8Ce4rOs8GduUpSPNe1ddGyDT1sQ=="], - "@types/bun/bun-types/@types/node": ["@types/node@25.0.7", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-C/er7DlIZgRJO7WtTdYovjIFzGsz0I95UlMyR9anTb4aCpBSRWe5Jc1/RvLKUfzmOxHPGjSE5+63HgLtndxU4w=="], - "ai-v5/@ai-sdk/gateway/@vercel/oidc": ["@vercel/oidc@3.0.5", "", {}, "sha512-fnYhv671l+eTTp48gB4zEsTW/YtRgRPnkI2nT7x6qw5rkI1Lq2hTmQIpHPgyThI0znLK+vX2n9XxKdXZ7BUbbw=="], "apify/@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=="], @@ -3136,6 +3099,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=="], @@ -3172,14 +3137,6 @@ "gemini-pro-vision/@modelcontextprotocol/sdk/ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], - "hyperdx/@decocms/bindings/zod-from-json-schema": ["zod-from-json-schema@0.0.5", "", { "dependencies": { "zod": "^3.24.2" } }, "sha512-zYEoo86M1qpA1Pq6329oSyHLS785z/mTwfr9V1Xf/ZLhuuBGaMlDGu/pDVGVUe4H4oa1EFgWZT53DP0U3oT9CQ=="], - - "hyperdx/@decocms/runtime/@ai-sdk/provider": ["@ai-sdk/provider@2.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="], - - "hyperdx/@decocms/runtime/@deco/mcp": ["@jsr/deco__mcp@0.7.8", "https://npm.jsr.io/~/11/@jsr/deco__mcp/0.7.8.tgz", { "dependencies": { "@jsr/deco__deco": "^1.112.1", "@jsr/hono__hono": "^4.5.4", "@modelcontextprotocol/sdk": "^1.11.4", "fetch-to-node": "^2.1.0", "zod": "^3.24.2" } }, "sha512-NcDGuKv2ZxId8ZHCD3zGZhHNLzrExn+DL11yDo60mZ44zbbRwuLRRmAYTwrdtZ6A45fVWYmEIloPY93tKrtimw=="], - - "hyperdx/@decocms/runtime/zod-to-json-schema": ["zod-to-json-schema@3.25.0", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-HvWtU2UG41LALjajJrML6uQejQhNJx+JBO9IflpSja4R03iNWfKXrj6W2h7ljuLyc1nKS+9yDyL/9tD1U/yBnQ=="], - "hyperdx/@modelcontextprotocol/sdk/ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], "inquirer-search-checkbox/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], @@ -3526,8 +3483,6 @@ "gemini-pro-vision/@modelcontextprotocol/sdk/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], - "hyperdx/@decocms/runtime/@deco/mcp/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.1", "", { "dependencies": { "@hono/node-server": "^1.19.7", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "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", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-yO28oVFFC7EBoiKdAn+VqRm+plcfv4v0xp6osG/VsCB0NlPZWi87ajbCZZ8f/RvOFLEu7//rSRmuZZ7lMoe3gQ=="], - "hyperdx/@modelcontextprotocol/sdk/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], "inquirer-search-checkbox/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], @@ -3904,8 +3859,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=="], @@ -4008,10 +3961,6 @@ "gemini-pro-vision/@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=="], - "hyperdx/@decocms/runtime/@deco/mcp/@modelcontextprotocol/sdk/zod": ["zod@4.3.5", "", {}, "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g=="], - - "hyperdx/@decocms/runtime/@deco/mcp/@modelcontextprotocol/sdk/zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="], - "inquirer-search-checkbox/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], "inquirer-search-checkbox/inquirer/cli-cursor/restore-cursor/onetime": ["onetime@2.0.1", "", { "dependencies": { "mimic-fn": "^1.0.0" } }, "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ=="], 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, + }; + }, + });