diff --git a/bun.lock b/bun.lock index 04fa74a1c289..f5fe5b2523e1 100644 --- a/bun.lock +++ b/bun.lock @@ -724,6 +724,7 @@ "@opencode-ai/client": "workspace:*", "@opencode-ai/schema": "workspace:*", "@opencode-ai/sdk": "workspace:*", + "@standard-schema/spec": "^1.1.0", "effect": "catalog:", "zod": "catalog:", }, @@ -3008,7 +3009,7 @@ "@standard-community/standard-openapi": ["@standard-community/standard-openapi@0.2.9", "", { "peerDependencies": { "@standard-community/standard-json": "^0.3.5", "@standard-schema/spec": "^1.0.0", "arktype": "^2.1.20", "effect": "^3.17.14", "openapi-types": "^12.1.3", "sury": "^10.0.0", "typebox": "^1.0.0", "valibot": "^1.1.0", "zod": "^3.25.0 || ^4.0.0", "zod-openapi": "^4" }, "optionalPeers": ["arktype", "effect", "sury", "typebox", "valibot", "zod", "zod-openapi"] }, "sha512-htj+yldvN1XncyZi4rehbf9kLbu8os2Ke/rfqoZHCMHuw34kiF3LP/yQPdA0tQ940y8nDq3Iou8R3wG+AGGyvg=="], - "@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="], + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@stoplight/better-ajv-errors": ["@stoplight/better-ajv-errors@1.0.3", "", { "dependencies": { "jsonpointer": "^5.0.0", "leven": "^3.1.0" }, "peerDependencies": { "ajv": ">=8" } }, "sha512-0p9uXkuB22qGdNfy3VeEhxkU5uwvp/KrBTAbrLBURv6ilxIVwanKwjMc41lQfIVgPGcOkmLbTolfFrSsueu7zA=="], @@ -6392,8 +6393,6 @@ "@ai-sdk/perplexity/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], - "@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "@ai-sdk/togetherai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], "@ai-sdk/vercel/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], @@ -6610,6 +6609,8 @@ "@hey-api/openapi-ts/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "@hono/standard-validator/@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="], + "@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], "@inquirer/core/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=="], @@ -7332,8 +7333,6 @@ "editorconfig/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], - "effect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "electron-builder/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "electron-builder/yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], @@ -7394,8 +7393,6 @@ "fumadocs-core/shiki": ["shiki@4.3.1", "", { "dependencies": { "@shikijs/core": "4.3.1", "@shikijs/engine-javascript": "4.3.1", "@shikijs/engine-oniguruma": "4.3.1", "@shikijs/langs": "4.3.1", "@shikijs/themes": "4.3.1", "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-oR+qDVi2OjX1tmDpyv+3KviX01KzO6Af+0NNnKnsp9491UEGz2YpxTuJboS/6VhYpTdqzmuJBuiTlrAWWJAssw=="], - "fumadocs-mdx/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "fumadocs-mdx/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], "fumadocs-mdx/esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="], @@ -7534,6 +7531,8 @@ "opencode/@solid-primitives/scheduled": ["@solid-primitives/scheduled@1.5.2", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-/j2igE0xyNaHhj6kMfcUQn5rAVSTLbAX+CDEBm25hSNBmNiHLu2lM7Usj2kJJ5j36D67bE8wR1hBNA8hjtvsQA=="], + "opencode/@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="], + "opencode/minimatch": ["minimatch@10.0.3", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw=="], "opencode/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], @@ -7740,46 +7739,6 @@ "@actions/github/@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@12.6.0", "", { "dependencies": { "@octokit/openapi-types": "^20.0.0" } }, "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw=="], - "@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "@ai-sdk/anthropic/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "@ai-sdk/azure/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "@ai-sdk/cerebras/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "@ai-sdk/cohere/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "@ai-sdk/deepgram/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "@ai-sdk/deepinfra/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "@ai-sdk/deepseek/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "@ai-sdk/elevenlabs/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "@ai-sdk/fireworks/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "@ai-sdk/google-vertex/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "@ai-sdk/google/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "@ai-sdk/groq/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "@ai-sdk/mistral/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "@ai-sdk/openai-compatible/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "@ai-sdk/openai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "@ai-sdk/perplexity/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "@ai-sdk/togetherai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "@ai-sdk/vercel/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "@ai-sdk/xai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "@astrojs/check/yargs/cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], "@astrojs/check/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], @@ -8382,10 +8341,6 @@ "@solidjs/start/shiki/@shikijs/types": ["@shikijs/types@1.29.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4" } }, "sha512-VJjK0eIijTZf0QSTODEXCqinjBn0joAHQ+aPSBzrv4O2d/QSbsMw+ZeSRx03kV34Hy7NzUvV/7NqfYGRLrASmw=="], - "@standard-community/standard-json/effect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "@standard-community/standard-openapi/effect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "@stoplight/spectral-core/minimatch/brace-expansion": ["brace-expansion@1.1.15", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg=="], "@storybook/addon-docs/react-dom/scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="], @@ -8850,10 +8805,6 @@ "venice-ai-sdk-provider/@ai-sdk/provider-utils/@ai-sdk/provider": ["@ai-sdk/provider@3.0.12", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-sj9DWTJ2Ze0WR9qsiOPqoqzNx3OxL6iMxHImbhvoe9qOspekbzxNDMiJ4TIGfYHYh9w4OmBjz3prvqhzTi96+Q=="], - "venice-ai-sdk-provider/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "vitest/@vitest/expect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "vitest/@vitest/expect/chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], "wrangler/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q=="], @@ -9118,8 +9069,6 @@ "@octokit/rest/@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], - "@opencode-ai/core/@ai-sdk/openai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "@opencode-ai/desktop/@actions/artifact/@actions/http-client/undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="], "@opencode-ai/www/@cloudflare/vite-plugin/@cloudflare/unenv-preset/workerd": ["workerd@1.20260708.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260708.1", "@cloudflare/workerd-darwin-arm64": "1.20260708.1", "@cloudflare/workerd-linux-64": "1.20260708.1", "@cloudflare/workerd-linux-arm64": "1.20260708.1", "@cloudflare/workerd-windows-64": "1.20260708.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-WAK+Kt/VVCSldH2qSr8lx46XCJ4Q+bdlHNaFqUtOHthBEIB8C1N8HVW+VOLrxDoTCk0NGNv0zajnBeQK4JOB9w=="], @@ -9302,14 +9251,6 @@ "@vitejs/plugin-react/vite/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], - "ai-gateway-provider/@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "ai-gateway-provider/@ai-sdk/anthropic/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "ai-gateway-provider/@ai-sdk/azure/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "ai-gateway-provider/@ai-sdk/deepseek/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "app-builder-lib/@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], diff --git a/packages/core/src/tool/AGENTS.md b/packages/core/src/tool/AGENTS.md index 08c9fabefe2d..8e74bd5bb3af 100644 --- a/packages/core/src/tool/AGENTS.md +++ b/packages/core/src/tool/AGENTS.md @@ -4,7 +4,7 @@ This folder owns Core's one local tool representation, process and Location regi ## Representations -- `tool.ts` defines the opaque canonical `Tool.make({ description, input, output, execute, toModelOutput })` value. Shipped built-ins and plugin tools use the same type. +- `tool.ts` defines the structural canonical `Tool.make({ description, input, output, execute, toModelOutput })` declaration. Shipped built-ins and plugin tools use the same type. - `tools.ts` exposes the registration-only `Tools.Service` view used by Location producers. - `registry.ts` stores only canonical Location registrations, derives definitions, invokes tools, and applies generic output bounding. @@ -12,7 +12,7 @@ Do not add a second executable entry type, registry-owned executor, authorizatio ## Construction -Tool schemas and projection use `input` and `output` terminology. A tool value is opaque: its codecs, executor, definition derivation, and catalog permission declaration are private runtime details. +Tool schemas and projection use `input` and `output` terminology. A tool value carries its schemas, executor, projection, and optional catalog permission directly so separately loaded plugin package instances can exchange it structurally. Location-scoped built-in layers acquire `PermissionV2.Service` and every other required Location service while the layer is constructed. The executor captures those services. Permission sources are always constructed from the canonical invocation context: diff --git a/packages/core/test/session-runner-tool-registry.test.ts b/packages/core/test/session-runner-tool-registry.test.ts index c488c1f7fc92..ae80449523c1 100644 --- a/packages/core/test/session-runner-tool-registry.test.ts +++ b/packages/core/test/session-runner-tool-registry.test.ts @@ -107,17 +107,6 @@ describe("ToolRegistry", () => { }), ) - it.effect("reuses model definitions across requests", () => - Effect.gen(function* () { - const service = yield* ToolRegistry.Service - yield* service.register({ echo: make() }, { codemode: false }) - const first = yield* toolDefinitions(service) - const second = yield* toolDefinitions(service) - - expect(second[0]).toBe(first[0]) - }), - ) - it.effect("removes a scoped registration", () => Effect.gen(function* () { const service = yield* ToolRegistry.Service diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 084f6c03ffef..825a5c0f0856 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -29,6 +29,7 @@ "@opencode-ai/client": "workspace:*", "@opencode-ai/schema": "workspace:*", "@opencode-ai/sdk": "workspace:*", + "@standard-schema/spec": "^1.1.0", "effect": "catalog:", "zod": "catalog:" }, diff --git a/packages/plugin/src/v2/effect/tool.ts b/packages/plugin/src/v2/effect/tool.ts index 8668570f8fba..59ef488433b3 100644 --- a/packages/plugin/src/v2/effect/tool.ts +++ b/packages/plugin/src/v2/effect/tool.ts @@ -4,7 +4,8 @@ import { Agent } from "@opencode-ai/schema/agent" import type { LLM } from "@opencode-ai/schema/llm" import { Session } from "@opencode-ai/schema/session" import { SessionMessage } from "@opencode-ai/schema/session-message" -import { Effect, JsonSchema, Schema, type Scope } from "effect" +import type { StandardJSONSchemaV1, StandardSchemaV1 } from "@standard-schema/spec" +import { Effect, JsonSchema, Schema } from "effect" import type { Hooks, Transform } from "./registration.js" export interface Context { @@ -20,7 +21,34 @@ export interface Progress { readonly content?: ReadonlyArray } -export type SchemaType = Schema.Codec +export type StandardSchemaType = StandardSchemaV1 & + StandardJSONSchemaV1 +export type SchemaType = Schema.Codec | StandardSchemaType +type IsAny = 0 extends 1 & A ? true : false +export type InputValue = + IsAny extends true + ? any + : S extends Schema.Codec + ? A + : S extends StandardSchemaV1 + ? A + : never +export type OutputValue = + IsAny extends true + ? any + : S extends Schema.Codec + ? A + : S extends StandardSchemaV1 + ? A + : never +export type EncodedValue = + IsAny extends true + ? any + : S extends Schema.Codec + ? A + : S extends StandardSchemaV1 + ? A + : never type ToolDefinition = { readonly name: string @@ -45,16 +73,6 @@ type ToolOutput = { readonly content: ReadonlyArray } -declare const TypeId: unique symbol - -export interface Definition, Output extends SchemaType> { - readonly [TypeId]: { - readonly _Input: Input - readonly _Output: Output - } -} - -export type AnyTool = Definition export class Failure extends Schema.TaggedErrorClass()("LLM.ToolFailure", { message: Schema.String, error: Schema.optional(Schema.Defect()), @@ -70,26 +88,24 @@ export type Content = | { readonly type: "text"; readonly text: string } | { readonly type: "file"; readonly data: string; readonly mime: string; readonly name?: string } -type Config< +export type Definition< Input extends SchemaType, - Output extends SchemaType, - Structured extends SchemaType = Output, + Structured extends SchemaType, + Output extends SchemaType = any, > = { readonly description: string readonly input: Input readonly output: Output readonly structured?: Structured + readonly permission?: string readonly toStructuredOutput?: (input: { - readonly input: Schema.Schema.Type - readonly output: Output["Encoded"] - }) => Schema.Schema.Type - readonly execute: ( - input: Schema.Schema.Type, - context: Context, - ) => Effect.Effect, Failure> + readonly input: InputValue + readonly output: EncodedValue + }) => OutputValue + readonly execute: (input: InputValue, context: Context) => Effect.Effect, Failure> readonly toModelOutput?: (input: { - readonly input: Schema.Schema.Type - readonly output: Output["Encoded"] + readonly input: InputValue + readonly output: EncodedValue }) => ReadonlyArray } @@ -103,109 +119,25 @@ export type DynamicOutput = { * time (MCP servers, plugin manifests). Input is passed through as `unknown`; * `execute` returns the already-projected structured value and model content. */ -type DynamicConfig = { +export type DynamicDefinition = { readonly description: string readonly jsonSchema: JsonSchema.JsonSchema readonly outputSchema?: JsonSchema.JsonSchema - readonly execute: (input: unknown, context: Context) => Effect.Effect -} - -type Runtime = { readonly permission?: string - readonly definition: (name: string) => ToolDefinition - readonly settle: (call: ToolCall, context: Context) => Effect.Effect + readonly execute: (input: unknown, context: Context) => Effect.Effect } -const runtimes = new WeakMap() +export type AnyTool = Definition | DynamicDefinition export function make< Input extends SchemaType, Output extends SchemaType, Structured extends SchemaType = Output, ->(config: Config): Definition -export function make(config: DynamicConfig): AnyTool -export function make(config: Config | DynamicConfig): AnyTool { - if ("jsonSchema" in config) return makeDynamic(config) - return makeTyped(config) -} - -function makeTyped< - Input extends SchemaType, - Output extends SchemaType, - Structured extends SchemaType = Output, ->(config: Config): Definition { - const tool = Object.freeze({}) as Definition - const definitions = new Map() - runtimes.set(tool, { - definition: (name) => { - const cached = definitions.get(name) - if (cached) return cached - const definition: ToolDefinition = { - name, - description: config.description, - inputSchema: toJsonSchema(config.input), - outputSchema: toJsonSchema(config.structured ?? config.output), - } - definitions.set(name, definition) - return definition - }, - settle: (call, context) => - Schema.decodeUnknownEffect(config.input)(call.input).pipe( - Effect.mapError((error) => new Failure({ message: `Invalid tool input: ${error.message}` })), - Effect.flatMap((input) => - config.execute(input, context).pipe( - Effect.flatMap((output) => - Schema.encodeEffect(config.output)(output).pipe( - Effect.flatMap((output) => { - if (!config.structured || !config.toStructuredOutput) - return Effect.succeed({ output, structured: output }) - return Schema.encodeEffect(config.structured)(config.toStructuredOutput({ input, output })).pipe( - Effect.map((structured) => ({ output, structured })), - ) - }), - Effect.mapError( - (error) => - new Failure({ - message: `Tool returned an invalid value for its output schema: ${error.message}`, - }), - ), - ), - ), - Effect.map(({ output, structured }) => ({ - structured, - content: - config.toModelOutput?.({ input, output }).map(toModelContent) ?? - (typeof output === "string" ? [{ type: "text" as const, text: output }] : []), - })), - ), - ), - ), - }) - return tool -} - -function makeDynamic(config: DynamicConfig): AnyTool { - const tool = Object.freeze({}) as AnyTool - const definitions = new Map() - runtimes.set(tool, { - definition: (name) => { - const cached = definitions.get(name) - if (cached) return cached - const definition: ToolDefinition = { - name, - description: config.description, - inputSchema: config.jsonSchema, - outputSchema: config.outputSchema, - } - definitions.set(name, definition) - return definition - }, - settle: (call, context) => - config - .execute(call.input, context) - .pipe(Effect.map((output) => ({ structured: output.structured, content: output.content.map(toModelContent) }))), - }) - return tool +>(config: Definition): Definition +export function make(config: DynamicDefinition): DynamicDefinition +export function make(config: AnyTool): AnyTool +export function make(config: AnyTool): AnyTool { + return config } function toModelContent(part: Content) { @@ -230,23 +162,102 @@ export const registrationEntries = (tools: Readonly>, gr } }) -export const withPermission = , Output extends SchemaType>( - tool: Definition, +export const withPermission = ( + tool: T, permission: string, -) => { - const decorated = Object.freeze({}) as Definition - runtimes.set(decorated, { ...runtimeOf(tool), permission }) - return decorated +): Omit & { + readonly permission: string +} => ({ ...tool, permission }) + +export const permission = (tool: AnyTool, name: string) => tool.permission ?? name + +export const definition = (name: string, tool: AnyTool): ToolDefinition => + "jsonSchema" in tool + ? { + name, + description: tool.description, + inputSchema: tool.jsonSchema, + outputSchema: tool.outputSchema, + } + : { + name, + description: tool.description, + inputSchema: inputJsonSchema(tool.input), + outputSchema: outputJsonSchema(tool.structured ?? tool.output), + } + +export const settle = (tool: AnyTool, call: ToolCall, context: Context): Effect.Effect => + Effect.gen(function* () { + if ("jsonSchema" in tool) { + const output = yield* tool.execute(call.input, context) + return { structured: output.structured, content: output.content.map(toModelContent) } + } + + const input = yield* decodeInput(tool.input, call.input) + const value = yield* tool.execute(input, context) + const output = yield* encodeOutput(tool.output, value) + const structured = + tool.structured && tool.toStructuredOutput + ? yield* encodeOutput(tool.structured, tool.toStructuredOutput({ input, output })) + : output + return { + structured, + content: + tool.toModelOutput?.({ input, output }).map(toModelContent) ?? + (typeof output === "string" ? [{ type: "text" as const, text: output }] : []), + } + }) + +function decodeInput(schema: SchemaType, value: unknown): Effect.Effect { + if (Schema.isSchema(schema)) + return Schema.decodeUnknownEffect(schema)(value).pipe( + Effect.mapError((error) => new Failure({ message: `Invalid tool input: ${error.message}` })), + ) + return validateStandard(schema, value, "Invalid tool input") +} + +function encodeOutput(schema: SchemaType, value: unknown): Effect.Effect { + if (Schema.isSchema(schema)) + return Schema.encodeEffect(schema)(value).pipe( + Effect.mapError( + (error) => new Failure({ message: `Tool returned an invalid value for its output schema: ${error.message}` }), + ), + ) + return validateStandard(schema, value, "Tool returned an invalid value for its output schema") } -export const permission = (tool: AnyTool, name: string) => runtimeOf(tool).permission ?? name -export const definition = (name: string, tool: AnyTool) => runtimeOf(tool).definition(name) -export const settle = (tool: AnyTool, call: ToolCall, context: Context) => runtimeOf(tool).settle(call, context) +function validateStandard(schema: StandardSchemaType, value: unknown, prefix: string): Effect.Effect { + return Effect.gen(function* () { + const pending = yield* Effect.try({ + try: () => schema["~standard"].validate(value), + catch: (error) => standardFailure(prefix, error), + }) + const result = + pending instanceof Promise + ? yield* Effect.tryPromise({ try: () => pending, catch: (error) => standardFailure(prefix, error) }) + : pending + if (result.issues) + return yield* Effect.fail( + new Failure({ message: `${prefix}: ${result.issues.map((issue) => issue.message).join(", ")}` }), + ) + return result.value + }) +} + +function standardFailure(prefix: string, error: unknown) { + return new Failure({ message: `${prefix}: ${error instanceof Error ? error.message : String(error)}` }) +} + +function inputJsonSchema(schema: SchemaType): JsonSchema.JsonSchema { + if (!Schema.isSchema(schema)) + return schema["~standard"].jsonSchema.input({ target: "draft-2020-12" }) as JsonSchema.JsonSchema + return toJsonSchema(schema) +} -function runtimeOf(tool: AnyTool) { - const runtime = runtimes.get(tool) - if (!runtime) throw new TypeError("Invalid Tool value") - return runtime +function outputJsonSchema(schema: SchemaType): JsonSchema.JsonSchema { + if (!Schema.isSchema(schema)) + return schema["~standard"].jsonSchema.output({ target: "draft-2020-12" }) as JsonSchema.JsonSchema + return toJsonSchema(schema) } function toJsonSchema(schema: Schema.Top): JsonSchema.JsonSchema { diff --git a/packages/plugin/src/v2/promise/tool.ts b/packages/plugin/src/v2/promise/tool.ts index 3f85aaae54ba..91b9b2c1e286 100644 --- a/packages/plugin/src/v2/promise/tool.ts +++ b/packages/plugin/src/v2/promise/tool.ts @@ -1,8 +1,4 @@ import type { Tool } from "../effect/tool.js" -import type { Agent } from "@opencode-ai/schema/agent" -import type { Session } from "@opencode-ai/schema/session" -import type { SessionMessage } from "@opencode-ai/schema/session-message" -import type { JsonSchema, Schema } from "effect" import type { Hooks, Transform } from "./registration.js" export type Context = Omit & { @@ -16,71 +12,28 @@ export type Definition< Input extends SchemaType, Output extends SchemaType, Structured extends SchemaType = Output, -> = { +> = Omit, "execute" | "permission"> & { readonly name: string readonly options?: RegisterOptions - readonly description: string - readonly input: Input - readonly output: Output - readonly structured?: Structured - readonly toStructuredOutput?: (input: { - readonly input: Schema.Schema.Type - readonly output: Output["Encoded"] - }) => Schema.Schema.Type - readonly execute: ( - input: Schema.Schema.Type, - context: Context, - ) => Promise> - readonly toModelOutput?: (input: { - readonly input: Schema.Schema.Type - readonly output: Output["Encoded"] - }) => ReadonlyArray + readonly execute: (input: Tool.InputValue, context: Context) => Promise> } -export type DynamicDefinition = { +export type DynamicDefinition = Omit & { readonly name: string readonly options?: RegisterOptions - readonly description: string - readonly jsonSchema: JsonSchema.JsonSchema - readonly outputSchema?: JsonSchema.JsonSchema readonly execute: (input: unknown, context: Context) => Promise } export type AnyTool = Definition | DynamicDefinition -export interface ToolExecuteBeforeEvent { - readonly tool: string - readonly sessionID: Session.ID - readonly agent: Agent.ID - readonly messageID: SessionMessage.ID - readonly callID: string - input: unknown -} - -export interface ToolExecuteAfterEvent { - readonly tool: string - readonly sessionID: Session.ID - readonly agent: Agent.ID - readonly messageID: SessionMessage.ID - readonly callID: string - readonly input: unknown - result: Tool.ToolExecuteAfterEvent["result"] - output?: Tool.ToolExecuteAfterEvent["output"] - outputPaths?: ReadonlyArray -} - -export interface RegisterOptions { - readonly group?: string - /** Defaults to true. False exposes the tool directly to the provider. */ - readonly codemode?: boolean -} +export type ToolExecuteBeforeEvent = Tool.ToolExecuteBeforeEvent +export type ToolExecuteAfterEvent = Tool.ToolExecuteAfterEvent +export type RegisterOptions = Tool.RegisterOptions export interface ToolDraft { - add< - Input extends SchemaType, - Output extends SchemaType, - Structured extends SchemaType = Output, - >(tool: Definition): void + add, Output extends SchemaType, Structured extends SchemaType = Output>( + tool: Definition, + ): void add(tool: DynamicDefinition): void } diff --git a/packages/plugin/test/tool.test.ts b/packages/plugin/test/tool.test.ts new file mode 100644 index 000000000000..c22ddbec9a71 --- /dev/null +++ b/packages/plugin/test/tool.test.ts @@ -0,0 +1,132 @@ +import { expect, test } from "bun:test" +import { Agent } from "@opencode-ai/schema/agent" +import { Session } from "@opencode-ai/schema/session" +import { SessionMessage } from "@opencode-ai/schema/session-message" +import { Effect, Schema } from "effect" +import * as Tool from "../src/v2/effect/tool" + +const context = { + sessionID: Session.ID.make("ses_test"), + agent: Agent.ID.make("build"), + messageID: SessionMessage.ID.make("msg_test"), + callID: "call_test", + progress: () => Effect.void, +} satisfies Tool.Context + +test("tools remain valid across separate module instances", async () => { + const ForeignTool = await import(`${new URL("../src/v2/effect/tool.ts", import.meta.url).href}?foreign`) + const config = { + description: "Foreign tool", + input: Schema.Struct({ value: Schema.String }), + output: Schema.Struct({ ok: Schema.Boolean }), + execute: () => Effect.succeed({ ok: true }), + } + const tool = ForeignTool.make(config) + + expect(Tool.definition("foreign", tool)).toEqual({ + name: "foreign", + description: "Foreign tool", + inputSchema: { + type: "object", + properties: { value: { type: "string" } }, + required: ["value"], + additionalProperties: false, + }, + outputSchema: { + type: "object", + properties: { ok: { type: "boolean" } }, + required: ["ok"], + additionalProperties: false, + }, + }) + expect(await Effect.runPromise(Tool.settle(tool, { input: { value: "input" } }, context))).toEqual({ + structured: { ok: true }, + content: [], + }) +}) + +test("portable schemas validate and describe typed tools", async () => { + const input: Tool.StandardSchemaType<{ count: string }, { count: number }> = { + "~standard": { + version: 1, + vendor: "test", + validate: (value) => { + if (typeof value !== "object" || value === null || !("count" in value) || typeof value.count !== "string") + return { issues: [{ message: "count must be numeric" }] } + const count = Number(value.count) + return Number.isFinite(count) ? { value: { count } } : { issues: [{ message: "count must be numeric" }] } + }, + jsonSchema: { + input: () => ({ type: "object", properties: { count: { type: "string" } } }), + output: () => ({ type: "object", properties: { count: { type: "number" } } }), + }, + }, + } + const output: Tool.StandardSchemaType = { + "~standard": { + version: 1, + vendor: "test", + validate: (value) => ({ value: String(value) }), + jsonSchema: { + input: () => ({ type: "number" }), + output: () => ({ type: "string" }), + }, + }, + } + const tool = Tool.make({ + description: "Portable tool", + input, + output, + execute: ({ count }) => Effect.succeed(count + 1), + }) + + expect(Tool.definition("portable", tool)).toEqual({ + name: "portable", + description: "Portable tool", + inputSchema: { type: "object", properties: { count: { type: "string" } } }, + outputSchema: { type: "string" }, + }) + expect(await Effect.runPromise(Tool.settle(tool, { input: { count: "41" } }, context))).toEqual({ + structured: "42", + content: [{ type: "text", text: "42" }], + }) +}) + +test("portable schema failures become tool failures", async () => { + const input: Tool.StandardSchemaType = { + "~standard": { + version: 1, + vendor: "test", + validate: () => ({ issues: [{ message: "expected a string" }] }), + jsonSchema: { + input: () => ({ type: "string" }), + output: () => ({ type: "string" }), + }, + }, + } + const tool = Tool.make({ + description: "Failing tool", + input, + output: input, + execute: Effect.succeed, + }) + + const error = await Effect.runPromiseExit(Tool.settle(tool, { input: 1 }, context)) + expect(error.toString()).toContain("Invalid tool input: expected a string") +}) + +test("two-parameter Definition annotations retain their original meaning", () => { + const input = Schema.Struct({ value: Schema.String }) + const output = Schema.Struct({ value: Schema.String, internal: Schema.Boolean }) + const structured = Schema.Struct({ value: Schema.String }) + const tool: Tool.Definition = Tool.make({ + description: "Annotated tool", + input, + output, + structured, + toStructuredOutput: ({ output }) => ({ value: output.value }), + execute: ({ value }) => Effect.succeed({ value, internal: true }), + }) + + expect(tool.structured).toBe(structured) +}) diff --git a/specs/v2/tools.md b/specs/v2/tools.md index 21c11067e5fe..2b30c0199187 100644 --- a/specs/v2/tools.md +++ b/specs/v2/tools.md @@ -2,9 +2,9 @@ Status: **Current semantic overview.** The Plugin package owns the public tool type; Core owns registration, settlement, and generic output bounding. -## Tool Definitions Are Opaque +## Tool Declarations -V2 has one opaque type for locally executable tools. Typed tools declare codecs, execution, and optional model-facing projection together: +V2 has one structural declaration for locally executable tools. Typed tools declare schemas, execution, and optional model-facing projection together: ```ts const read = Tool.make({ @@ -16,11 +16,13 @@ const read = Tool.make({ }) ``` -`structured` and `toStructuredOutput` may expose a smaller validated result than the complete execution output. Dynamic MCP and manifest tools use the same opaque representation with runtime JSON Schema. +`structured` and `toStructuredOutput` may expose a smaller validated result than the complete execution output. Dynamic MCP and manifest tools use the same declaration with runtime JSON Schema. Built-ins and statically authored plugin tools use this same constructor and execution contract. -`Tool.Definition` is opaque and has exactly one executor. Its schemas and executor are not public fields. The Tool module privately derives model definitions and interprets invocations for the registry; callers normally rely on `Tool.make` inference rather than naming the carrier type. +`Tool.Definition` is a transparent structural value with exactly one executor. Effect schemas and schemas implementing both Standard Schema V1 and Standard JSON Schema V1 are accepted. The Tool module derives model definitions and interprets invocations for the registry; callers normally rely on `Tool.make` inference rather than naming the declaration type. + +Standard input schemas validate model input into the handler value. Standard output schemas validate the handler result into the model-facing value. Effect codecs retain their native decode-input and encode-output directions. Input and output codecs are self-contained. Schema conversion cannot require services. Tool dependencies are acquired during construction and captured by `execute`.