From 291fc1727b935206320f098e36641e3400c253d0 Mon Sep 17 00:00:00 2001 From: Hasan Kumar <769950+mintuhouse@users.noreply.github.com> Date: Fri, 23 Oct 2026 13:28:48 +0530 Subject: [PATCH] Upgrade TanStack AI packages to v1 --- ARCHITECTURE.md | 4 +- bun.lock | 90 ++++++-------- examples/todos/e2e/pages/chat-widget.ts | 6 +- platform/package.json | 26 ++-- platform/public/api/openapi.json | 4 - platform/src/lib/chat/README.md | 2 +- .../src/lib/chat/adapter-responses.test.ts | 8 +- platform/src/lib/chat/adapter.test.ts | 7 +- platform/src/lib/chat/adapter.ts | 11 +- platform/src/lib/chat/chat.test.ts | 2 - platform/src/lib/chat/chat.ts | 18 ++- platform/src/lib/chat/sandbox/sandbox.ts | 7 +- platform/src/lib/chat/sandbox/tools.test.ts | 47 ++++++- platform/src/lib/chat/sandbox/tools.ts | 62 ++++++---- .../src/lib/chat/threads/commands.test.ts | 1 - platform/src/lib/chat/threads/commands.ts | 1 - platform/src/lib/chat/threads/stream.test.ts | 117 ++++++++++++------ platform/src/lib/chat/threads/stream.ts | 17 +-- .../chat/threads/threads.integration.test.ts | 1 - platform/src/lib/chat/usage-limits.test.ts | 48 ++++++- platform/src/lib/chat/usage-limits.ts | 26 +++- platform/src/lib/chat/web.test.ts | 1 + platform/src/lib/chat/web.ts | 9 +- platform/src/lib/mcp/tools.server.test.ts | 2 + platform/src/lib/mcp/tools.server.ts | 59 +++------ .../lib/model-providers/test-model.server.ts | 1 + .../lib/model-providers/usage-cost.test.ts | 17 +++ .../src/lib/model-providers/usage-cost.ts | 8 +- .../src/lib/model-providers/web.server.ts | 16 +-- .../routes/api/v1/chat/-lib/chat.server.ts | 3 - sdk/package.json | 4 +- sdk/src/api/generated/api.ts | 7 -- sdk/src/core/session-persistence.test.ts | 105 +++++++++------- 33 files changed, 438 insertions(+), 299 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index e9ac6e8a1..0302a4ef1 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -87,7 +87,7 @@ One encrypted authorization attempt per connection lives in the existing Postgre Refresh claims a timestamp inside the encrypted credential envelope against the current credential ciphertext and lock version. Token exchange runs outside transactions. Conditional commits prevent stale work from replacing new credentials or undoing disconnect. A rotating response without a replacement refresh token preserves the current grant's refresh token. An expired claim or ambiguous response requires reconnection, without redeeming the old token again. Google authorization requests consent and offline access. Disconnect is local and does not revoke provider-wide grants. Tokens never enter workflow journals or idempotency receipts. -Deterministic fixtures cover protocol, ownership, OAuth and cross-process refresh. Live provider verification is separate. Legacy MCP revisions, stdio, protocol sessions, private networks, subscriptions, resources, prompts, sampling, elicitation and incremental scope authorization are outside this client. The package's modern tool path does not mirror `x-mcp-header` parameters and ignores `structuredContent` without a declared output schema. Organizations configure required OAuth scopes before connecting. Discovery has response-size, page, time and connection bounds. All discovered tools are exposed without a separate catalog allocator, so the model provider's own limits apply to the combined tool set. +Deterministic fixtures cover protocol, ownership, OAuth and cross-process refresh. Live provider verification is separate. Legacy MCP revisions, stdio, protocol sessions, private networks, subscriptions, resources, prompts, sampling, elicitation and incremental scope authorization are outside this client. The SDK mirrors `x-mcp-header` parameters, while AstralBeam filters invalid declarations and rejects unsafe mirrored values before dispatch. The package ignores `structuredContent` without a declared output schema. Organizations configure required OAuth scopes before connecting. Discovery has response-size, page, time and connection bounds. All discovered tools are exposed without a separate catalog allocator, so the model provider's own limits apply to the combined tool set. To verify live providers, open **MCP connections** in the embedded sidebar, connect to a server, and choose **Test connection**. This lists tools without invoking business tools. See [MCP servers](platform/src/routes/docs/-content/dashboard/connections.md) for hosted examples. @@ -167,7 +167,7 @@ Execution remains attached to its foreground request. Process crashes can leave Sandbox identity includes the configured provider connection and authenticated principal. Each process reserves at most 25 leases before provisioning, protects active runs, and expires workspaces 15 minutes after the final run closes. Reported deletion failures retain their lease for retry. Cleanup remains best effort: abrupt process termination loses these records, and the installed Docker, Daytona, and Vercel adapters suppress deletion errors. Guaranteed eventual cleanup would require provider expiry or external reconciliation. -Sandbox files and command output are buffered before size checks or truncation, and wrapper timeouts may not stop transfers. Sparse XLSX worksheets are padded and rendered synchronously before text truncation, without a document-wide work budget. Both paths can exhaust replica resources. +Known regular-file artifact sizes are checked before transfer and bytes are checked again before publication. File changes can race that metadata check. Other sandbox reads and command output are buffered before truncation, and wrapper timeouts may not stop transfers. Sparse XLSX worksheets are padded and rendered synchronously before text truncation, without a document-wide work budget. Both paths can exhaust replica resources. Management APIs lack application body, name, and metadata budgets. Their schemas can accept NUL or ill-formed Unicode that PostgreSQL rejects as internal failures. Ingress limits and client-address forwarding require hosted verification. diff --git a/bun.lock b/bun.lock index ae6ec967b..08a345621 100644 --- a/bun.lock +++ b/bun.lock @@ -134,19 +134,19 @@ "@pydantic/genai-prices": "^0.1.12", "@react-email/render": "2.1.0", "@scalar/api-reference-react": "^0.9.81", - "@tanstack/ai": "^0.69.0", - "@tanstack/ai-anthropic": "^0.22.0", - "@tanstack/ai-gemini": "^0.38.0", - "@tanstack/ai-grok": "^0.22.0", - "@tanstack/ai-mcp": "0.9.0", - "@tanstack/ai-openai": "^0.30.0", - "@tanstack/ai-openrouter": "^0.22.0", - "@tanstack/ai-persistence": "0.9.0", - "@tanstack/ai-sandbox": "^0.5.23", - "@tanstack/ai-sandbox-daytona": "^0.3.4", - "@tanstack/ai-sandbox-docker": "^0.3.2", - "@tanstack/ai-sandbox-sprites": "^0.2.5", - "@tanstack/ai-sandbox-vercel": "^0.2.5", + "@tanstack/ai": "^1.0.0", + "@tanstack/ai-anthropic": "^1.0.0", + "@tanstack/ai-gemini": "^1.0.0", + "@tanstack/ai-grok": "^1.0.0", + "@tanstack/ai-mcp": "^1.0.0", + "@tanstack/ai-openai": "^1.0.0", + "@tanstack/ai-openrouter": "^1.0.0", + "@tanstack/ai-persistence": "^1.0.0", + "@tanstack/ai-sandbox": "^1.0.0", + "@tanstack/ai-sandbox-daytona": "^1.0.0", + "@tanstack/ai-sandbox-docker": "^1.0.0", + "@tanstack/ai-sandbox-sprites": "^1.0.0", + "@tanstack/ai-sandbox-vercel": "^1.0.0", "@tanstack/highlight": "^1.1.0", "@tanstack/markdown": "^1.0.0", "@tanstack/react-devtools": "^0.10.13", @@ -225,8 +225,8 @@ "@phosphor-icons/react": "^2.1.10", "@shadcn/react": "^0.3.1", "@tailwindcss/cli": "^4.3.3", - "@tanstack/ai": "^0.68.0", - "@tanstack/ai-client": "^0.39.2", + "@tanstack/ai": "^1.0.0", + "@tanstack/ai-client": "^1.0.0", "@tanstack/eslint-plugin-query": "^5.103.2", "@tanstack/markdown": "^1.0.0", "@tanstack/react-pacer": "^0.23.0", @@ -332,7 +332,7 @@ "@ai-sdk/vue": ["@ai-sdk/vue@3.0.214", "", { "dependencies": { "@ai-sdk/provider-utils": "4.0.33", "ai": "6.0.214", "swrv": "^1.0.4" }, "peerDependencies": { "vue": "^3.3.4" } }, "sha512-YADiezfhQfjqWJttXkweSLZNWCUH0VORv3VkCuwbAD+ViOTYiO/Im3Nfr49AgUwq+LaYZGDFCXtrg5DNobgR3g=="], - "@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.97.1", "", { "dependencies": { "json-schema-to-ts": "^3.1.1", "standardwebhooks": "^1.0.0" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-wOf7AUeJPitcVpvKO4UMu63mWH5SaVipkGd7OOQJt/G6VYGlV8D2Gp9dLxOrttDJh/9gqPqdaBwDGcBevumeAg=="], + "@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.135.0", "", { "dependencies": { "@opentelemetry/api": "^1.4.1", "json-schema-to-ts": "^3.1.1", "standardwebhooks": "^1.0.0" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-WWrdL57JhU5KoynpTjt9GXK6k7ud9nPw2uw/Trn+oBXh6s0KpjhAccWZm2Qxw08ARYG1UDQj5iC+JO42j1rt/g=="], "@astralbeam/cli": ["@astralbeam/cli@workspace:cli"], @@ -484,6 +484,8 @@ "@cacheable/utils": ["@cacheable/utils@2.5.0", "", { "dependencies": { "hashery": "^1.5.1", "keyv": "^5.6.0" } }, "sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA=="], + "@cfworker/json-schema": ["@cfworker/json-schema@4.1.1", "", {}, "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og=="], + "@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.5.0", "", {}, "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg=="], "@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.16.2", "", { "peerDependencies": { "unenv": "2.0.0-rc.24", "workerd": ">1.20260305.0 <2.0.0-0" }, "optionalPeers": ["workerd"] }, "sha512-JBP1+Z7ZSNG/d4mRP+y8VC5dka3tZVMLEZRvS+rzQ4DGV1EoxRFQckcJTTkXbHSQiTj0DtNI01Zwb/V2fX0mvQ=="], @@ -554,8 +556,6 @@ "@effect/sql-pg": ["@effect/sql-pg@4.0.5", "", { "peerDependencies": { "effect": "^4.0.5" } }, "sha512-F9tG76H+aryhYcp5pvgd+MwWrNltv3XAuLcZSBsOOX6q+PEl+IM4zkQKzx0TF9OsEMH/SQ+3gZRdYWOsDF+vJQ=="], - "@effect/vitest": ["@effect/vitest@4.0.5", "", { "peerDependencies": { "effect": "^4.0.5", "vitest": ">=5.0.0 <6.0.0" } }, "sha512-q64paZ/i4V2NiRhTyqCmT2KiDbFjlxc2EZrZca8GGt+Nf8844946HyJM5bPk/q+YeRkFSxIrlCs+J+LSfky/wA=="], - "@emnapi/core": ["@emnapi/core@1.11.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA=="], "@emnapi/runtime": ["@emnapi/runtime@1.11.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA=="], @@ -1372,39 +1372,39 @@ "@tailwindcss/vite": ["@tailwindcss/vite@4.3.3", "", { "dependencies": { "@tailwindcss/node": "4.3.3", "@tailwindcss/oxide": "4.3.3", "tailwindcss": "4.3.3" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw=="], - "@tanstack/ai": ["@tanstack/ai@0.69.0", "", { "dependencies": { "@ag-ui/core": "1.0.0", "@standard-schema/spec": "^1.1.0", "@tanstack/ai-event-client": "^0.13.1", "@tanstack/ai-utils": "^0.4.2", "fast-json-patch": "^3.1.1", "partial-json": "^0.1.7" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0" }, "optionalPeers": ["@opentelemetry/api"] }, "sha512-Y018M4Cg3LPr1meRoVG/ePDB1mdazL9cPBA7bnm8WJm2LT0cN2LoUEXRimKSZjyYY7G+9BtQeNiKlaLJkbqHKQ=="], + "@tanstack/ai": ["@tanstack/ai@1.0.0", "", { "dependencies": { "@ag-ui/core": "1.0.0", "@cfworker/json-schema": "^4.1.1", "@standard-schema/spec": "^1.1.0", "@tanstack/ai-event-client": "^1.0.0", "@tanstack/ai-utils": "^1.0.0", "fast-json-patch": "^3.1.1", "partial-json": "^0.1.7" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0" }, "optionalPeers": ["@opentelemetry/api"] }, "sha512-kqHkfJPLmNwZ5+dwC5cCZy4UASfIEVToBBSccRUP2ued2g1/TytZkTOHMlNsbVca2Q1pJ/pQfdQkQ7xwhze8qw=="], - "@tanstack/ai-anthropic": ["@tanstack/ai-anthropic@0.22.0", "", { "dependencies": { "@anthropic-ai/sdk": "^0.97.1", "@tanstack/ai-utils": "^0.4.2" }, "peerDependencies": { "@anthropic-ai/vertex-sdk": "^0.19.0", "@tanstack/ai": "^0.69.0" }, "optionalPeers": ["@anthropic-ai/vertex-sdk"] }, "sha512-4yEmtvtZoBKeUltuWg3DcbbHdOIFl1F/TyTCShHxReE4LplaC8ZAJGsIvE+4Wh7VOhOgx8N4Zm5qEIoYVu4MEA=="], + "@tanstack/ai-anthropic": ["@tanstack/ai-anthropic@1.0.0", "", { "dependencies": { "@anthropic-ai/sdk": "^0.135.0", "@tanstack/ai-utils": "^1.0.0" }, "peerDependencies": { "@anthropic-ai/vertex-sdk": "^0.19.0", "@tanstack/ai": "^1.0.0" }, "optionalPeers": ["@anthropic-ai/vertex-sdk"] }, "sha512-YeJoyaSJ4wSqALXHfIX5Lxd9CulqwqU55bcNaLaqPPut+X6eH96aFXhX2qRhUOvH9vhK/8r7MmIroE/dpICBww=="], - "@tanstack/ai-client": ["@tanstack/ai-client@0.39.2", "", { "dependencies": { "@tanstack/ai": "^0.68.0", "@tanstack/ai-event-client": "^0.13.1", "@tanstack/ai-utils": "^0.4.1" } }, "sha512-AAAqgpou//LbucooWGTmkG+c7ZZYtBNC1bwxvwPwwDxe3+7KUIZg7/LKiO7MqpzMsuII3EIC9dKmmxuVEHEU2Q=="], + "@tanstack/ai-client": ["@tanstack/ai-client@1.0.0", "", { "dependencies": { "@tanstack/ai": "^1.0.0", "@tanstack/ai-event-client": "^1.0.0", "@tanstack/ai-utils": "^1.0.0", "@tanstack/store": "^0.11.1" } }, "sha512-lPba8LAMmJn7X/D6LongAR4RfwWv/6I/XJPiYKZ8C235VKQMN1ZszhswyFOCIDYJzgzO1N7azHTwoEwSohR48Q=="], - "@tanstack/ai-event-client": ["@tanstack/ai-event-client@0.13.1", "", { "dependencies": { "@tanstack/devtools-event-client": "^0.4.1" } }, "sha512-lCPcshu6Sd4tFNeMSr+HXgdqCGupYU2cpJZVUent4GkFx41dXzrBaKkwOgkji6olDBT99OBagWCce1NZ15XeJA=="], + "@tanstack/ai-event-client": ["@tanstack/ai-event-client@1.0.0", "", { "dependencies": { "@tanstack/devtools-event-client": "^0.4.1" } }, "sha512-RXlk6XkIYU4n25QYoCmETKiFjPnEpwvQv5Ve62pdtHaEVP//VZUdwGSRGCf/PATgwYdib8FT49tPmqnKtXPDhw=="], - "@tanstack/ai-gemini": ["@tanstack/ai-gemini@0.38.0", "", { "dependencies": { "@google/genai": "^2.10.0", "@tanstack/ai-utils": "^0.4.2", "partial-json": "^0.1.7" }, "peerDependencies": { "@tanstack/ai": "^0.69.0" } }, "sha512-CG3+iRSB9Qp9YvuI7oZad/OOLi26WM+lzW7CxTTF4CJPvX9VP5f1P0Nj2qkJ+qVEFk6N18uEUKw+DdNKLHOCLQ=="], + "@tanstack/ai-gemini": ["@tanstack/ai-gemini@1.0.0", "", { "dependencies": { "@google/genai": "^2.10.0", "@tanstack/ai-utils": "^1.0.0", "partial-json": "^0.1.7" }, "peerDependencies": { "@tanstack/ai": "^1.0.0" } }, "sha512-63StDDoH5ePD4Xux0EEcucmcgqid/XY0P48oOXILQ2d1TkJPPjUgVYkIwO0CmUJkdviIGWshSHuqss29psUmYw=="], - "@tanstack/ai-grok": ["@tanstack/ai-grok@0.22.0", "", { "dependencies": { "@tanstack/ai-utils": "^0.4.2", "@tanstack/openai-base": "^0.14.0", "openai": "^7.30.0" }, "peerDependencies": { "@tanstack/ai": "^0.69.0", "google-auth-library": "^10.5.0" }, "optionalPeers": ["google-auth-library"] }, "sha512-yfoaQ4PkAE6RGZrrSd5f6ofH1Maf/YUIiRYGI6uzE8y3YWOMwia5rFSmeF39Be7RV4TPVTj8PSNXljCyt/O2Zg=="], + "@tanstack/ai-grok": ["@tanstack/ai-grok@1.0.0", "", { "dependencies": { "@tanstack/ai-utils": "^1.0.0", "@tanstack/openai-base": "^1.0.0", "openai": "^7.30.0" }, "peerDependencies": { "@tanstack/ai": "^1.0.0", "google-auth-library": "^10.5.0" }, "optionalPeers": ["google-auth-library"] }, "sha512-ZL8RsxWV8/AtL2qNm3Tsk2BeSP1ftnf5UnFHlVtNeByBHH4XfWnGvynKlpSXNEyEMQ7VUqRcPOP377lF0fvvKw=="], - "@tanstack/ai-mcp": ["@tanstack/ai-mcp@0.9.0", "", { "dependencies": { "@modelcontextprotocol/client": "^2.0.0", "@modelcontextprotocol/server": "^2.0.0", "@tanstack/ai": "^0.69.0", "jose": "^6.2.12" }, "bin": { "tanstack-ai-mcp": "./dist/bin/bin.js" } }, "sha512-E8HNsIxgNDuIx06ExNVbx902gbJJu/lmDuW+FDpqF0XGA/p/vjsYgIi+RCUcF5/AFyPI69m1/1x8QlUMLCX95A=="], + "@tanstack/ai-mcp": ["@tanstack/ai-mcp@1.0.0", "", { "dependencies": { "@modelcontextprotocol/client": "^2.0.0", "@modelcontextprotocol/server": "^2.0.0", "@tanstack/ai": "^1.0.0", "jose": "^6.2.12" }, "peerDependencies": { "@tanstack/ai-harness": "^0.1.0" }, "optionalPeers": ["@tanstack/ai-harness"], "bin": { "tanstack-ai-mcp": "./dist/bin/bin.js" } }, "sha512-HPjtpK1nb7WKaAr5cE1/DlcDR/BJyLYeYoMcuS64V7ZefCJvF/6Dj0yb+Elt9B4dZpJ85SWRBzNyf7qGjJI1hQ=="], - "@tanstack/ai-openai": ["@tanstack/ai-openai@0.30.0", "", { "dependencies": { "@tanstack/ai-utils": "^0.4.2", "@tanstack/openai-base": "^0.14.0", "openai": "^7.30.0" }, "peerDependencies": { "@tanstack/ai": "^0.69.0" } }, "sha512-LZPs6jmWh+nlvYdHTS4bOhA+0qGZpYatuEdTkIqF9W+p9C86spDUhoaEsSXimBbd+mL1RTqahUIHrsAwyNSeSA=="], + "@tanstack/ai-openai": ["@tanstack/ai-openai@1.0.0", "", { "dependencies": { "@tanstack/ai-utils": "^1.0.0", "@tanstack/openai-base": "^1.0.0", "openai": "^7.30.0" }, "peerDependencies": { "@tanstack/ai": "^1.0.0" } }, "sha512-p0blsUs98FSnIXgYk8KMAbyBTWCHdZj4PyvhrUuO3vU4aNAnjlwunDyrqVhmP8gXA3k0oSUru4UAmXXKnN5j4g=="], - "@tanstack/ai-openrouter": ["@tanstack/ai-openrouter@0.22.0", "", { "dependencies": { "@openrouter/sdk": "0.13.20", "@tanstack/ai-utils": "^0.4.2" }, "peerDependencies": { "@tanstack/ai": "^0.69.0" } }, "sha512-2mxpmeq3m42tzHvQ3OetUhezLtRcNvDq4b88zHIHAOudDZVnzmb7Eped7wr7RKRNrGcdAsQPKfNDgP/Md1FrOA=="], + "@tanstack/ai-openrouter": ["@tanstack/ai-openrouter@1.0.0", "", { "dependencies": { "@openrouter/sdk": "0.13.20", "@tanstack/ai-utils": "^1.0.0" }, "peerDependencies": { "@tanstack/ai": "^1.0.0" } }, "sha512-loPDL6RF0kuYOgjNNBnA5/0JSyGt7ox7N8gpf2O0Yd/VnKuP+Y0ahzkikdXR+gmktuVpsPZU5EEY3bC1Qpmn2g=="], - "@tanstack/ai-persistence": ["@tanstack/ai-persistence@0.9.0", "", { "dependencies": { "@tanstack/ai-utils": "^0.4.2" }, "peerDependencies": { "@tanstack/ai": "^0.69.0", "vitest": "^4.1.10" }, "optionalPeers": ["vitest"] }, "sha512-4uHrieFqLpLB6gWHSqx9Uwg6tQalyk5ZXzvh5U7V8GnL3YqvkK3Ma5RZaJ2DjBOJkx4NrLQ3WldZjpsQt1CusA=="], + "@tanstack/ai-persistence": ["@tanstack/ai-persistence@1.0.0", "", { "dependencies": { "@tanstack/ai-utils": "^1.0.0" }, "peerDependencies": { "@tanstack/ai": "^1.0.0", "vitest": "^4.1.10" }, "optionalPeers": ["vitest"] }, "sha512-IS+KFKm64fSGpL3wVIWK1d/OuB31NhNjVImCIdrFMYT6K4IEPfHvQchRzxXBeK4xUta5iki9bdOkQnvHjRQQmA=="], - "@tanstack/ai-sandbox": ["@tanstack/ai-sandbox@0.5.23", "", { "dependencies": { "@modelcontextprotocol/sdk": "^1.29.0", "@tanstack/ai-skills": "^0.1.19" }, "peerDependencies": { "@ngrok/ngrok": "^1.0.0", "@tanstack/ai": "^0.69.0", "@tanstack/ai-persistence": "^0.9.0", "vitest": "^4.1.10" }, "optionalPeers": ["@ngrok/ngrok", "@tanstack/ai-persistence", "vitest"] }, "sha512-lxKYX0RC660KJbOtJ8PEFuYq48A00EBXSED8zYEskyU6Gxf5HsTGn6opHSsvPOf7T7CaYfLQnUhifLJbh3jQVQ=="], + "@tanstack/ai-sandbox": ["@tanstack/ai-sandbox@1.0.0", "", { "dependencies": { "@modelcontextprotocol/sdk": "^1.29.0", "@tanstack/ai-skills": "^1.0.0", "@tanstack/ai-utils": "^1.0.0" }, "peerDependencies": { "@ngrok/ngrok": "^1.0.0", "@tanstack/ai": "^1.0.0", "@tanstack/ai-harness": "^0.1.0", "@tanstack/ai-persistence": "^1.0.0", "vitest": "^4.1.10" }, "optionalPeers": ["@ngrok/ngrok", "@tanstack/ai-harness", "@tanstack/ai-persistence", "vitest"] }, "sha512-sGi7v9wjVZRGveHFbHg33BgGNZi9WTrezxfBfi6FgwrjYMK9uHVrDgm5ebndxXWfd0BSc3b+p76g81TNNToRCw=="], - "@tanstack/ai-sandbox-daytona": ["@tanstack/ai-sandbox-daytona@0.3.4", "", { "dependencies": { "@daytona/sdk": "^0.192.0" }, "peerDependencies": { "@tanstack/ai-sandbox": "^0.5.15" } }, "sha512-AumlHSt2v3GylYW3kcVG+VJaRH2tKqvt4szsJQIiEqPazgIleGQKcF9g2J3vuPOfVqv8CdCdxuN5fwNU0sSlwA=="], + "@tanstack/ai-sandbox-daytona": ["@tanstack/ai-sandbox-daytona@1.0.0", "", { "dependencies": { "@daytona/sdk": "^0.192.0" }, "peerDependencies": { "@tanstack/ai-sandbox": "^1.0.0" } }, "sha512-31fGIjZ8950LeMB7GpcjV6x6Ryp4E8oq7ds3FP8M+XJuSRNptpFpmoy2OC4v7GQ/nhj5O+j/EMux2GE6DWJtNQ=="], - "@tanstack/ai-sandbox-docker": ["@tanstack/ai-sandbox-docker@0.3.2", "", { "dependencies": { "dockerode": "^4.0.2" }, "peerDependencies": { "@tanstack/ai-sandbox": "^0.5.0" } }, "sha512-HD8uefHrL6zP+6lJhxDf6lBvZ6Effu/V38XgSvNOxE5Xbqt4qzLLMlL11/zuAw70275kgwoaFO5GE3GQyEnGxA=="], + "@tanstack/ai-sandbox-docker": ["@tanstack/ai-sandbox-docker@1.0.0", "", { "dependencies": { "dockerode": "^4.0.2" }, "peerDependencies": { "@tanstack/ai-sandbox": "^1.0.0" } }, "sha512-AsZBurgM52Tl5mooRofDFej3a5gIlHjFRYc8mAJh3Af+u4kt46XQqx7mpcEJPObMCx6gGoiNdJt0cg66YciM5g=="], - "@tanstack/ai-sandbox-sprites": ["@tanstack/ai-sandbox-sprites@0.2.5", "", { "peerDependencies": { "@tanstack/ai-sandbox": "^0.5.15" } }, "sha512-GaRsC05LCWyX2Knp29v85KKeIroyAEnTZnR4F1kEcmSUoWdKmF/GuPv4k/DkuBslFp1qcu/FDmgRIW7cGR+bdA=="], + "@tanstack/ai-sandbox-sprites": ["@tanstack/ai-sandbox-sprites@1.0.0", "", { "peerDependencies": { "@tanstack/ai-sandbox": "^1.0.0" } }, "sha512-BXbrVViCLDcjy0dll/qqKwK38M0sk8njTeNu4+sQw4n83B5tEP/EyDnzVdBJ8k5n0Y2cGfrqlSZJ/eXorKdeaA=="], - "@tanstack/ai-sandbox-vercel": ["@tanstack/ai-sandbox-vercel@0.2.5", "", { "dependencies": { "@vercel/sandbox": "^2.2.1" }, "peerDependencies": { "@tanstack/ai-sandbox": "^0.5.15" } }, "sha512-H89hBiH3bL+Yr3f5+mkoLOgD5yJTGuO8WxAuXjQ/9r8uEiW1AorOgmrvfVTFq6whe1CguSGTsFEDbnxC5h+/FA=="], + "@tanstack/ai-sandbox-vercel": ["@tanstack/ai-sandbox-vercel@1.0.0", "", { "dependencies": { "@vercel/sandbox": "^2.2.1" }, "peerDependencies": { "@tanstack/ai-sandbox": "^1.0.0" } }, "sha512-hwcEC7V1E4TLLC0He5JLNUAVM2I3ZOKgi78if0Woevd1eS7mL1xWFtYM3mU4Ohrr5C7HT3UKlfKwoZyZuR9p6w=="], - "@tanstack/ai-skills": ["@tanstack/ai-skills@0.1.19", "", { "dependencies": { "@tanstack/ai": "^0.69.0" }, "peerDependencies": { "vitest": "^4.1.10", "zod": "^3.0.0 || ^4.0.0" }, "optionalPeers": ["vitest"] }, "sha512-cMsbeDdY8NclfFiiN0ZEc8H1FS7NEo+LQzF4ANnA20gGCmnO71EMzjTkoesicKlKC3gHctbDw7D7vPCZOguO1Q=="], + "@tanstack/ai-skills": ["@tanstack/ai-skills@1.0.0", "", { "dependencies": { "@tanstack/ai": "^1.0.0" }, "peerDependencies": { "@tanstack/ai-harness": "^0.1.0", "vitest": "^4.1.10", "zod": "^3.0.0 || ^4.0.0" }, "optionalPeers": ["@tanstack/ai-harness", "vitest"] }, "sha512-DPId6O55a7KgOBW0kPfCndV5l8NHA2Dg2iLjT7XZT+L+OXoX4JiZSH6jn8FcyljVyIcoSHPbaqoY40IsQ8OdvQ=="], - "@tanstack/ai-utils": ["@tanstack/ai-utils@0.4.2", "", {}, "sha512-ucSgSK9kLloInqr/7s0VZlp1IFtbpgPnskISvnLXti/hc8koqXc4Zv9pfTTc8V80WzIKYWMf+ZgSIDeFM50eCw=="], + "@tanstack/ai-utils": ["@tanstack/ai-utils@1.0.0", "", {}, "sha512-0Nl9rUmS3zTGKP8dPmyS2zJ18mieAheKqS9V+390vjViJvD6kpP+1gdCdHkNUqQsSzSFvb32QW8bkTVCAOv0qA=="], "@tanstack/devtools": ["@tanstack/devtools@0.15.0", "", { "dependencies": { "@neodrag/core": "3.0.0-next.11", "@neodrag/solid": "3.0.0-next.11", "@solid-primitives/event-listener": "^2.4.3", "@solid-primitives/keyboard": "^1.3.3", "@solid-primitives/resize-observer": "^2.1.3", "@tanstack/devtools-client": "0.0.8", "@tanstack/devtools-event-bus": "0.4.3", "@tanstack/devtools-ui": "0.7.1", "clsx": "^2.1.1", "goober": "^2.1.16", "solid-js": "^1.9.9" }, "bin": { "intent": "./bin/intent.js" } }, "sha512-KyS7Qi82Eq9yG8C+uae3ikAPb+D5rfzVhvOOjXVAHZ/64s6TWJlDWpREw1FrHIYZaI7paErpsExSSKlF5BKA/A=="], @@ -1432,7 +1432,7 @@ "@tanstack/markdown": ["@tanstack/markdown@1.0.0", "", { "peerDependencies": { "octane": ">=0.1.12", "react": ">=18" }, "optionalPeers": ["octane", "react"] }, "sha512-zE07RepFMbSQlHdnE82twgiVOKy50rP6a/X9UPOOBt8Z8x9lzaPvGpn2zioCnjXMvNt/IQkW5sSalXQ25C/FKw=="], - "@tanstack/openai-base": ["@tanstack/openai-base@0.14.0", "", { "dependencies": { "@tanstack/ai-utils": "^0.4.2", "openai": "^7.30.0" }, "peerDependencies": { "@tanstack/ai": "^0.69.0" } }, "sha512-Uw6oo0COZJbCPKGKJB2RAEu+6X0B7CWLlEHo/fpYfSPEQ9dRDTB5c8yYkqqkiAa56uMj/9rJg0awfWNuw5O1bg=="], + "@tanstack/openai-base": ["@tanstack/openai-base@1.0.0", "", { "dependencies": { "@tanstack/ai-utils": "^1.0.0", "openai": "^7.30.0" }, "peerDependencies": { "@tanstack/ai": "^1.0.0" } }, "sha512-PCihnJASpykPy72tI3VkZX1VI4AxutXPRzr/yyWsCXPXnM7NIN8Nc/YgLqkYm+2wNDNQumX4vXge2iIsoYF+KQ=="], "@tanstack/pacer": ["@tanstack/pacer@0.23.1", "", { "dependencies": { "@tanstack/devtools-event-client": "^0.5.0", "@tanstack/store": "^0.11.2" } }, "sha512-ROZj19aPPGYebeRPhFZNRlqNaR2e4vkcLbgiXfbi5uJu8WiW/etsYmkcAcoSQRGOwGZbbioWRX7xssewDRVRhw=="], @@ -3160,9 +3160,9 @@ "@astralbeam/example-todos/vitest": ["vitest@5.0.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/mocker": "5.0.4", "chai": "^6.3.0", "es-module-lexer": "^2.3.2", "expect-type": "^1.4.0", "magic-string": "^1.4.3", "obug": "^3.0.0", "picomatch": "^4.0.7", "std-env": "^4.3.0", "tinybench": "^6.2.2", "tinyexec": "^1.3.1", "tinyglobby": "^0.2.17", "why-is-node-running": "3.2.1" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "5.0.4", "@vitest/browser-preview": "5.0.4", "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0", "@vitest/coverage-istanbul": "5.0.4", "@vitest/coverage-v8": "5.0.4", "@vitest/ui": "5.0.4", "happy-dom": "*", "jsdom": "*", "vite": "^6.4.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "./vitest.mjs" } }, "sha512-z0WxZRV2Sg48wF0FSgMq3TFG8JS38/Qr96vZVZdX6+Em9raQTvnXQvdTFJe2m3rfyJnEHeHQ/snceIdi8D1pVg=="], - "@astralbeam/platform/vitest": ["vitest@5.0.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/mocker": "5.0.4", "chai": "^6.3.0", "es-module-lexer": "^2.3.2", "expect-type": "^1.4.0", "magic-string": "^1.4.3", "obug": "^3.0.0", "picomatch": "^4.0.7", "std-env": "^4.3.0", "tinybench": "^6.2.2", "tinyexec": "^1.3.1", "tinyglobby": "^0.2.17", "why-is-node-running": "3.2.1" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "5.0.4", "@vitest/browser-preview": "5.0.4", "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0", "@vitest/coverage-istanbul": "5.0.4", "@vitest/coverage-v8": "5.0.4", "@vitest/ui": "5.0.4", "happy-dom": "*", "jsdom": "*", "vite": "^6.4.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "./vitest.mjs" } }, "sha512-z0WxZRV2Sg48wF0FSgMq3TFG8JS38/Qr96vZVZdX6+Em9raQTvnXQvdTFJe2m3rfyJnEHeHQ/snceIdi8D1pVg=="], + "@astralbeam/platform/@effect/vitest": ["@effect/vitest@4.0.5", "", { "peerDependencies": { "effect": "^4.0.5", "vitest": ">=5.0.0 <6.0.0" } }, "sha512-q64paZ/i4V2NiRhTyqCmT2KiDbFjlxc2EZrZca8GGt+Nf8844946HyJM5bPk/q+YeRkFSxIrlCs+J+LSfky/wA=="], - "@astralbeam/sdk/@tanstack/ai": ["@tanstack/ai@0.68.0", "", { "dependencies": { "@ag-ui/core": "1.0.0", "@standard-schema/spec": "^1.1.0", "@tanstack/ai-event-client": "^0.13.1", "@tanstack/ai-utils": "^0.4.1", "fast-json-patch": "^3.1.1", "partial-json": "^0.1.7" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0" }, "optionalPeers": ["@opentelemetry/api"] }, "sha512-l++ORsrL5dAC/nNJ2ZyQiTkDmxNo2Xh0lsS4m0QbWzUVyvmSmFriDemqQmAiko+zMvVeeQ8QlTXUMOLLPfZyEQ=="], + "@astralbeam/platform/vitest": ["vitest@5.0.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/mocker": "5.0.4", "chai": "^6.3.0", "es-module-lexer": "^2.3.2", "expect-type": "^1.4.0", "magic-string": "^1.4.3", "obug": "^3.0.0", "picomatch": "^4.0.7", "std-env": "^4.3.0", "tinybench": "^6.2.2", "tinyexec": "^1.3.1", "tinyglobby": "^0.2.17", "why-is-node-running": "3.2.1" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "5.0.4", "@vitest/browser-preview": "5.0.4", "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0", "@vitest/coverage-istanbul": "5.0.4", "@vitest/coverage-v8": "5.0.4", "@vitest/ui": "5.0.4", "happy-dom": "*", "jsdom": "*", "vite": "^6.4.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "./vitest.mjs" } }, "sha512-z0WxZRV2Sg48wF0FSgMq3TFG8JS38/Qr96vZVZdX6+Em9raQTvnXQvdTFJe2m3rfyJnEHeHQ/snceIdi8D1pVg=="], "@astralbeam/sdk/@tanstack/react-pacer": ["@tanstack/react-pacer@0.23.0", "", { "dependencies": { "@tanstack/pacer": "0.22.0", "@tanstack/react-store": "^0.11.1" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-Ec3h+kT8kYlpGb1M8F7836q+udE/M2RmE8KLr1rhs+UDBgvxjfmyR2bIUeaDEe9Zn1YXNI986RbErXyr5fEJIg=="], @@ -3206,8 +3206,6 @@ "@dotenvx/dotenvx/undici": ["undici@7.30.0", "", {}, "sha512-dkrQXeHSaoamnItlYbmzG0wFYrM0ZwDxCIg0A7aKjTyyhh9svRzCNFEzV+Vm05/yehjCzjDZ31KXfGEjYSztDQ=="], - "@effect/vitest/vitest": ["vitest@5.0.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/mocker": "5.0.4", "chai": "^6.3.0", "es-module-lexer": "^2.3.2", "expect-type": "^1.4.0", "magic-string": "^1.4.3", "obug": "^3.0.0", "picomatch": "^4.0.7", "std-env": "^4.3.0", "tinybench": "^6.2.2", "tinyexec": "^1.3.1", "tinyglobby": "^0.2.17", "why-is-node-running": "3.2.1" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "5.0.4", "@vitest/browser-preview": "5.0.4", "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0", "@vitest/coverage-istanbul": "5.0.4", "@vitest/coverage-v8": "5.0.4", "@vitest/ui": "5.0.4", "happy-dom": "*", "jsdom": "*", "vite": "^6.4.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "./vitest.mjs" } }, "sha512-z0WxZRV2Sg48wF0FSgMq3TFG8JS38/Qr96vZVZdX6+Em9raQTvnXQvdTFJe2m3rfyJnEHeHQ/snceIdi8D1pVg=="], - "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], "@grpc/grpc-js/@grpc/proto-loader": ["@grpc/proto-loader@0.8.1", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.5.5", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg=="], @@ -3322,8 +3320,6 @@ "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "@tanstack/ai-client/@tanstack/ai": ["@tanstack/ai@0.68.0", "", { "dependencies": { "@ag-ui/core": "1.0.0", "@standard-schema/spec": "^1.1.0", "@tanstack/ai-event-client": "^0.13.1", "@tanstack/ai-utils": "^0.4.1", "fast-json-patch": "^3.1.1", "partial-json": "^0.1.7" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0" }, "optionalPeers": ["@opentelemetry/api"] }, "sha512-l++ORsrL5dAC/nNJ2ZyQiTkDmxNo2Xh0lsS4m0QbWzUVyvmSmFriDemqQmAiko+zMvVeeQ8QlTXUMOLLPfZyEQ=="], - "@tanstack/ai-event-client/@tanstack/devtools-event-client": ["@tanstack/devtools-event-client@0.4.4", "", { "bin": { "intent": "./bin/intent.js" } }, "sha512-6T5Yop/793YI+H+5J8Hsyj4kCih9sl4t3ElLgKioW5hk3ocn+ZdSJ94tT7vL7uabxSugWYBZlOTMPzEw2puvQw=="], "@tanstack/devtools-bundler-core/oxc-parser": ["oxc-parser@0.120.0", "", { "dependencies": { "@oxc-project/types": "^0.120.0" }, "optionalDependencies": { "@oxc-parser/binding-android-arm-eabi": "0.120.0", "@oxc-parser/binding-android-arm64": "0.120.0", "@oxc-parser/binding-darwin-arm64": "0.120.0", "@oxc-parser/binding-darwin-x64": "0.120.0", "@oxc-parser/binding-freebsd-x64": "0.120.0", "@oxc-parser/binding-linux-arm-gnueabihf": "0.120.0", "@oxc-parser/binding-linux-arm-musleabihf": "0.120.0", "@oxc-parser/binding-linux-arm64-gnu": "0.120.0", "@oxc-parser/binding-linux-arm64-musl": "0.120.0", "@oxc-parser/binding-linux-ppc64-gnu": "0.120.0", "@oxc-parser/binding-linux-riscv64-gnu": "0.120.0", "@oxc-parser/binding-linux-riscv64-musl": "0.120.0", "@oxc-parser/binding-linux-s390x-gnu": "0.120.0", "@oxc-parser/binding-linux-x64-gnu": "0.120.0", "@oxc-parser/binding-linux-x64-musl": "0.120.0", "@oxc-parser/binding-openharmony-arm64": "0.120.0", "@oxc-parser/binding-wasm32-wasi": "0.120.0", "@oxc-parser/binding-win32-arm64-msvc": "0.120.0", "@oxc-parser/binding-win32-ia32-msvc": "0.120.0", "@oxc-parser/binding-win32-x64-msvc": "0.120.0" } }, "sha512-WyPWZlcIm+Fkte63FGfgFB8mAAk33aH9h5N9lphXVOHSXEBFFsmYdOBedVKly363aWABjZdaj/m9lBfEY4wt+w=="], @@ -3614,14 +3610,6 @@ "@dotenvx/dotenvx/open/define-lazy-prop": ["define-lazy-prop@2.0.0", "", {}, "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og=="], - "@effect/vitest/vitest/@vitest/mocker": ["@vitest/mocker@5.0.4", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.31", "@vitest/spy": "5.0.4", "estree-walker": "^3.0.3", "magic-string": "^1.4.3" }, "peerDependencies": { "msw": "^2.4.9 || ^3.0.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-tqAlrYWce4iUxhr3ncNohejtCTOcHeE8XWrSNZY3OhPoDn01yeGv+gnaBy64ATp7o6pb03eIbBAjzmibpc9vIQ=="], - - "@effect/vitest/vitest/magic-string": ["magic-string@1.4.3", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.6.0" } }, "sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ=="], - - "@effect/vitest/vitest/tinybench": ["tinybench@6.2.2", "", {}, "sha512-nIZ2N2dFpjBsg2hgitKu7mA+FvsIJuGJOwysRngmVRRmWgR/UyPjwJj8c/fT/dhm8EkHnF5EOogRf+0Z2qrimA=="], - - "@effect/vitest/vitest/why-is-node-running": ["why-is-node-running@3.2.1", "", { "bin": { "why-is-node-running": "cli.js" } }, "sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ=="], - "@modelcontextprotocol/sdk/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], "@scalar/agent-chat/@scalar/json-magic/undici": ["undici@7.29.1", "", {}, "sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q=="], @@ -3764,8 +3752,6 @@ "@dotenvx/dotenvx/conf/dot-prop/is-obj": ["is-obj@2.0.0", "", {}, "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w=="], - "@effect/vitest/vitest/@vitest/mocker/@vitest/spy": ["@vitest/spy@5.0.4", "", {}, "sha512-MZKk/gSkm2q+ICobMxCpH1mA+cCAWm8WV7UhxlqGjq/sJ3AGc1BqFOs5CRVXxH78wQtme9q/NAAgBquj4AcyMw=="], - "pkg-up/find-up/locate-path/p-locate": ["p-locate@3.0.0", "", { "dependencies": { "p-limit": "^2.0.0" } }, "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ=="], "pkg-up/find-up/locate-path/path-exists": ["path-exists@3.0.0", "", {}, "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ=="], diff --git a/examples/todos/e2e/pages/chat-widget.ts b/examples/todos/e2e/pages/chat-widget.ts index 42e1e166c..66b2609c6 100644 --- a/examples/todos/e2e/pages/chat-widget.ts +++ b/examples/todos/e2e/pages/chat-widget.ts @@ -142,8 +142,10 @@ export function chatWidget(page: Page) { await test.step(`send "${text}"`, async () => { await this.waitForReady() await composer.fill(text) - await composer.press("Enter") - await expect(root.getByText(text, { exact: false }).first()).toBeVisible() + await sendButton.click() + await expect( + root.locator('[data-slot="message"][data-align="end"]').getByText(text, { exact: true }), + ).toBeVisible() }) }, diff --git a/platform/package.json b/platform/package.json index 6d9f9e7e8..de08728cd 100644 --- a/platform/package.json +++ b/platform/package.json @@ -55,19 +55,19 @@ "@pydantic/genai-prices": "^0.1.12", "@react-email/render": "2.1.0", "@scalar/api-reference-react": "^0.9.81", - "@tanstack/ai": "^0.69.0", - "@tanstack/ai-anthropic": "^0.22.0", - "@tanstack/ai-gemini": "^0.38.0", - "@tanstack/ai-grok": "^0.22.0", - "@tanstack/ai-mcp": "0.9.0", - "@tanstack/ai-openai": "^0.30.0", - "@tanstack/ai-openrouter": "^0.22.0", - "@tanstack/ai-persistence": "0.9.0", - "@tanstack/ai-sandbox": "^0.5.23", - "@tanstack/ai-sandbox-daytona": "^0.3.4", - "@tanstack/ai-sandbox-docker": "^0.3.2", - "@tanstack/ai-sandbox-sprites": "^0.2.5", - "@tanstack/ai-sandbox-vercel": "^0.2.5", + "@tanstack/ai": "^1.0.0", + "@tanstack/ai-anthropic": "^1.0.0", + "@tanstack/ai-gemini": "^1.0.0", + "@tanstack/ai-grok": "^1.0.0", + "@tanstack/ai-mcp": "^1.0.0", + "@tanstack/ai-openai": "^1.0.0", + "@tanstack/ai-openrouter": "^1.0.0", + "@tanstack/ai-persistence": "^1.0.0", + "@tanstack/ai-sandbox": "^1.0.0", + "@tanstack/ai-sandbox-daytona": "^1.0.0", + "@tanstack/ai-sandbox-docker": "^1.0.0", + "@tanstack/ai-sandbox-sprites": "^1.0.0", + "@tanstack/ai-sandbox-vercel": "^1.0.0", "@tanstack/highlight": "^1.1.0", "@tanstack/markdown": "^1.0.0", "@tanstack/react-devtools": "^0.10.13", diff --git a/platform/public/api/openapi.json b/platform/public/api/openapi.json index 87fe80efe..7141615b4 100644 --- a/platform/public/api/openapi.json +++ b/platform/public/api/openapi.json @@ -13260,10 +13260,6 @@ } ] }, - "data": { - "type": "object", - "description": "Legacy mirror of forwardedProps sent by TanStack AI clients." - }, "state": {}, "parentRunId": { "type": "string" diff --git a/platform/src/lib/chat/README.md b/platform/src/lib/chat/README.md index 2f2219522..7d046d1f2 100644 --- a/platform/src/lib/chat/README.md +++ b/platform/src/lib/chat/README.md @@ -176,7 +176,7 @@ For example, an assistant's `metadata` can contain: Optional fields also preserve input tool declarations, opaque `modelMessages`, provider instance/type, API protocol, model identity, and usage. Provider continuation data is replayed only with the same provider instance, protocol, and model. Otherwise the adapter uses portable canonical parts. -Assistant provenance usage includes a decimal-string `costUsd` when a reported cost or resolved token rate is available. This snapshots the generation-time cost for retained history. Directory totals exclude drafts and temporarily count unpriceable settled replies as zero. +Assistant provenance usage includes a decimal-string `costUsd` when a reported cost or resolved token rate is available. This snapshots the generation-time cost for retained history. Reported usage from failed model calls is retained and recorded once per model phase, including partial Anthropic input/cache usage. One-hour cache writes are priced as a subset of total cache writes. Directory totals exclude drafts and temporarily count unpriceable settled replies as zero. `initiatorTenantUserId` identifies the actor of this foreground invocation. A participant resolving the last browser response can start a continuation, so that actor can differ from the original user-message author. diff --git a/platform/src/lib/chat/adapter-responses.test.ts b/platform/src/lib/chat/adapter-responses.test.ts index 4ba33093a..1ad49e89d 100644 --- a/platform/src/lib/chat/adapter-responses.test.ts +++ b/platform/src/lib/chat/adapter-responses.test.ts @@ -11,7 +11,7 @@ const responsesRequestSchema = Schema.Struct({ describe("OpenAI Responses compatibility", () => { test.each([ - { modelId: "gpt-5.6-terra", include: ["reasoning.encrypted_content"] }, + { modelId: "gpt-6.1-sol", include: ["reasoning.encrypted_content"] }, { modelId: "custom-deployment", include: undefined }, ])("preserves native request semantics for $modelId", async ({ modelId, include }) => { const requests: (typeof responsesRequestSchema.Type)[] = [] @@ -35,7 +35,11 @@ describe("OpenAI Responses compatibility", () => { modelId, fetch: responsesFetch, }) - for await (const _event of chat({ adapter, messages: [{ role: "user", content: "Hello" }] })) { + for await (const _event of chat({ + threadId: "provider-test", + adapter, + messages: [{ role: "user", content: "Hello" }], + })) { // Drain the run so the SDK maps and sends its request to the synthetic provider boundary. } expect(requests).toHaveLength(1) diff --git a/platform/src/lib/chat/adapter.test.ts b/platform/src/lib/chat/adapter.test.ts index dad55de81..ea15a666c 100644 --- a/platform/src/lib/chat/adapter.test.ts +++ b/platform/src/lib/chat/adapter.test.ts @@ -97,7 +97,11 @@ describe("chat provider request routing", () => { fetch: configuredFetch, }) const events: StreamChunk[] = [] - for await (const event of chat({ adapter, messages: [{ role: "user", content: "Hello" }] })) + for await (const event of chat({ + threadId: "provider-test", + adapter, + messages: [{ role: "user", content: "Hello" }], + })) events.push(event) expect(events.some((event) => event.type === EventType.RUN_ERROR)).toBe(true) expect(requests).toHaveLength(1) @@ -219,6 +223,7 @@ describe("native Gemini and Grok turns", () => { }).server(() => ({ ok: true })) const events: StreamChunk[] = [] for await (const event of chat({ + threadId: "provider-test", adapter, tools: [lookup], agentLoopStrategy: maxIterations(2), diff --git a/platform/src/lib/chat/adapter.ts b/platform/src/lib/chat/adapter.ts index f7776f8ae..99d064902 100644 --- a/platform/src/lib/chat/adapter.ts +++ b/platform/src/lib/chat/adapter.ts @@ -178,19 +178,10 @@ function createChatProviderAdapter(configuration: ChatModelConfiguration, native } export function createChatAdapter(model: ChatModelConfiguration, web?: ChatWebState) { - const provider = createChatProviderAdapter( + const adapter = createChatProviderAdapter( web ? { ...model, fetch: (input, init) => fetchChatWeb(model, web, input, init) } : model, web !== undefined, ) - const adapter = Object.assign(provider, { - inputModalities: provider.inputModalities ?? (["text", "image", "document"] as const), - // TextAdapter omits `| undefined` here, as for inputModalities before, so chat() rejects every - // adapter under exactOptionalPropertyTypes. All-off equals absent. https://github.com/TanStack/ai/issues/1702 - midConversationChannels: provider.midConversationChannels ?? { - tools: false, - systemPrompts: false, - }, - }) if (!web) return adapter const stream = adapter.chatStream.bind(adapter) as ( options: TextOptions, diff --git a/platform/src/lib/chat/chat.test.ts b/platform/src/lib/chat/chat.test.ts index bab65a7ee..3b68c9197 100644 --- a/platform/src/lib/chat/chat.test.ts +++ b/platform/src/lib/chat/chat.test.ts @@ -430,7 +430,6 @@ describe("Chat.run", () => { type: EventType.RUN_ERROR, runId: "run", message, - error: { message }, }) }).pipe(Effect.provide(chatTestLayer({ agent: sandboxedAgent, model: CHAT_TEST_MODEL }))), ) @@ -452,7 +451,6 @@ describe("Chat.run", () => { runId: "run", message: "Request aborted", code: "aborted", - error: { message: "Request aborted", code: "aborted" }, }) }).pipe(Effect.provide(chatTestLayer({ agent: sandboxedAgent, model: CHAT_TEST_MODEL }))), ) diff --git a/platform/src/lib/chat/chat.ts b/platform/src/lib/chat/chat.ts index 66f5ad010..8fa873e44 100644 --- a/platform/src/lib/chat/chat.ts +++ b/platform/src/lib/chat/chat.ts @@ -55,7 +55,7 @@ import type { ChatParams, ChatPrincipal } from "./types" import { IS_DEVELOPMENT_SERVER } from "@/lib/runtime/environment.server" import { errorReason } from "@/lib/runtime/failure-report.server" import { UsageLimits } from "@/lib/usage-limits/usage-limits.server" -import { chatUsageLimitGuard } from "./usage-limits" +import { chatFailedUsageMiddleware, chatUsageLimitGuard } from "./usage-limits" // Only recognized provider codes get actionable copy. Provider messages can contain credentials. const modelErrorMessages: Readonly> = { @@ -387,6 +387,10 @@ export class Chat extends Context.Service< ), }) turn = managed.state + const accounting = [ + ...managed.middleware, + ...(usageLimitGuard ? [usageLimitGuard.middleware] : []), + ] return chatEventStream((abortController) => { const source = chat({ adapter: createChatAdapter(model, web), @@ -398,11 +402,12 @@ export class Chat extends Context.Service< // A sandbox command or publication can depend on an earlier tool's file write. // https://github.com/TanStack/ai/blob/main/packages/ai/CHANGELOG.md#0640 toolExecution: "sequential", + repairToolInput: false, // Interrupt snapshots preserve original uploads after provider normalization. middleware: [ + chatFailedUsageMiddleware(accounting), createChatAttachmentSnapshotMiddleware(inputMessages, history), - ...managed.middleware, - ...(usageLimitGuard ? [usageLimitGuard.middleware] : []), + ...accounting, { name: "mcp-presentation", onStart: (ctx) => @@ -450,7 +455,6 @@ export class Chat extends Context.Service< ...(chunk.threadId === undefined ? {} : { threadId: chunk.threadId }), ...(chunk.runId === undefined ? {} : { runId: chunk.runId }), message, - error: aborted ? { message, code: "aborted" } : { message }, ...(aborted ? { code: "aborted" } : {}), } return Effect.as( @@ -459,6 +463,12 @@ export class Chat extends Context.Service< organizationId: principal.organization.id, providerId: model.providerId, code: errorReason(chunk), + ...(typeof chunk.metadata?.tanstack?.errorType === "string" + ? { errorType: chunk.metadata.tanstack.errorType.slice(0, 128) } + : {}), + ...(typeof chunk.metadata?.tanstack?.requestId === "string" + ? { requestId: chunk.metadata.tanstack.requestId.slice(0, 256) } + : {}), }), ), runError, diff --git a/platform/src/lib/chat/sandbox/sandbox.ts b/platform/src/lib/chat/sandbox/sandbox.ts index e1089a3ee..18f1ef613 100644 --- a/platform/src/lib/chat/sandbox/sandbox.ts +++ b/platform/src/lib/chat/sandbox/sandbox.ts @@ -37,12 +37,15 @@ const writeChatSandboxUploads = Effect.fn("writeChatSandboxUploads")(function* ( if (uploads.length === 0) return const directory = `${resolveHarnessCwd(handle)}/${CHAT_ATTACHMENT_UPLOAD_DIRECTORY}` // One level below a workspace that already exists, so this needs no recursive `mkdir`. - yield* chatSandboxCall(() => handle.fs.mkdir(directory), CHAT_SANDBOX_FILE_TIMEOUT_MS) + yield* chatSandboxCall( + (signal) => handle.fs.mkdir(directory, { signal }), + CHAT_SANDBOX_FILE_TIMEOUT_MS, + ) yield* Effect.forEach( uploads, (upload) => chatSandboxCall( - () => handle.fs.write(`${directory}/${upload.handle}`, upload.bytes), + (signal) => handle.fs.write(`${directory}/${upload.handle}`, upload.bytes, { signal }), CHAT_SANDBOX_FILE_TIMEOUT_MS, ), { concurrency: "unbounded", discard: true }, diff --git a/platform/src/lib/chat/sandbox/tools.test.ts b/platform/src/lib/chat/sandbox/tools.test.ts index 4e8385b7f..60be7df41 100644 --- a/platform/src/lib/chat/sandbox/tools.test.ts +++ b/platform/src/lib/chat/sandbox/tools.test.ts @@ -1,6 +1,10 @@ -import { describe, expect, it } from "vitest" +import { describe, expect, it, vi } from "vitest" -import { clampSandboxText } from "./tools.ts" +import { Context, Effect } from "effect" +import type { SandboxFsOptions, SandboxHandle } from "@tanstack/ai-sandbox" + +import { CHAT_SANDBOX_MAX_ARTIFACT_BYTES } from "./constants.ts" +import { clampSandboxText, createChatSandboxTools } from "./tools.ts" describe("clampSandboxText", () => { it("elides the middle so a failing command's last line survives", () => { @@ -10,3 +14,42 @@ describe("clampSandboxText", () => { expect(clamped.text.endsWith("THE-ERROR")).toBe(true) }) }) + +it.each([ + { size: CHAT_SANDBOX_MAX_ARTIFACT_BYTES + 1, bytes: 0, reads: 0, published: false }, + { size: 4, bytes: CHAT_SANDBOX_MAX_ARTIFACT_BYTES + 1, reads: 1, published: false }, + { size: 4, bytes: 4, reads: 1, published: true }, + { size: new Error("Provider metadata unavailable"), bytes: 0, reads: 0, published: false }, +])( + "publishes only readable artifacts within the size limit: %j", + async ({ size, bytes, reads, published }) => { + const readBytes = vi.fn(() => Promise.resolve(new Uint8Array(bytes))) + const publish = vi.fn(() => Effect.succeed("stored-file")) + const lstat = vi.fn((_path: string, _options?: SandboxFsOptions) => + size instanceof Error ? Promise.reject(size) : Promise.resolve({ type: "file", size }), + ) + const handle = { + workspaceRoot: "/workspace", + fs: { lstat, readBytes }, + } as unknown as SandboxHandle + const tools = createChatSandboxTools({ + session: { acquire: () => Effect.succeed(handle), prepareUploads: () => Effect.void }, + services: Context.empty(), + publishArtifact: publish, + }) + const tool = tools.find((tool) => tool.name === "sandbox_publish_artifact")! + const result: unknown = await tool.execute!({ path: "result.txt" }) + expect(readBytes).toHaveBeenCalledTimes(reads) + expect(publish).toHaveBeenCalledTimes(published ? 1 : 0) + expect(lstat.mock.calls[0]?.[0]).toBe("/workspace/result.txt") + expect(lstat.mock.calls[0]?.[1]?.signal).toBeInstanceOf(AbortSignal) + expect(result).toHaveProperty( + published ? "fileId" : "refusal", + published + ? "stored-file" + : size instanceof Error + ? "The file could not be read. Check the path with sandbox_list_files." + : `Artifacts are limited to ${CHAT_SANDBOX_MAX_ARTIFACT_BYTES} bytes. Compress or split the file.`, + ) + }, +) diff --git a/platform/src/lib/chat/sandbox/tools.ts b/platform/src/lib/chat/sandbox/tools.ts index 2b0be1507..277c7cbc3 100644 --- a/platform/src/lib/chat/sandbox/tools.ts +++ b/platform/src/lib/chat/sandbox/tools.ts @@ -1,4 +1,4 @@ -import { type AnyServerTool, toolDefinition } from "@tanstack/ai" +import { type AnyServerTool, type ToolExecutionContext, toolDefinition } from "@tanstack/ai" import { resolveHarnessCwd, type SandboxHandle } from "@tanstack/ai-sandbox" import { Clock, type Context, Effect, Result, Schema } from "effect" @@ -17,10 +17,12 @@ import { } from "./constants" import { ChatSandboxOperationFailed, type ChatSandboxUnavailable } from "./errors.ts" import { chatSandboxCall, logChatSandboxFailure } from "./operations" -import type { ChatSandboxSession, ChatSandboxStatus } from "./sandbox" +import type { ChatSandboxSession } from "./sandbox" import { resolveSandboxPath } from "./paths" import { chatToolInputSchema } from "../tool-schema" +const sandboxArtifactLimitMessage = `Artifacts are limited to ${CHAT_SANDBOX_MAX_ARTIFACT_BYTES} bytes. Compress or split the file.` + const sandboxPath = NonEmptyStringSchema.pipe( Schema.check(Schema.isMaxLength(CHAT_SANDBOX_MAX_PATH_LENGTH)), ) @@ -72,12 +74,6 @@ const RunSandboxCommandInputSchema = chatToolInputSchema( }), ) -/** What the sandbox tools need from TanStack's tool execution context, which is itself optional. */ -interface SandboxToolContext { - emitCustomEvent: (name: string, value: ChatSandboxStatus) => void - abortSignal?: AbortSignal | undefined -} - type SandboxToolEffect = Effect.Effect /** The sandbox tools, which run here rather than in the host page. A refused path, a failed @@ -97,7 +93,7 @@ export function createChatSandboxTools(input: { */ const runSandboxTool = ( tool: string, - context: SandboxToolContext | undefined, + context: ToolExecutionContext | undefined, effect: (handle: SandboxHandle) => SandboxToolEffect, ) => runPromise( @@ -124,7 +120,7 @@ export function createChatSandboxTools(input: { "Create or replace a file in the sandbox. Missing parent directories are created. Write " + "the whole file: there is no partial edit.", inputSchema: WriteSandboxFileInputSchema, - }).server(({ path, content }, context) => { + }).server(({ path, content }, context) => { if (content.length > CHAT_SANDBOX_MAX_WRITE_CHARACTERS) { return { refusal: @@ -150,7 +146,7 @@ export function createChatSandboxTools(input: { } } yield* chatSandboxCall( - () => handle.fs.write(resolved.path, content), + (signal) => handle.fs.write(resolved.path, content, { signal }), CHAT_SANDBOX_FILE_TIMEOUT_MS, ) return { @@ -167,13 +163,13 @@ export function createChatSandboxTools(input: { name: "sandbox_read_file", description: "Read a file from the sandbox. A long file comes back truncated in the middle.", inputSchema: ReadSandboxFileInputSchema, - }).server(({ path }, context) => + }).server(({ path }, context) => runSandboxTool("sandbox_read_file", context, (handle) => Effect.gen(function* () { const resolved = resolveSandboxPath(resolveHarnessCwd(handle), path) if ("refusal" in resolved) return resolved const content = yield* chatSandboxCall( - () => handle.fs.read(resolved.path), + (signal) => handle.fs.read(resolved.path, { signal }), CHAT_SANDBOX_FILE_TIMEOUT_MS, ) const clamped = clampSandboxText(content, CHAT_SANDBOX_MAX_FILE_CHARACTERS) @@ -191,14 +187,14 @@ export function createChatSandboxTools(input: { name: "sandbox_list_files", description: "List the files and directories directly inside a sandbox directory.", inputSchema: ListSandboxFilesInputSchema, - }).server(({ path }, context) => + }).server(({ path }, context) => runSandboxTool("sandbox_list_files", context, (handle) => Effect.gen(function* () { const root = resolveHarnessCwd(handle) const resolved = resolveSandboxPath(root, path ?? root) if ("refusal" in resolved) return resolved const entries = yield* chatSandboxCall( - () => handle.fs.list(resolved.path), + (signal) => handle.fs.list(resolved.path, { signal }), CHAT_SANDBOX_FILE_TIMEOUT_MS, ) return { @@ -220,7 +216,7 @@ export function createChatSandboxTools(input: { "Run a shell command in the sandbox and read its output. A non-zero exit code comes back " + "as a result rather than an error, so check `exitCode` before trusting the output.", inputSchema: RunSandboxCommandInputSchema, - }).server(({ command, cwd }, context) => + }).server(({ command, cwd }, context) => runSandboxTool("sandbox_run_command", context, (handle) => Effect.gen(function* () { const root = resolveHarnessCwd(handle) @@ -268,23 +264,27 @@ export function createChatSandboxTools(input: { "JPEG, GIF, WebP) also renders inline in the conversation. Publish anything you generated " + "for the user instead of describing it.", inputSchema: PublishSandboxArtifactInputSchema, - }).server(({ path }, context) => + }).server(({ path }, context) => runSandboxTool("sandbox_publish_artifact", context, (handle) => Effect.gen(function* () { const resolved = resolveSandboxPath(resolveHarnessCwd(handle), path) if ("refusal" in resolved) return resolved - const read = yield* Effect.result( - chatSandboxCall(() => handle.fs.readBytes(resolved.path), CHAT_SANDBOX_FILE_TIMEOUT_MS), - ) - // The agent only learns the path did not work, and the reason goes to the log. - if (Result.isFailure(read)) { - yield* logChatSandboxFailure("publish_artifact", read.failure) - return { refusal: "The file could not be read. Check the path with sandbox_list_files." } + if (handle.fs.lstat) { + const stat = handle.fs.lstat.bind(handle.fs) + const info = yield* chatSandboxCall( + (signal) => stat(resolved.path, { signal }), + CHAT_SANDBOX_FILE_TIMEOUT_MS, + ) + if (info?.type === "file" && info.size > CHAT_SANDBOX_MAX_ARTIFACT_BYTES) + return { refusal: sandboxArtifactLimitMessage } } - const bytes = read.success + const bytes = yield* chatSandboxCall( + (signal) => handle.fs.readBytes(resolved.path, { signal }), + CHAT_SANDBOX_FILE_TIMEOUT_MS, + ) if (bytes.byteLength > CHAT_SANDBOX_MAX_ARTIFACT_BYTES) { return { - refusal: `Artifacts are limited to ${CHAT_SANDBOX_MAX_ARTIFACT_BYTES} bytes. Compress or split the file.`, + refusal: sandboxArtifactLimitMessage, } } // Sniffed from content, never from the extension, so a renamed file cannot change how @@ -299,7 +299,15 @@ export function createChatSandboxTools(input: { size: bytes.byteLength, fileId, } - }), + }).pipe( + Effect.catchTag("ChatSandboxOperationFailed", (error) => + logChatSandboxFailure("publish_artifact", error).pipe( + Effect.as({ + refusal: "The file could not be read. Check the path with sandbox_list_files.", + }), + ), + ), + ), ), ) diff --git a/platform/src/lib/chat/threads/commands.test.ts b/platform/src/lib/chat/threads/commands.test.ts index e6f0262c6..3214a5cc1 100644 --- a/platform/src/lib/chat/threads/commands.test.ts +++ b/platform/src/lib/chat/threads/commands.test.ts @@ -24,7 +24,6 @@ const commandParams: ChatParams = { messages: [{ id: "input", role: "user", content: "Hello" }], tools: [], state: undefined, - context: [], aguiContext: [], forwardedProps: { clientId: commandUserId }, } diff --git a/platform/src/lib/chat/threads/commands.ts b/platform/src/lib/chat/threads/commands.ts index a549b416e..8f12ed0f3 100644 --- a/platform/src/lib/chat/threads/commands.ts +++ b/platform/src/lib/chat/threads/commands.ts @@ -391,7 +391,6 @@ export const managedChatRunParams = Effect.fn("managedChatRunParams")(function* messages: [], tools, state: undefined, - context: [], aguiContext: [], forwardedProps: { agentId: `agent_${claim.scope.organizationId}_${admission.thread.agentId}`, diff --git a/platform/src/lib/chat/threads/stream.test.ts b/platform/src/lib/chat/threads/stream.test.ts index fbbf673b5..01ba70ec6 100644 --- a/platform/src/lib/chat/threads/stream.test.ts +++ b/platform/src/lib/chat/threads/stream.test.ts @@ -12,6 +12,7 @@ import { describe, expect, test } from "vitest" import { createChatAdapter } from "../adapter" import { chatToolInputSchema } from "../tool-schema" +import { chatFailedUsageMiddleware } from "../usage-limits" import type { ChatThreads } from "./threads" import type { ChatMessagePayload, ChatToolResolution, ChatWriterClaim } from "./schemas" import { managedChatDelivery, managedChatMiddleware } from "./stream" @@ -63,7 +64,9 @@ async function exerciseManagedStream(options: { failFinal?: boolean failSnapshot?: boolean failTool?: boolean + failBeforeText?: boolean invalidInput?: boolean + transformInput?: string | number failResult?: boolean unknownTool?: boolean snapshots?: boolean @@ -97,9 +100,10 @@ async function exerciseManagedStream(options: { const definition = toolDefinition({ name: options.unknownTool ? "different" : "change", description: "Test action", - inputSchema: options.invalidInput - ? chatToolInputSchema(Schema.Struct({ required: Schema.String })) - : { type: "object", properties: {} }, + inputSchema: + options.invalidInput || options.transformInput !== undefined + ? chatToolInputSchema(Schema.Struct({ required: Schema.String })) + : { type: "object", properties: {} }, }) const tool = options.browser ? definition.client() @@ -193,13 +197,25 @@ async function exerciseManagedStream(options: { } : {}), }) + const adapter = createChatAdapter(model) + if (options.failBeforeText) + adapter.chatStream = async function* () { + requests += 1 + yield await Promise.resolve({ + type: EventType.RUN_ERROR, + message: "Synthetic failure before text", + usage: { promptTokens: 100, completionTokens: 0, totalTokens: 100 }, + } satisfies StreamChunk) + } const source = chat({ - adapter: createChatAdapter(model), + adapter, + repairToolInput: false, messages: history, tools: options.refresh ? [] : [tool], threadId: managed.claim.threadId, runId: "run", middleware: [ + chatFailedUsageMiddleware(bridge.middleware), ...(options.snapshots ? [ { @@ -222,6 +238,13 @@ async function exerciseManagedStream(options: { } satisfies ChatMiddleware, ] : []), + { + name: "transform-input", + onBeforeToolCall: () => + options.transformInput === undefined + ? undefined + : { type: "transformArgs", args: { required: options.transformInput } }, + }, ...bridge.middleware, ], }) @@ -262,27 +285,31 @@ describe("managed TanStack persistence boundaries", () => { }, ) - test("commits decisions before server effects and outcomes before the next model call", async () => { - const result = await exerciseManagedStream({}) - expect(result.failed).toBe(false) - expect(result.executed).toBe(1) - expect(result.requests).toBe(2) - expect(result.results).toHaveLength(1) - expect(result.order.indexOf("decision")).toBeLessThan(result.order.indexOf("execute")) - expect(result.order.indexOf("result")).toBeLessThan(result.order.lastIndexOf("decision")) - expect( - result.chunks.findIndex((chunk) => chunk.type === EventType.TOOL_CALL_RESULT), - ).toBeLessThan( - result.chunks.findLastIndex((chunk) => chunk.type === EventType.TEXT_MESSAGE_START), - ) - expect( - result.saved.some((payload) => - payload.parts.some((part) => part.executionLocation === "server_api"), - ), - ).toBe(true) - expect(JSON.stringify(result.saved)).not.toContain("synthetic-test-key") - expect(result.saved.at(-1)?.provenance?.usage).toMatchObject({ costUsd: "0.000105" }) - }) + test.each([{}, { transformInput: "checked" }])( + "commits decisions before server effects and outcomes before the next model call: %j", + async (options) => { + const result = await exerciseManagedStream(options) + expect(result.failed).toBe(false) + expect(result.executed).toBe(1) + expect(result.requests).toBe(2) + expect(result.results).toHaveLength(1) + expect(result.results[0]!.payload.parts[0]!.outcome).toBe("succeeded") + expect(result.order.indexOf("decision")).toBeLessThan(result.order.indexOf("execute")) + expect(result.order.indexOf("result")).toBeLessThan(result.order.lastIndexOf("decision")) + expect( + result.chunks.findIndex((chunk) => chunk.type === EventType.TOOL_CALL_RESULT), + ).toBeLessThan( + result.chunks.findLastIndex((chunk) => chunk.type === EventType.TEXT_MESSAGE_START), + ) + expect( + result.saved.some((payload) => + payload.parts.some((part) => part.executionLocation === "server_api"), + ), + ).toBe(true) + expect(JSON.stringify(result.saved)).not.toContain("synthetic-test-key") + expect(result.saved.at(-1)?.provenance?.usage).toMatchObject({ costUsd: "0.000105" }) + }, + ) test("refreshes provider context between phases without replacing canonical saved history", async () => { const result = await exerciseManagedStream({ refresh: true }) @@ -297,6 +324,19 @@ describe("managed TanStack persistence boundaries", () => { }) }) + test("retains billed usage when a model fails before producing content", async () => { + const result = await exerciseManagedStream({ failBeforeText: true }) + expect(result.failed).toBe(true) + expect(result.requests).toBe(1) + expect(result.saved.at(-1)).toMatchObject({ + parts: [], + provenance: { + usage: { promptTokens: 100, completionTokens: 0, totalTokens: 100, costUsd: "0.0003" }, + }, + }) + expect(result.order).not.toContain("finish") + }) + test("a failed decision commit prevents the business effect", async () => { const result = await exerciseManagedStream({ failDecision: true }) expect(result.executed).toBe(0) @@ -304,27 +344,22 @@ describe("managed TanStack persistence boundaries", () => { expect(result.chunks.some((chunk) => chunk.type === EventType.TOOL_CALL_END)).toBe(false) }) - test("saves input validation failures before continuing without executing the tool", async () => { - const result = await exerciseManagedStream({ invalidInput: true }) - expect(result.failed).toBe(false) - expect(result.executed).toBe(0) - expect(result.requests).toBe(2) - expect(result.results).toHaveLength(1) - const output = result.results[0]!.payload.parts[0]! - expect(output.outcome).toBe("failed") - expect(JSON.stringify(output.output)).toContain("Input validation failed") - expect(result.order.indexOf("result")).toBeLessThan(result.order.indexOf("next-draft")) - }) - - test.each([{ browser: true, invalidInput: true }, { unknownTool: true }])( - "persists rejected tool calls without leaving an executable browser response: %j", - async (options) => { + test.each([ + [{ invalidInput: true }, "Input validation failed"], + [{ transformInput: 5 }, "Input validation failed"], + [{ browser: true, invalidInput: true }, "Input validation failed"], + [{ unknownTool: true }, "Unknown tool"], + ] as const)( + "persists rejected tool calls before continuing without execution: %j", + async (options, error) => { const result = await exerciseManagedStream(options) expect(result.failed).toBe(false) expect(result.executed).toBe(0) expect(result.requests).toBe(2) expect(result.results).toHaveLength(1) - expect(result.results[0]!.payload.parts[0]!.outcome).toBe("failed") + const output = result.results[0]!.payload.parts[0]! + expect(output.outcome).toBe("failed") + expect(JSON.stringify(output.output)).toContain(error) expect(result.order.indexOf("result")).toBeLessThan(result.order.indexOf("next-draft")) }, ) diff --git a/platform/src/lib/chat/threads/stream.ts b/platform/src/lib/chat/threads/stream.ts index 638782ee9..0c64db9f6 100644 --- a/platform/src/lib/chat/threads/stream.ts +++ b/platform/src/lib/chat/threads/stream.ts @@ -1,6 +1,7 @@ import { EventType, convertSchemaToJsonSchema, + isProviderExecutedToolCall, modelMessagesToUIMessages, type ChatMiddleware, type ModelMessage, @@ -163,12 +164,7 @@ function managedAssistantPayload( .flatMap((model) => modelMessagesToUIMessages([model])) .flatMap((message) => message.parts - .filter( - (part) => - part.type !== "tool-call" || - (part as { metadata?: { providerExecuted?: boolean } }).metadata?.providerExecuted !== - true, - ) + .filter((part) => part.type !== "tool-call" || !isProviderExecutedToolCall(part)) .map((part) => { const json = { ...chatStoredJson(part) } if (part.type === "tool-call") return managedToolPart(options, state, json) @@ -291,7 +287,7 @@ async function saveManagedProjection( ) { if (state.committed) return const payload = managedAssistantPayload(options, state, messages) - if (payload.parts.length === 0 && !complete) return + if (payload.parts.length === 0 && !state.usage && !complete) return await options.execute( options.threads.checkpoint({ claim: state.claim, @@ -431,6 +427,7 @@ export function managedChatMiddleware(options: ManagedChatStreamOptions) { }), { snapshotStreaming: true, snapshotIntervalMs: 1000 }, ) + const guardedToolCalls = new Set() let completion: Promise | undefined const checkpointInterrupted: NonNullable = async (ctx) => { state.failed = true @@ -476,6 +473,7 @@ export function managedChatMiddleware(options: ManagedChatStreamOptions) { state.partIds.clear() state.modelIds.clear() state.settledToolCalls.clear() + guardedToolCalls.clear() state.messageIds.set(info.messageId, state.claim.assistantMessageId) }, onUsage(_ctx, usage) { @@ -495,9 +493,14 @@ export function managedChatMiddleware(options: ManagedChatStreamOptions) { }, async onBeforeToolCall(ctx) { await saveManagedProjection(options, state, ctx.messages, true) + }, + async onGuardToolCall(_ctx, info) { await options.execute(options.threads.assertActive({ claim: state.claim })) + guardedToolCalls.add(info.toolCallId) }, async onAfterToolCall(_ctx, info) { + // Validation failures have no external outcome. A passed guard only establishes eligibility. + if (!info.ok && !guardedToolCalls.has(info.toolCallId)) return await saveManagedToolResult( options, state, diff --git a/platform/src/lib/chat/threads/threads.integration.test.ts b/platform/src/lib/chat/threads/threads.integration.test.ts index cf47edd40..d2c783918 100644 --- a/platform/src/lib/chat/threads/threads.integration.test.ts +++ b/platform/src/lib/chat/threads/threads.integration.test.ts @@ -614,7 +614,6 @@ describe.skipIf(!integration.url)("PostgreSQL chat conversations", () => { }, ], tools: [], - context: [], aguiContext: [], state: undefined, forwardedProps: { clientId: targetA }, diff --git a/platform/src/lib/chat/usage-limits.test.ts b/platform/src/lib/chat/usage-limits.test.ts index 45763d450..4d270612a 100644 --- a/platform/src/lib/chat/usage-limits.test.ts +++ b/platform/src/lib/chat/usage-limits.test.ts @@ -5,6 +5,7 @@ import { toolDefinition, type ChatMiddleware, type StreamChunk, + type TokenUsage, } from "@tanstack/ai" import { fakeText } from "@tanstack/ai/testing" import { Effect } from "effect" @@ -12,13 +13,10 @@ import { expect, it } from "vitest" import { UsageLimitReached } from "@/lib/usage-limits/errors" import type { UsageLimit, UsageLimits } from "@/lib/usage-limits/usage-limits.server" -import { chatUsageLimitGuard } from "./usage-limits.ts" +import { chatFailedUsageMiddleware, chatUsageLimitGuard } from "./usage-limits.ts" it("stops before the next model call and announces the limit once before the run finishes", async () => { - // Narrows the field TextAdapter types without `| undefined`, as in createChatAdapter. - const adapter = Object.assign(fakeText(), { - midConversationChannels: { tools: false, systemPrompts: false }, - }) + const adapter = fakeText() adapter.setResponses([{ toolCalls: [{ name: "lookup", input: {} }] }, { text: "unreachable" }]) const recorded: number[] = [] const usageLimits = { @@ -53,6 +51,7 @@ it("stops before the next model call and announces the limit once before the run yield* held } const source = chat({ + threadId: "usage-test", adapter, messages: [{ role: "user", content: "Look it up" }], tools: [ @@ -75,3 +74,42 @@ it("stops before the next model call and announces the limit once before the run ]) expect(types.slice(-2)).toEqual(["CUSTOM astralbeam.usage_limit.reached", "RUN_FINISHED"]) }) + +it.each([ + { usage: { promptTokens: 100, completionTokens: 2, totalTokens: 102 }, expected: 102 }, + { usage: [{ inputTokens: 100, outputTokens: 2, totalTokens: 102 }], expected: 102 }, + { usage: undefined, expected: undefined }, +])( + "accounts for a successful phase followed by failed usage: $usage", + async ({ usage, expected }) => { + const adapter = fakeText() + adapter.setResponses([{ toolCalls: [{ name: "lookup", input: {} }] }]) + const stream = adapter.chatStream.bind(adapter) + adapter.chatStream = async function* (options) { + if (adapter.state.callCount === 0) { + yield* stream(options) + return + } + yield { + type: EventType.RUN_ERROR, + message: "Synthetic interruption", + ...(usage ? { usage } : {}), + } + } + const recorded: TokenUsage[] = [] + const consumer: ChatMiddleware = { + name: "usage-consumer", + onUsage: (_ctx, usage) => void recorded.push(usage), + } + const source = chat({ + threadId: "failed-usage-test", + adapter, + messages: [{ role: "user", content: "Look up" }], + tools: [toolDefinition({ name: "lookup", description: "Look up" }).server(() => "42")], + middleware: [chatFailedUsageMiddleware([consumer]), consumer], + }) + expect((await Array.fromAsync(source)).at(-1)?.type).toBe(EventType.RUN_ERROR) + expect(recorded).toHaveLength(usage ? 2 : 1) + expect(recorded[1]?.totalTokens).toBe(expected) + }, +) diff --git a/platform/src/lib/chat/usage-limits.ts b/platform/src/lib/chat/usage-limits.ts index afd70b75d..52242d2c7 100644 --- a/platform/src/lib/chat/usage-limits.ts +++ b/platform/src/lib/chat/usage-limits.ts @@ -1,4 +1,4 @@ -import { EventType, type ChatMiddleware, type StreamChunk } from "@tanstack/ai" +import { fromSpecTokenUsage, EventType, type ChatMiddleware, type StreamChunk } from "@tanstack/ai" import { Effect, Exit } from "effect" import { APP_HANDLE } from "@/lib/constants" @@ -71,3 +71,27 @@ export function chatUsageLimitGuard(options: { return { middleware, announce } } + +/** Core reports successful-call usage, but usage-bearing failed streams bypass that hook. + * https://github.com/TanStack/ai/pull/1793 */ +export function chatFailedUsageMiddleware(consumers: readonly ChatMiddleware[]): ChatMiddleware { + let settled = false + return { + name: "failed-call-usage", + onIteration() { + settled = false + }, + onUsage() { + settled = true + }, + async onChunk(ctx, chunk) { + if (chunk.type !== EventType.RUN_ERROR || settled) return + const usage = Array.isArray(chunk.usage) + ? fromSpecTokenUsage(chunk.usage, chunk.metadata?.tanstack?.usage) + : (chunk.usage ?? fromSpecTokenUsage(undefined, chunk.metadata?.tanstack?.usage)) + if (!usage) return + settled = true + for (const consumer of consumers) await consumer.onUsage?.(ctx, usage) + }, + } +} diff --git a/platform/src/lib/chat/web.test.ts b/platform/src/lib/chat/web.test.ts index 709da9f15..3f15772a2 100644 --- a/platform/src/lib/chat/web.test.ts +++ b/platform/src/lib/chat/web.test.ts @@ -569,6 +569,7 @@ test.each(["openai", "xai", "anthropic", "openrouter"] as const)( expect(compatible[0]!.metadata).toHaveProperty("webProviderContext") const requests: unknown[] = [] for await (const _chunk of chat({ + threadId: "web-replay-test", adapter: createChatAdapter( { ...result.model, diff --git a/platform/src/lib/chat/web.ts b/platform/src/lib/chat/web.ts index dfe8365e3..28dd32fae 100644 --- a/platform/src/lib/chat/web.ts +++ b/platform/src/lib/chat/web.ts @@ -10,7 +10,7 @@ import { webSearchTool as routerSearch, webFetchTool as routerFetch, } from "@tanstack/ai-openrouter/tools" -import type { Tool, ModelMessage } from "@tanstack/ai" +import { isProviderExecutedToolCall, type Tool, type ModelMessage } from "@tanstack/ai" import type { WebEvidence, WebSource } from "@astralbeam/sdk/core" import type { ChatModelConfiguration } from "@/lib/model-providers/model-providers.server" import { modelWebAccess } from "@/lib/model-providers/web.server" @@ -413,12 +413,7 @@ export function chatWebContext( ), ), tool_calls: toolCalls - .filter( - (call) => - !Schema.is(Schema.Struct({ providerExecuted: Schema.Literal(true) }))( - call.metadata, - ), - ) + .filter((call) => !isProviderExecutedToolCall(call)) .map((call) => ({ id: call.id, type: call.type, function: call.function })), } : {}), diff --git a/platform/src/lib/mcp/tools.server.test.ts b/platform/src/lib/mcp/tools.server.test.ts index 22966c5bb..381736f0a 100644 --- a/platform/src/lib/mcp/tools.server.test.ts +++ b/platform/src/lib/mcp/tools.server.test.ts @@ -188,6 +188,8 @@ describe("request-oriented MCP tools", () => { await expect( set.tools[1]!.execute({ nested: { region: 2 ** 53 } }, undefined), ).rejects.toThrow() + for (const region of [NaN, Infinity, { toJSON: () => 42 }]) + await expect(set.tools[1]!.execute({ nested: { region } }, undefined)).rejects.toThrow() expect(mcpToolFixture.state.calls).toBe(1) await set.tools[1]!.execute(Object.create({ nested: { region: 42 } }), undefined) expect(mcpToolFixture.state.regionHeader).toBeUndefined() diff --git a/platform/src/lib/mcp/tools.server.ts b/platform/src/lib/mcp/tools.server.ts index 96ad6123f..8791c4034 100644 --- a/platform/src/lib/mcp/tools.server.ts +++ b/platform/src/lib/mcp/tools.server.ts @@ -29,7 +29,7 @@ function mcpToolAlias(connectionId: string, nativeName: string): string { return `mcp_${connectionId.replaceAll("-", "")}_${createHash("sha256").update(nativeName).digest("hex").slice(0, 24)}` } -// Work around ai-mcp bypassing SDK header validation, mirroring and cancellation. +// Raw discovery still bypasses SDK filtering, and SDK header omission is not dispatch rejection. // https://github.com/TanStack/ai/issues/1794 function mcpHeaderPaths( schema: unknown, @@ -95,9 +95,8 @@ function mcpHeaderPaths( return headers } -function mcpInputHeaders(paths: Map, args: unknown): Headers { - const headers = new Headers() - for (const [name, path] of paths) { +function mcpValidateInputHeaders(paths: Map, args: unknown): void { + for (const path of paths.values()) { const value = path.reduce( (current, key) => current && typeof current === "object" && Object.hasOwn(current, key) @@ -105,25 +104,14 @@ function mcpInputHeaders(paths: Map, args: unknown): Headers { : undefined, args, ) - if (value != null) { - if ( - typeof value !== "string" && - typeof value !== "boolean" && - !(typeof value === "number" && Number.isSafeInteger(value)) - ) - throw new McpUnavailable() - const text = String(value) - headers.set( - `Mcp-Param-${name}`, - /^[\t\u0020-\u007e]+$/.test(text) && - text === text.trim() && - !/^=\?base64\?.*\?=$/.test(text) - ? text - : `=?base64?${Buffer.from(text).toString("base64")}?=`, - ) - } + if ( + value != null && + typeof value !== "string" && + typeof value !== "boolean" && + !(typeof value === "number" && Number.isSafeInteger(value)) + ) + throw new McpUnavailable() } - return headers } const makeMcpTools = Effect.gen(function* () { @@ -136,13 +124,10 @@ const makeMcpTools = Effect.gen(function* () { Effect.promise(() => agent.close()), ) const credentialInput = { ...input, expectedVersion: connection.lockVersion } - const callSignals = new AsyncLocalStorage<{ - signal: AbortSignal | undefined - headers?: Headers - }>() + const callSignals = new AsyncLocalStorage() const readCredential = (rejectedToken?: string) => Effect.runPromise(authorization.getCredential({ ...credentialInput, rejectedToken }), { - signal: callSignals.getStore()?.signal, + signal: callSignals.getStore(), }).then(Redacted.value) let lastToken: string | undefined let pages = 0 @@ -153,7 +138,7 @@ const makeMcpTools = Effect.gen(function* () { fetch: async (url, init) => { const headers = new Headers(init?.headers) const method = headers.get("Mcp-Method") ?? "" - const signal = callSignals.getStore()?.signal + const signal = callSignals.getStore() if (headers.get("MCP-Protocol-Version") !== MCP_PROTOCOL_VERSION || !method) throw new McpUnsupportedProtocol() if ( @@ -161,9 +146,6 @@ const makeMcpTools = Effect.gen(function* () { (method === "tools/list" && pages >= MCP_MAX_DISCOVERY_PAGES) ) throw new McpUnavailable() - if (method === "tools/call") - for (const [name, value] of callSignals.getStore()?.headers ?? []) - headers.set(name, value) const credential = await readCredential() if (credential.kind === "static_headers") for (const [name, value] of Object.entries(credential.headers)) headers.set(name, value) @@ -199,7 +181,7 @@ const makeMcpTools = Effect.gen(function* () { const client = yield* Effect.acquireRelease( Effect.tryPromise({ try: (signal) => - callSignals.run({ signal }, () => + callSignals.run(signal, () => createMCPClient({ transport, clientOptions: { capabilities: { elicitation: undefined, sampling: undefined } }, @@ -213,7 +195,7 @@ const makeMcpTools = Effect.gen(function* () { (opened) => Effect.promise(() => opened.close()), ) const tools = yield* Effect.tryPromise({ - try: (signal) => callSignals.run({ signal }, () => client.tools()), + try: (signal) => callSignals.run(signal, () => client.tools()), catch: () => new McpUnavailable(), }).pipe(Effect.onError(() => Effect.promise(() => client.close()))) return yield* Effect.try({ @@ -247,12 +229,11 @@ const makeMcpTools = Effect.gen(function* () { }, execute: async (args: unknown, context?: ToolExecutionContext) => { try { - return await callSignals.run( - { - signal: context?.abortSignal, - headers: mcpInputHeaders(headers, args), - }, - () => remote.execute!(args, context), + mcpValidateInputHeaders(headers, args) + const input: unknown = JSON.parse(JSON.stringify(args ?? {})) + mcpValidateInputHeaders(headers, input) + return await callSignals.run(context?.abortSignal, () => + remote.execute!(input, context), ) } catch { throw new McpUnavailable() diff --git a/platform/src/lib/model-providers/test-model.server.ts b/platform/src/lib/model-providers/test-model.server.ts index 88dca28ba..e8e78aacc 100644 --- a/platform/src/lib/model-providers/test-model.server.ts +++ b/platform/src/lib/model-providers/test-model.server.ts @@ -80,6 +80,7 @@ export const testProviderModel = Effect.fn("testProviderModel")( let hasText = false let completed = false for await (const chunk of chat({ + threadId: crypto.randomUUID(), adapter: createChatAdapter({ ...configuration, fetch: guardedFetch }), messages: [{ role: "user", content: "Reply with OK." }], agentLoopStrategy: maxIterations(1), diff --git a/platform/src/lib/model-providers/usage-cost.test.ts b/platform/src/lib/model-providers/usage-cost.test.ts index 6f9964946..a2ad87707 100644 --- a/platform/src/lib/model-providers/usage-cost.test.ts +++ b/platform/src/lib/model-providers/usage-cost.test.ts @@ -30,6 +30,23 @@ describe("modelUsageCost", () => { expect(modelUsageCost(configuration, usage)).toBe("0.0234") }) + it.each([ + { rates: { cacheWrite1hPerMillion: "6", cacheWritePerMillion: "4" }, expected: "0.0044" }, + { rates: { cacheWritePerMillion: "4" }, expected: "0.0038" }, + { rates: {}, expected: "0.003" }, + ])("prices one-hour writes as part of total cache writes: $rates", ({ rates, expected }) => { + expect( + modelUsageCost( + { ...configuration, prices: { ...configuration.prices, ...rates } }, + { + promptTokens: 1000, + completionTokens: 0, + promptTokensDetails: { cacheWriteTokens: 800, cacheWrite1hTokens: 300 }, + }, + ), + ).toBe(expected) + }) + it("prices a call above a context threshold at that tier's rates", () => { const usage = { promptTokens: 250_000, completionTokens: 2_000 } expect(modelUsageCost(configuration, usage)).toBe("1.545") diff --git a/platform/src/lib/model-providers/usage-cost.ts b/platform/src/lib/model-providers/usage-cost.ts index d3e8ebb2b..53243c8a3 100644 --- a/platform/src/lib/model-providers/usage-cost.ts +++ b/platform/src/lib/model-providers/usage-cost.ts @@ -9,6 +9,7 @@ export const ModelRequestUsageSchema = Schema.Struct({ Schema.Struct({ cachedTokens: Schema.optional(Schema.Number), cacheWriteTokens: Schema.optional(Schema.Number), + cacheWrite1hTokens: Schema.optional(Schema.Number), }), ), }) @@ -40,10 +41,15 @@ export function modelUsageCost(configuration: ModelConfiguration, usage: ModelCa ?.prices ?? configuration.prices const cacheRead = usage.promptTokensDetails?.cachedTokens ?? 0 const cacheWrite = usage.promptTokensDetails?.cacheWriteTokens ?? 0 + const cacheWrite1h = usage.promptTokensDetails?.cacheWrite1hTokens ?? 0 const charges: [number, string][] = [ [Math.max(0, usage.promptTokens - cacheRead - cacheWrite), prices.inputPerMillion], [cacheRead, prices.cacheReadPerMillion ?? prices.inputPerMillion], - [cacheWrite, prices.cacheWritePerMillion ?? prices.inputPerMillion], + [Math.max(0, cacheWrite - cacheWrite1h), prices.cacheWritePerMillion ?? prices.inputPerMillion], + [ + cacheWrite1h, + prices.cacheWrite1hPerMillion ?? prices.cacheWritePerMillion ?? prices.inputPerMillion, + ], [usage.completionTokens, prices.outputPerMillion], ] const total = charges.reduce( diff --git a/platform/src/lib/model-providers/web.server.ts b/platform/src/lib/model-providers/web.server.ts index 69e8be323..b4594dbbc 100644 --- a/platform/src/lib/model-providers/web.server.ts +++ b/platform/src/lib/model-providers/web.server.ts @@ -1,8 +1,5 @@ import { OPENAI_CHAT_MODELS, type OpenAIChatModelToolCapabilitiesByName } from "@tanstack/ai-openai" -import { - ANTHROPIC_MODELS, - type AnthropicChatModelToolCapabilitiesByName, -} from "@tanstack/ai-anthropic" +import { ANTHROPIC_MODELS } from "@tanstack/ai-anthropic" import { GROK_CHAT_MODELS } from "@tanstack/ai-grok" import { OPENROUTER_CHAT_MODELS } from "@tanstack/ai-openrouter/model-meta" import type { ChatModelConfiguration } from "./model-providers.server" @@ -13,31 +10,22 @@ type WithoutWeb = // Runtime exclusions are checked against the adapter's type-only capability metadata. // https://github.com/TanStack/ai/blob/main/packages/ai-openai/src/model-meta.ts -const modelsWithoutWeb: Record< - | WithoutWeb - | WithoutWeb, - true -> = { +const modelsWithoutWeb: Record, true> = { "gpt-5.1-codex": true, "gpt-5-codex": true, "gpt-audio": true, "gpt-audio-mini": true, "o1-pro": true, - "computer-use-preview": true, "gpt-4o-mini-search-preview": true, "gpt-4o-search-preview": true, - "gpt-4o-mini-audio": true, o1: true, - "gpt-4o-audio": true, "gpt-4-turbo": true, "gpt-5.1-codex-mini": true, - "codex-mini-latest": true, "gpt-3.5-turbo": true, "gpt-4": true, "gpt-6-astra-pro": true, "gpt-6-luna-pro": true, "gpt-6-sol-pro": true, - "claude-opus-5-fast": true, } export function modelWebAccess( diff --git a/platform/src/routes/api/v1/chat/-lib/chat.server.ts b/platform/src/routes/api/v1/chat/-lib/chat.server.ts index c32b003a2..54e2d0d85 100644 --- a/platform/src/routes/api/v1/chat/-lib/chat.server.ts +++ b/platform/src/routes/api/v1/chat/-lib/chat.server.ts @@ -23,9 +23,6 @@ const chatRunInput = Schema.Struct({ forwardedProps: Schema.StructWithRest(Schema.Struct({ clientId: ApiUuidSchema }), [ Schema.Record(Schema.String, Schema.Unknown), ]), - data: Schema.optionalKey(Schema.Record(Schema.String, Schema.Unknown)).annotate({ - description: "Legacy mirror of forwardedProps sent by TanStack AI clients.", - }), state: Schema.optionalKey(Schema.Unknown), parentRunId: Schema.optionalKey(Schema.String), }).annotate({ diff --git a/sdk/package.json b/sdk/package.json index f41fc40cc..adc2cb72e 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -85,8 +85,8 @@ "@phosphor-icons/react": "^2.1.10", "@shadcn/react": "^0.3.1", "@tailwindcss/cli": "^4.3.3", - "@tanstack/ai": "^0.68.0", - "@tanstack/ai-client": "^0.39.2", + "@tanstack/ai": "^1.0.0", + "@tanstack/ai-client": "^1.0.0", "@tanstack/eslint-plugin-query": "^5.103.2", "@tanstack/markdown": "^1.0.0", "@tanstack/react-pacer": "^0.23.0", diff --git a/sdk/src/api/generated/api.ts b/sdk/src/api/generated/api.ts index 53dafd1b8..5fb3440a9 100644 --- a/sdk/src/api/generated/api.ts +++ b/sdk/src/api/generated/api.ts @@ -448,11 +448,6 @@ export type ChatRunInputForwardedProps = { [key: string]: unknown } & { clientId: string } -/** - * Legacy mirror of forwardedProps sent by TanStack AI clients. - */ -export type ChatRunInputData = { [key: string]: unknown } - /** * AG-UI RunAgentInput, validated by TanStack AI. Use the saved conversation UUID as threadId, send exactly one new user message and set forwardedProps.clientId. The server appends to the current history path, loads saved context, and uses the conversation's selected agent. Replacement history, resume entries, and systemPrompt are rejected. forwardedProps.debug is development-only. Maximum request size: 32 MiB. */ @@ -463,8 +458,6 @@ export interface ChatRunInput { tools: unknown[] context: unknown[] forwardedProps: ChatRunInputForwardedProps - /** Legacy mirror of forwardedProps sent by TanStack AI clients. */ - data?: ChatRunInputData state?: unknown parentRunId?: string } diff --git a/sdk/src/core/session-persistence.test.ts b/sdk/src/core/session-persistence.test.ts index 4345341a3..83ffd685d 100644 --- a/sdk/src/core/session-persistence.test.ts +++ b/sdk/src/core/session-persistence.test.ts @@ -1,4 +1,11 @@ import { afterEach, beforeEach, expect, test, vi } from "vitest" +import { + digestInterruptJson, + hashSchemaInput, + withInterruptBinding, + type RunFinishedOutcome, +} from "@tanstack/ai/client" +import type { ChatRunInput } from "../api/generated/api.ts" import { chatTestToken, createTestSession, disposeTestSessions } from "./session-fixtures.ts" import { createAstralBeamChat } from "./session.ts" @@ -45,6 +52,41 @@ function savedAssistantMessage(part: Record) { } } +function clientToolOutcome(options: { + runId: string + toolCallId: string + toolName: string + input: unknown +}): RunFinishedOutcome { + const { runId, toolCallId, toolName, input } = options + const id = `client_tool_${toolCallId}` + return { + type: "interrupt", + interrupts: [ + withInterruptBinding( + { + id, + reason: "tanstack:client_tool_execution", + toolCallId, + responseSchema: {}, + metadata: { kind: "client_tool", toolName, input }, + }, + { + v: 1, + kind: "client-tool-execution", + interruptId: id, + toolName, + toolCallId, + interruptedRunId: runId, + generation: 0, + outputSchemaHash: hashSchemaInput(undefined), + responseSchemaHash: digestInterruptJson("{}"), + }, + ), + ], + } +} + beforeEach(() => vi.stubGlobal("sessionStorage", undefined)) afterEach(() => { disposeTestSessions() @@ -667,12 +709,13 @@ test.each< ]), ) if (typeof init?.body !== "string") throw new Error("Expected a chat body") - targetClientId = (JSON.parse(init.body) as { forwardedProps: { clientId: string } }) - .forwardedProps.clientId + const body = JSON.parse(init.body) as ChatRunInput + targetClientId = body.forwardedProps.clientId + const runId = body.runId phase = "decision" return Promise.resolve( events([ - { type: "RUN_STARTED", threadId: thread.id, runId: "initial" }, + { type: "RUN_STARTED", threadId: thread.id, runId }, { type: "CUSTOM", name: "astralbeam_thread", @@ -730,19 +773,14 @@ test.each< { type: "RUN_FINISHED", threadId: thread.id, - runId: "initial", + runId, metadata: { tanstack: { finishReason: "tool_calls" } }, - outcome: { - type: "interrupt", - interrupts: [ - { - id: "client_tool_provider-call", - reason: "tanstack:client_tool_execution", - toolCallId: "provider-call", - metadata: { kind: "client_tool", toolName, input: {} }, - }, - ], - }, + outcome: clientToolOutcome({ + runId, + toolCallId: "provider-call", + toolName, + input: {}, + }), }, ]), ) @@ -1063,35 +1101,12 @@ test("a browser tool and widget can continue through consecutive committed turns metadata: { tanstack: { finishReason: index < 3 ? "tool_calls" : "stop" } }, ...(index < 3 ? { - outcome: { - type: "interrupt", - interrupts: [ - { - id: `client_tool_${part.toolCallId}`, - reason: "tanstack:client_tool_execution", - toolCallId: part.toolCallId, - responseSchema: {}, - metadata: { - kind: "client_tool", - toolName: part.name, - input: part.input, - "tanstack:interruptBinding": { - v: 1, - kind: "client-tool-execution", - interruptId: `client_tool_${part.toolCallId}`, - toolName: part.name, - toolCallId: part.toolCallId, - interruptedRunId: runId, - generation: 0, - outputSchemaHash: - "sha256:eb045d78d273107348b0300c01d29b7552d622abbc6faf81b3ec55359aa9950c", - responseSchemaHash: - "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", - }, - }, - }, - ], - }, + outcome: clientToolOutcome({ + runId, + toolCallId: part.toolCallId, + toolName: part.name, + input: part.input, + }), } : {}), }, @@ -1135,7 +1150,7 @@ test("a browser tool and widget can continue through consecutive committed turns if (path.endsWith("/chat")) { phase = 1 if (typeof init?.body !== "string") throw new Error("Expected a chat request body") - return Promise.resolve(stream(phase, (JSON.parse(init.body) as { runId: string }).runId)) + return Promise.resolve(stream(phase, (JSON.parse(init.body) as ChatRunInput).runId)) } if (path.endsWith("/tool-results")) { if (typeof init?.body !== "string") throw new Error("Expected a tool-result request body")