@raybase/core 0.0.2 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/.admin-build/AdminApp.css +2 -0
- package/dist/.admin-build/AdminApp.js +77 -0
- package/dist/.admin-docs-build/AdminDocsApp.css +2 -0
- package/dist/.admin-docs-build/AdminDocsApp.js +45 -0
- package/dist/actions.d.ts +347 -0
- package/dist/actions.d.ts.map +1 -0
- package/dist/actions.js +661 -0
- package/dist/actions.js.map +1 -0
- package/dist/admin-assets/admin.7738f50720a8ed7b.css +2 -0
- package/dist/admin-assets/admin.8678cb61db34dc81.js +77 -0
- package/dist/admin-boot.d.ts +94 -0
- package/dist/admin-boot.d.ts.map +1 -0
- package/dist/admin-boot.js +139 -0
- package/dist/admin-boot.js.map +1 -0
- package/dist/admin-docs-content.d.ts +95 -0
- package/dist/admin-docs-content.d.ts.map +1 -0
- package/dist/admin-docs-content.js +463 -0
- package/dist/admin-docs-content.js.map +1 -0
- package/dist/admin-docs-ui.d.ts +11 -0
- package/dist/admin-docs-ui.d.ts.map +1 -0
- package/dist/admin-docs-ui.js +23 -0
- package/dist/admin-docs-ui.js.map +1 -0
- package/dist/admin-kit.d.ts +29 -0
- package/dist/admin-kit.js +1557 -0
- package/dist/admin-role.d.ts +8 -0
- package/dist/admin-role.d.ts.map +1 -0
- package/dist/admin-role.js +22 -0
- package/dist/admin-role.js.map +1 -0
- package/dist/admin-ui.d.ts +3 -0
- package/dist/admin-ui.d.ts.map +1 -0
- package/dist/admin-ui.js +16 -0
- package/dist/admin-ui.js.map +1 -0
- package/dist/agent-ai-chat.d.ts +127 -0
- package/dist/agent-ai-chat.d.ts.map +1 -0
- package/dist/agent-ai-chat.js +5975 -0
- package/dist/agent-ai-chat.js.map +1 -0
- package/dist/agent-chapter-archive.d.ts +214 -0
- package/dist/agent-chapter-archive.d.ts.map +1 -0
- package/dist/agent-chapter-archive.js +696 -0
- package/dist/agent-chapter-archive.js.map +1 -0
- package/dist/agent-chapter-search.d.ts +116 -0
- package/dist/agent-chapter-search.d.ts.map +1 -0
- package/dist/agent-chapter-search.js +605 -0
- package/dist/agent-chapter-search.js.map +1 -0
- package/dist/agent-chapter-tools.d.ts +45 -0
- package/dist/agent-chapter-tools.d.ts.map +1 -0
- package/dist/agent-chapter-tools.js +344 -0
- package/dist/agent-chapter-tools.js.map +1 -0
- package/dist/agent-chapters.d.ts +228 -0
- package/dist/agent-chapters.d.ts.map +1 -0
- package/dist/agent-chapters.js +737 -0
- package/dist/agent-chapters.js.map +1 -0
- package/dist/agent-data-parts.d.ts +167 -0
- package/dist/agent-data-parts.d.ts.map +1 -0
- package/dist/agent-data-parts.js +68 -0
- package/dist/agent-data-parts.js.map +1 -0
- package/dist/agent-errors.d.ts +41 -0
- package/dist/agent-errors.d.ts.map +1 -0
- package/dist/agent-errors.js +191 -0
- package/dist/agent-errors.js.map +1 -0
- package/dist/agent-follow-up-store.d.ts +273 -0
- package/dist/agent-follow-up-store.d.ts.map +1 -0
- package/dist/agent-follow-up-store.js +703 -0
- package/dist/agent-follow-up-store.js.map +1 -0
- package/dist/agent-follow-ups.d.ts +314 -0
- package/dist/agent-follow-ups.d.ts.map +1 -0
- package/dist/agent-follow-ups.js +840 -0
- package/dist/agent-follow-ups.js.map +1 -0
- package/dist/agent-memory.d.ts +323 -0
- package/dist/agent-memory.d.ts.map +1 -0
- package/dist/agent-memory.js +1270 -0
- package/dist/agent-memory.js.map +1 -0
- package/dist/agent-message-parts.d.ts +58 -0
- package/dist/agent-message-parts.d.ts.map +1 -0
- package/dist/agent-message-parts.js +163 -0
- package/dist/agent-message-parts.js.map +1 -0
- package/dist/agent-remote-mcp-credentials.d.ts +154 -0
- package/dist/agent-remote-mcp-credentials.d.ts.map +1 -0
- package/dist/agent-remote-mcp-credentials.js +287 -0
- package/dist/agent-remote-mcp-credentials.js.map +1 -0
- package/dist/agent-remote-mcp-crypto.d.ts +32 -0
- package/dist/agent-remote-mcp-crypto.d.ts.map +1 -0
- package/dist/agent-remote-mcp-crypto.js +101 -0
- package/dist/agent-remote-mcp-crypto.js.map +1 -0
- package/dist/agent-remote-mcp-install.d.ts +299 -0
- package/dist/agent-remote-mcp-install.d.ts.map +1 -0
- package/dist/agent-remote-mcp-install.js +785 -0
- package/dist/agent-remote-mcp-install.js.map +1 -0
- package/dist/agent-remote-mcp-oauth.d.ts +73 -0
- package/dist/agent-remote-mcp-oauth.d.ts.map +1 -0
- package/dist/agent-remote-mcp-oauth.js +344 -0
- package/dist/agent-remote-mcp-oauth.js.map +1 -0
- package/dist/agent-remote-mcp-transport.d.ts +46 -0
- package/dist/agent-remote-mcp-transport.d.ts.map +1 -0
- package/dist/agent-remote-mcp-transport.js +225 -0
- package/dist/agent-remote-mcp-transport.js.map +1 -0
- package/dist/agent-remote-mcp.d.ts +147 -0
- package/dist/agent-remote-mcp.d.ts.map +1 -0
- package/dist/agent-remote-mcp.js +681 -0
- package/dist/agent-remote-mcp.js.map +1 -0
- package/dist/agent-room-redaction.d.ts +66 -0
- package/dist/agent-room-redaction.d.ts.map +1 -0
- package/dist/agent-room-redaction.js +264 -0
- package/dist/agent-room-redaction.js.map +1 -0
- package/dist/agent-rooms.d.ts +209 -0
- package/dist/agent-rooms.d.ts.map +1 -0
- package/dist/agent-rooms.js +244 -0
- package/dist/agent-rooms.js.map +1 -0
- package/dist/agent-runtime.d.ts +541 -0
- package/dist/agent-runtime.d.ts.map +1 -0
- package/dist/agent-runtime.js +4260 -0
- package/dist/agent-runtime.js.map +1 -0
- package/dist/agent-skill-archive.d.ts +20 -0
- package/dist/agent-skill-archive.d.ts.map +1 -0
- package/dist/agent-skill-archive.js +316 -0
- package/dist/agent-skill-archive.js.map +1 -0
- package/dist/agent-skill-discovery.d.ts +96 -0
- package/dist/agent-skill-discovery.d.ts.map +1 -0
- package/dist/agent-skill-discovery.js +464 -0
- package/dist/agent-skill-discovery.js.map +1 -0
- package/dist/agent-skill-format.d.ts +57 -0
- package/dist/agent-skill-format.d.ts.map +1 -0
- package/dist/agent-skill-format.js +238 -0
- package/dist/agent-skill-format.js.map +1 -0
- package/dist/agent-skill-import.d.ts +64 -0
- package/dist/agent-skill-import.d.ts.map +1 -0
- package/dist/agent-skill-import.js +295 -0
- package/dist/agent-skill-import.js.map +1 -0
- package/dist/agent-skill-publish.d.ts +51 -0
- package/dist/agent-skill-publish.d.ts.map +1 -0
- package/dist/agent-skill-publish.js +136 -0
- package/dist/agent-skill-publish.js.map +1 -0
- package/dist/agent-skill-store.d.ts +252 -0
- package/dist/agent-skill-store.d.ts.map +1 -0
- package/dist/agent-skill-store.js +514 -0
- package/dist/agent-skill-store.js.map +1 -0
- package/dist/agent-skills.d.ts +85 -0
- package/dist/agent-skills.d.ts.map +1 -0
- package/dist/agent-skills.js +291 -0
- package/dist/agent-skills.js.map +1 -0
- package/dist/agent-transcript-pager.d.ts +77 -0
- package/dist/agent-transcript-pager.d.ts.map +1 -0
- package/dist/agent-transcript-pager.js +210 -0
- package/dist/agent-transcript-pager.js.map +1 -0
- package/dist/agent-user-data-parts.d.ts +20 -0
- package/dist/agent-user-data-parts.d.ts.map +1 -0
- package/dist/agent-user-data-parts.js +161 -0
- package/dist/agent-user-data-parts.js.map +1 -0
- package/dist/agent-user-transcript.d.ts +46 -0
- package/dist/agent-user-transcript.d.ts.map +1 -0
- package/dist/agent-user-transcript.js +106 -0
- package/dist/agent-user-transcript.js.map +1 -0
- package/dist/agent-voice-notes.d.ts +15 -0
- package/dist/agent-voice-notes.d.ts.map +1 -0
- package/dist/agent-voice-notes.js +17 -0
- package/dist/agent-voice-notes.js.map +1 -0
- package/dist/agent-wakes.d.ts +213 -0
- package/dist/agent-wakes.d.ts.map +1 -0
- package/dist/agent-wakes.js +534 -0
- package/dist/agent-wakes.js.map +1 -0
- package/dist/agent.d.ts +1780 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +3123 -0
- package/dist/agent.js.map +1 -0
- package/dist/ai.d.ts +583 -0
- package/dist/ai.d.ts.map +1 -0
- package/dist/ai.js +1858 -0
- package/dist/ai.js.map +1 -0
- package/dist/app-manifest.d.ts +255 -0
- package/dist/app-manifest.d.ts.map +1 -0
- package/dist/app-manifest.js +372 -0
- package/dist/app-manifest.js.map +1 -0
- package/dist/app-users.d.ts +30 -0
- package/dist/app-users.d.ts.map +1 -0
- package/dist/app-users.js +91 -0
- package/dist/app-users.js.map +1 -0
- package/dist/audit-archive.d.ts +85 -0
- package/dist/audit-archive.d.ts.map +1 -0
- package/dist/audit-archive.js +178 -0
- package/dist/audit-archive.js.map +1 -0
- package/dist/audit.d.ts +54 -0
- package/dist/audit.d.ts.map +1 -0
- package/dist/audit.js +95 -0
- package/dist/audit.js.map +1 -0
- package/dist/auth-logger.d.ts +28 -0
- package/dist/auth-logger.d.ts.map +1 -0
- package/dist/auth-logger.js +53 -0
- package/dist/auth-logger.js.map +1 -0
- package/dist/auth-password-secret.d.ts +59 -0
- package/dist/auth-password-secret.d.ts.map +1 -0
- package/dist/auth-password-secret.js +409 -0
- package/dist/auth-password-secret.js.map +1 -0
- package/dist/auth-schema.d.ts +120 -0
- package/dist/auth-schema.d.ts.map +1 -0
- package/dist/auth-schema.js +285 -0
- package/dist/auth-schema.js.map +1 -0
- package/dist/auth.d.ts +321 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +1562 -0
- package/dist/auth.js.map +1 -0
- package/dist/backup-archive.d.ts +257 -0
- package/dist/backup-archive.d.ts.map +1 -0
- package/dist/backup-archive.js +2068 -0
- package/dist/backup-archive.js.map +1 -0
- package/dist/backup-crypto.d.ts +313 -0
- package/dist/backup-crypto.d.ts.map +1 -0
- package/dist/backup-crypto.js +693 -0
- package/dist/backup-crypto.js.map +1 -0
- package/dist/backup-format.d.ts +200 -0
- package/dist/backup-format.d.ts.map +1 -0
- package/dist/backup-format.js +1247 -0
- package/dist/backup-format.js.map +1 -0
- package/dist/cli.d.ts +9 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +15 -0
- package/dist/cli.js.map +1 -0
- package/dist/clock.d.ts +35 -0
- package/dist/clock.d.ts.map +1 -0
- package/dist/clock.js +83 -0
- package/dist/clock.js.map +1 -0
- package/dist/data-client.d.ts +187 -0
- package/dist/data-client.d.ts.map +1 -0
- package/dist/data-client.js +590 -0
- package/dist/data-client.js.map +1 -0
- package/dist/data-secrets.d.ts +164 -0
- package/dist/data-secrets.d.ts.map +1 -0
- package/dist/data-secrets.js +383 -0
- package/dist/data-secrets.js.map +1 -0
- package/dist/db.d.ts +453 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +324 -0
- package/dist/db.js.map +1 -0
- package/dist/dev-activity.d.ts +48 -0
- package/dist/dev-activity.d.ts.map +1 -0
- package/dist/dev-activity.js +50 -0
- package/dist/dev-activity.js.map +1 -0
- package/dist/do.d.ts +429 -0
- package/dist/do.d.ts.map +1 -0
- package/dist/do.js +4526 -0
- package/dist/do.js.map +1 -0
- package/dist/email.d.ts +137 -0
- package/dist/email.d.ts.map +1 -0
- package/dist/email.js +527 -0
- package/dist/email.js.map +1 -0
- package/dist/generated/admin-assets.d.ts +16 -0
- package/dist/generated/admin-assets.d.ts.map +1 -0
- package/dist/generated/admin-assets.js +16 -0
- package/dist/generated/admin-assets.js.map +1 -0
- package/dist/generated/admin-docs-assets.d.ts +3 -0
- package/dist/generated/admin-docs-assets.d.ts.map +1 -0
- package/dist/generated/admin-docs-assets.js +3 -0
- package/dist/generated/admin-docs-assets.js.map +1 -0
- package/dist/handoff-consumer.d.ts +79 -0
- package/dist/handoff-consumer.d.ts.map +1 -0
- package/dist/handoff-consumer.js +300 -0
- package/dist/handoff-consumer.js.map +1 -0
- package/dist/handoff.d.ts +70 -0
- package/dist/handoff.d.ts.map +1 -0
- package/dist/handoff.js +228 -0
- package/dist/handoff.js.map +1 -0
- package/dist/host-auth.d.ts +47 -0
- package/dist/host-auth.d.ts.map +1 -0
- package/dist/host-auth.js +59 -0
- package/dist/host-auth.js.map +1 -0
- package/dist/host-user.d.ts +15 -0
- package/dist/host-user.d.ts.map +1 -0
- package/dist/host-user.js +23 -0
- package/dist/host-user.js.map +1 -0
- package/dist/html.d.ts +35 -0
- package/dist/html.d.ts.map +1 -0
- package/dist/html.js +634 -0
- package/dist/html.js.map +1 -0
- package/dist/http-error.d.ts +13 -0
- package/dist/http-error.d.ts.map +1 -0
- package/dist/http-error.js +15 -0
- package/dist/http-error.js.map +1 -0
- package/dist/imports.d.ts +219 -0
- package/dist/imports.d.ts.map +1 -0
- package/dist/imports.js +571 -0
- package/dist/imports.js.map +1 -0
- package/dist/index.d.ts +78 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +76 -0
- package/dist/index.js.map +1 -0
- package/dist/inference-tape.d.ts +64 -0
- package/dist/inference-tape.d.ts.map +1 -0
- package/dist/inference-tape.js +278 -0
- package/dist/inference-tape.js.map +1 -0
- package/dist/internal.d.ts +15 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.js +48 -0
- package/dist/internal.js.map +1 -0
- package/dist/jobs.d.ts +796 -0
- package/dist/jobs.d.ts.map +1 -0
- package/dist/jobs.js +538 -0
- package/dist/jobs.js.map +1 -0
- package/dist/json-schema.d.ts +91 -0
- package/dist/json-schema.d.ts.map +1 -0
- package/dist/json-schema.js +884 -0
- package/dist/json-schema.js.map +1 -0
- package/dist/knowledge.d.ts +33 -0
- package/dist/knowledge.d.ts.map +1 -0
- package/dist/knowledge.js +348 -0
- package/dist/knowledge.js.map +1 -0
- package/dist/local-dev-chrome.d.ts +29 -0
- package/dist/local-dev-chrome.d.ts.map +1 -0
- package/dist/local-dev-chrome.js +514 -0
- package/dist/local-dev-chrome.js.map +1 -0
- package/dist/local-dev-personas.d.ts +15 -0
- package/dist/local-dev-personas.d.ts.map +1 -0
- package/dist/local-dev-personas.js +46 -0
- package/dist/local-dev-personas.js.map +1 -0
- package/dist/mcp.d.ts +567 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +2480 -0
- package/dist/mcp.js.map +1 -0
- package/dist/modules.d.ts +145 -0
- package/dist/modules.d.ts.map +1 -0
- package/dist/modules.js +164 -0
- package/dist/modules.js.map +1 -0
- package/dist/notification-client.d.ts +94 -0
- package/dist/notification-client.d.ts.map +1 -0
- package/dist/notification-client.js +127 -0
- package/dist/notification-client.js.map +1 -0
- package/dist/notifications.d.ts +447 -0
- package/dist/notifications.d.ts.map +1 -0
- package/dist/notifications.js +1625 -0
- package/dist/notifications.js.map +1 -0
- package/dist/operator-data-query.d.ts +117 -0
- package/dist/operator-data-query.d.ts.map +1 -0
- package/dist/operator-data-query.js +431 -0
- package/dist/operator-data-query.js.map +1 -0
- package/dist/operator-mcp.d.ts +63 -0
- package/dist/operator-mcp.d.ts.map +1 -0
- package/dist/operator-mcp.js +2828 -0
- package/dist/operator-mcp.js.map +1 -0
- package/dist/outbound-https.d.ts +24 -0
- package/dist/outbound-https.d.ts.map +1 -0
- package/dist/outbound-https.js +172 -0
- package/dist/outbound-https.js.map +1 -0
- package/dist/pitr.d.ts +60 -0
- package/dist/pitr.d.ts.map +1 -0
- package/dist/pitr.js +94 -0
- package/dist/pitr.js.map +1 -0
- package/dist/platform.d.ts +199 -0
- package/dist/platform.d.ts.map +1 -0
- package/dist/platform.js +196 -0
- package/dist/platform.js.map +1 -0
- package/dist/policies.d.ts +115 -0
- package/dist/policies.d.ts.map +1 -0
- package/dist/policies.js +158 -0
- package/dist/policies.js.map +1 -0
- package/dist/principal.d.ts +143 -0
- package/dist/principal.d.ts.map +1 -0
- package/dist/principal.js +174 -0
- package/dist/principal.js.map +1 -0
- package/dist/realtime.d.ts +280 -0
- package/dist/realtime.d.ts.map +1 -0
- package/dist/realtime.js +578 -0
- package/dist/realtime.js.map +1 -0
- package/dist/retrieval.d.ts +263 -0
- package/dist/retrieval.d.ts.map +1 -0
- package/dist/retrieval.js +602 -0
- package/dist/retrieval.js.map +1 -0
- package/dist/runtime-error-client.d.ts +10 -0
- package/dist/runtime-error-client.d.ts.map +1 -0
- package/dist/runtime-error-client.js +70 -0
- package/dist/runtime-error-client.js.map +1 -0
- package/dist/runtime-errors.d.ts +180 -0
- package/dist/runtime-errors.d.ts.map +1 -0
- package/dist/runtime-errors.js +572 -0
- package/dist/runtime-errors.js.map +1 -0
- package/dist/schema.d.ts +434 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +232 -0
- package/dist/schema.js.map +1 -0
- package/dist/search.d.ts +216 -0
- package/dist/search.d.ts.map +1 -0
- package/dist/search.js +785 -0
- package/dist/search.js.map +1 -0
- package/dist/settings.d.ts +191 -0
- package/dist/settings.d.ts.map +1 -0
- package/dist/settings.js +478 -0
- package/dist/settings.js.map +1 -0
- package/dist/sql.d.ts +7 -0
- package/dist/sql.d.ts.map +1 -0
- package/dist/sql.js +30 -0
- package/dist/sql.js.map +1 -0
- package/dist/storage-client.d.ts +69 -0
- package/dist/storage-client.d.ts.map +1 -0
- package/dist/storage-client.js +469 -0
- package/dist/storage-client.js.map +1 -0
- package/dist/storage.d.ts +21 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +82 -0
- package/dist/storage.js.map +1 -0
- package/dist/subject-domains.d.ts +15 -0
- package/dist/subject-domains.d.ts.map +1 -0
- package/dist/subject-domains.js +38 -0
- package/dist/subject-domains.js.map +1 -0
- package/dist/support.d.ts +132 -0
- package/dist/support.d.ts.map +1 -0
- package/dist/support.js +483 -0
- package/dist/support.js.map +1 -0
- package/dist/system-tables.d.ts +12 -0
- package/dist/system-tables.d.ts.map +1 -0
- package/dist/system-tables.js +225 -0
- package/dist/system-tables.js.map +1 -0
- package/dist/test.d.ts +57 -0
- package/dist/test.d.ts.map +1 -0
- package/dist/test.js +454 -0
- package/dist/test.js.map +1 -0
- package/dist/vite.d.ts +82 -0
- package/dist/vite.d.ts.map +1 -0
- package/dist/vite.js +82 -0
- package/dist/vite.js.map +1 -0
- package/dist/webpush-crypto.d.ts +39 -0
- package/dist/webpush-crypto.d.ts.map +1 -0
- package/dist/webpush-crypto.js +213 -0
- package/dist/webpush-crypto.js.map +1 -0
- package/dist/webpush-private.d.ts +11 -0
- package/dist/webpush-private.d.ts.map +1 -0
- package/dist/webpush-private.js +11 -0
- package/dist/webpush-private.js.map +1 -0
- package/dist/webpush.d.ts +86 -0
- package/dist/webpush.d.ts.map +1 -0
- package/dist/webpush.js +375 -0
- package/dist/webpush.js.map +1 -0
- package/dist/worker.d.ts +185 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +13021 -0
- package/dist/worker.js.map +1 -0
- package/package.json +161 -8
- package/README.md +0 -3
|
@@ -0,0 +1,884 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { stableJson } from "./internal.js";
|
|
3
|
+
export function isZodSchema(schema) {
|
|
4
|
+
return typeof schema.parse === "function";
|
|
5
|
+
}
|
|
6
|
+
export function jsonSchemaFor(schema, options = {}) {
|
|
7
|
+
if (isZodSchema(schema))
|
|
8
|
+
return z.toJSONSchema(schema, options);
|
|
9
|
+
return schema;
|
|
10
|
+
}
|
|
11
|
+
export function parseWithSchema(schema, value, path) {
|
|
12
|
+
if (isZodSchema(schema))
|
|
13
|
+
return schema.parse(value);
|
|
14
|
+
return validateJsonSchema(schema, value, path);
|
|
15
|
+
}
|
|
16
|
+
export function validateJsonSchema(schema, value, path) {
|
|
17
|
+
if (!schema.type)
|
|
18
|
+
return value;
|
|
19
|
+
if (Array.isArray(schema.enum) && !schema.enum.some((candidate) => jsonSchemaValuesEqual(candidate, value))) {
|
|
20
|
+
throw new Error(`${path} must be one of ${schema.enum.map(String).join(", ")}`);
|
|
21
|
+
}
|
|
22
|
+
switch (schema.type) {
|
|
23
|
+
case "object": {
|
|
24
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
25
|
+
throw new Error(`${path} must be an object`);
|
|
26
|
+
const object = value;
|
|
27
|
+
const properties = schema.properties ?? {};
|
|
28
|
+
for (const required of schema.required ?? []) {
|
|
29
|
+
if (object[required] === undefined)
|
|
30
|
+
throw new Error(`${path}.${required} is required`);
|
|
31
|
+
}
|
|
32
|
+
if (schema.additionalProperties === false) {
|
|
33
|
+
for (const key of Object.keys(object)) {
|
|
34
|
+
if (!properties[key])
|
|
35
|
+
throw new Error(`${path}.${key} is not allowed`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return Object.fromEntries(Object.entries(object).map(([key, item]) => [key, properties[key] ? validateJsonSchema(properties[key], item, `${path}.${key}`) : item]));
|
|
39
|
+
}
|
|
40
|
+
case "array": {
|
|
41
|
+
if (!Array.isArray(value))
|
|
42
|
+
throw new Error(`${path} must be an array`);
|
|
43
|
+
if (typeof schema.minItems === "number" && value.length < schema.minItems)
|
|
44
|
+
throw new Error(`${path} must contain at least ${schema.minItems} items`);
|
|
45
|
+
if (typeof schema.maxItems === "number" && value.length > schema.maxItems)
|
|
46
|
+
throw new Error(`${path} must contain at most ${schema.maxItems} items`);
|
|
47
|
+
return schema.items ? value.map((item, index) => validateJsonSchema(schema.items, item, `${path}[${index}]`)) : value;
|
|
48
|
+
}
|
|
49
|
+
case "string":
|
|
50
|
+
if (typeof value !== "string")
|
|
51
|
+
throw new Error(`${path} must be a string`);
|
|
52
|
+
if (typeof schema.minLength === "number" && value.length < schema.minLength)
|
|
53
|
+
throw new Error(`${path} must be at least ${schema.minLength} characters`);
|
|
54
|
+
if (typeof schema.maxLength === "number" && value.length > schema.maxLength)
|
|
55
|
+
throw new Error(`${path} must be at most ${schema.maxLength} characters`);
|
|
56
|
+
return value;
|
|
57
|
+
case "number":
|
|
58
|
+
case "integer":
|
|
59
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
60
|
+
throw new Error(`${path} must be a number`);
|
|
61
|
+
if (schema.type === "integer" && !Number.isInteger(value))
|
|
62
|
+
throw new Error(`${path} must be an integer`);
|
|
63
|
+
if (typeof schema.minimum === "number" && value < schema.minimum)
|
|
64
|
+
throw new Error(`${path} must be at least ${schema.minimum}`);
|
|
65
|
+
if (typeof schema.maximum === "number" && value > schema.maximum)
|
|
66
|
+
throw new Error(`${path} must be at most ${schema.maximum}`);
|
|
67
|
+
return value;
|
|
68
|
+
case "boolean":
|
|
69
|
+
if (typeof value !== "boolean")
|
|
70
|
+
throw new Error(`${path} must be a boolean`);
|
|
71
|
+
return value;
|
|
72
|
+
case "null":
|
|
73
|
+
if (value !== null)
|
|
74
|
+
throw new Error(`${path} must be null`);
|
|
75
|
+
return value;
|
|
76
|
+
default:
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Keywords a strict tool-parameter schema may not carry.
|
|
82
|
+
*
|
|
83
|
+
* They are validation, not shape, and the provider rejects them outright — the
|
|
84
|
+
* whole request, not the one tool. Dropping them from what is published loosens
|
|
85
|
+
* nothing: the action still validates its input with its own schema when the
|
|
86
|
+
* call arrives, so a discarded `maxLength` is enforced a moment later by the
|
|
87
|
+
* thing that always enforced it.
|
|
88
|
+
*/
|
|
89
|
+
const NON_STRUCTURAL_JSON_SCHEMA_KEYWORDS = new Set([
|
|
90
|
+
"minLength", "maxLength", "pattern", "format",
|
|
91
|
+
"minimum", "maximum", "exclusiveMinimum", "exclusiveMaximum", "multipleOf",
|
|
92
|
+
"minItems", "maxItems", "uniqueItems",
|
|
93
|
+
"minProperties", "maxProperties",
|
|
94
|
+
"default", "examples", "contentEncoding", "contentMediaType",
|
|
95
|
+
"$schema", "$id", "$comment"
|
|
96
|
+
]);
|
|
97
|
+
/**
|
|
98
|
+
* The strict-subset form of a schema, for publishing to a model.
|
|
99
|
+
*
|
|
100
|
+
* Providers in the OpenAI family validate every tool's `parameters` against a
|
|
101
|
+
* narrow subset and fail the entire request when one tool falls outside it —
|
|
102
|
+
* so an app whose schemas are ordinary, valid JSON Schema gets no reply at all,
|
|
103
|
+
* and nothing says which tool was at fault. This makes the published form
|
|
104
|
+
* conform without touching what the action validates with.
|
|
105
|
+
*
|
|
106
|
+
* Objects are closed. Property *requiredness* is left exactly as it was: an
|
|
107
|
+
* optional property could only be made strict by declaring a null union, and a
|
|
108
|
+
* schema derived from `.optional()` without `.nullable()` would then invite a
|
|
109
|
+
* `null` its own validator rejects. Where that matters, an action declares its
|
|
110
|
+
* tool schema itself.
|
|
111
|
+
*
|
|
112
|
+
* A reused or recursive subschema arrives as a root-relative `$ref` into
|
|
113
|
+
* `$defs`, and the reference is what gets published — that is the point of it.
|
|
114
|
+
* But every judgment about a child's *shape* — whether it permits null, whether
|
|
115
|
+
* it constrains anything — resolves the reference first, because a nullable
|
|
116
|
+
* shape lifted into `$defs` is otherwise read as one that refuses null and the
|
|
117
|
+
* property stays optional in a schema that must have none. `root` is the
|
|
118
|
+
* document those references point into; it defaults to the schema itself, which
|
|
119
|
+
* is right for every call that starts at a root.
|
|
120
|
+
*/
|
|
121
|
+
export function strictJsonSchema(schema, root = schema) {
|
|
122
|
+
if (!schema || typeof schema !== "object" || Array.isArray(schema))
|
|
123
|
+
return schema;
|
|
124
|
+
// `oneOf` and `anyOf` at one path are two different claims about the same
|
|
125
|
+
// value, and the subset has only `anyOf`. Merging them would publish a union
|
|
126
|
+
// neither of them asked for, so this refuses instead of guessing: the schema
|
|
127
|
+
// is a mistake a person has to resolve, not a shape to invent.
|
|
128
|
+
if (Array.isArray(schema.oneOf) && Array.isArray(schema.anyOf)) {
|
|
129
|
+
throw new Error("A strict tool schema cannot combine oneOf and anyOf at the same path.");
|
|
130
|
+
}
|
|
131
|
+
const next = {};
|
|
132
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
133
|
+
// Reused and recursive subschemas live in `$defs` and are reached by
|
|
134
|
+
// `$ref`. They are published verbatim, so a validation keyword left inside
|
|
135
|
+
// one refuses the request exactly like a keyword at the top level.
|
|
136
|
+
if ((key === "$defs" || key === "definitions") && value && typeof value === "object" && !Array.isArray(value)) {
|
|
137
|
+
next[key] = Object.fromEntries(Object.entries(value).map(([name, child]) => [name, strictJsonSchema(child, root)]));
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (NON_STRUCTURAL_JSON_SCHEMA_KEYWORDS.has(key))
|
|
141
|
+
continue;
|
|
142
|
+
if (key === "properties" && value && typeof value === "object") {
|
|
143
|
+
// A property whose schema constrains nothing — `{}`, from `z.unknown()`
|
|
144
|
+
// or a hand-written open field — has no strict form at all, and one of
|
|
145
|
+
// them refuses the entire request. It cannot be given a shape, so it is
|
|
146
|
+
// offered as a string holding JSON, which the subset does have a rule
|
|
147
|
+
// for. `restoreStrictActionInput` parses it back before the action's own
|
|
148
|
+
// schema sees it. Leaving the property out instead would publish a tool
|
|
149
|
+
// that quietly cannot carry the field at all.
|
|
150
|
+
next.properties = Object.fromEntries(Object.entries(value)
|
|
151
|
+
.map(([name, child]) => {
|
|
152
|
+
const normalized = strictJsonSchema(child, root);
|
|
153
|
+
return [name, isUnconstrainedJsonSchema(normalized, root) ? jsonEncodedSchema(normalized, "value") : normalized];
|
|
154
|
+
}));
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if ((key === "items" || key === "not") && value && typeof value === "object") {
|
|
158
|
+
next[key] = strictJsonSchema(value, root);
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
// The subset has `anyOf` and no `oneOf`. Publishing the union as `anyOf`
|
|
162
|
+
// drops only the exclusivity claim, which the action's own schema still
|
|
163
|
+
// enforces when the call arrives — a value matching two branches is refused
|
|
164
|
+
// there, with a message, rather than here by a provider naming no tool.
|
|
165
|
+
if ((key === "anyOf" || key === "oneOf") && Array.isArray(value)) {
|
|
166
|
+
next.anyOf = value.map((entry) => strictJsonSchema(entry, root));
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (key === "allOf" && Array.isArray(value)) {
|
|
170
|
+
next[key] = value.map((entry) => strictJsonSchema(entry, root));
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
// `const` is how a literal — a discriminator, a schema version — arrives
|
|
174
|
+
// from every Zod-shaped schema, and the strict subset has no such keyword.
|
|
175
|
+
// A one-value `enum` is supported and says exactly the same thing, so this
|
|
176
|
+
// is a rename rather than a loosening.
|
|
177
|
+
if (key === "const") {
|
|
178
|
+
if (next.enum === undefined && typeof value === "string")
|
|
179
|
+
next.enum = [value];
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
// `enum` in the strict subset is a set of strings. A boolean or numeric
|
|
183
|
+
// literal — `z.literal(true)`, a schema version — publishes as its plain
|
|
184
|
+
// type; the action's own schema still refuses any other value.
|
|
185
|
+
if (key === "enum" && Array.isArray(value)) {
|
|
186
|
+
if (value.every((entry) => typeof entry === "string"))
|
|
187
|
+
next.enum = value;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
next[key] = value;
|
|
191
|
+
}
|
|
192
|
+
// A union branch often carries `properties` with no `type`, and an unclosed
|
|
193
|
+
// object anywhere in the tree fails the whole request just the same.
|
|
194
|
+
const isObject = next.type === "object" || (next.type === undefined && next.properties !== undefined);
|
|
195
|
+
if (isObject && next.additionalProperties === undefined)
|
|
196
|
+
next.additionalProperties = false;
|
|
197
|
+
// A property whose own schema already permits null can be published as
|
|
198
|
+
// required without changing what anything accepts: the model may send null,
|
|
199
|
+
// and a validator that made the property optional-and-nullable takes null.
|
|
200
|
+
// This is the safe half of "derive `required` automatically" — a property
|
|
201
|
+
// that is merely optional keeps its optionality, because inviting a null its
|
|
202
|
+
// validator would reject is the one way this could bite.
|
|
203
|
+
if (isObject && next.properties) {
|
|
204
|
+
const required = new Set(next.required ?? []);
|
|
205
|
+
for (const [name, child] of Object.entries(next.properties)) {
|
|
206
|
+
if (!required.has(name) && permitsNull(child, root))
|
|
207
|
+
required.add(name);
|
|
208
|
+
}
|
|
209
|
+
if (required.size)
|
|
210
|
+
next.required = [...Object.keys(next.properties)].filter((name) => required.has(name));
|
|
211
|
+
}
|
|
212
|
+
return next;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* The strict form of a *tool's* `parameters`, which has one rule beyond the
|
|
216
|
+
* rest: the root must be a single object. A discriminated union — the natural
|
|
217
|
+
* way to write an action that takes several shapes — publishes `anyOf` at the
|
|
218
|
+
* root, which is ordinary valid JSON Schema and refused outright, taking every
|
|
219
|
+
* other tool in the request down with it.
|
|
220
|
+
*
|
|
221
|
+
* So a union root is merged into one object: every branch's properties, each
|
|
222
|
+
* nullable because it belongs to only some branches, and the discriminator's
|
|
223
|
+
* enums unioned so the model still sees which values pair with which shape.
|
|
224
|
+
* This widens only what is *published*. The action still validates with its own
|
|
225
|
+
* union, so a call mixing two branches is refused by `parseWithSchema` with a
|
|
226
|
+
* real message rather than by the provider with none.
|
|
227
|
+
*/
|
|
228
|
+
export function strictToolParameters(schema) {
|
|
229
|
+
// Normalise first so the branches are already in subset form when they merge:
|
|
230
|
+
// a discriminator arrives as `const`, and only once that is an `enum` can the
|
|
231
|
+
// branches collapse into one field offering the set of operations. Merging
|
|
232
|
+
// raw would leave eleven single-value unions where one enum belongs.
|
|
233
|
+
const normalized = strictJsonSchema(schema);
|
|
234
|
+
return inlineDefinitions(strictJsonSchema(flattenRootUnion(normalized, normalized)));
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Shapes lifted into `$defs`, put back where a reader needs them.
|
|
238
|
+
*
|
|
239
|
+
* `reused: "ref"` dedupes repeated subschemas to keep a large tool list small,
|
|
240
|
+
* and for an object shape used many times the reference pays its way. Two
|
|
241
|
+
* kinds of definition cost more than they save, and each took a feature down
|
|
242
|
+
* in production:
|
|
243
|
+
*
|
|
244
|
+
* - A leaf — a primitive, an enum, a union of those. On 2026-08-28 a
|
|
245
|
+
* `.nullish()` number published as `{"$ref": "#/$defs/__schema1"}`, the live
|
|
246
|
+
* model never chased the pointer, guessed the field was a string, and the
|
|
247
|
+
* action refused the call every time.
|
|
248
|
+
* - A definition used exactly once. It dedupes nothing, so the reference is
|
|
249
|
+
* pure cost — and Zod lifts *every* property of an object carrying a root
|
|
250
|
+
* `.superRefine()` into `$defs` whether or not anything else uses it. On
|
|
251
|
+
* 2026-09-01 a union of two closed objects (a homework failure price) was
|
|
252
|
+
* published that way; across twenty calls the model sent the object as a
|
|
253
|
+
* string of JSON, and the work was never set.
|
|
254
|
+
*
|
|
255
|
+
* A model reads a published schema as text; a field's *type* has to be visible
|
|
256
|
+
* at the field. So a leaf, or a definition with a single use, is inlined at
|
|
257
|
+
* that use — under the use's own annotations, which win — and whatever no
|
|
258
|
+
* longer has a reference reaching it from the root is dropped from `$defs`.
|
|
259
|
+
* Object and array shapes with several uses keep their references, which is
|
|
260
|
+
* the dedupe's real value, and a definition that reaches itself while being
|
|
261
|
+
* inlined keeps the reference at that point, so recursion terminates.
|
|
262
|
+
*/
|
|
263
|
+
function inlineDefinitions(schema) {
|
|
264
|
+
const defsKey = ["$defs", "definitions"].find((key) => jsonSchemaObject(schema[key]));
|
|
265
|
+
if (!defsKey)
|
|
266
|
+
return schema;
|
|
267
|
+
const definitions = schema[defsKey];
|
|
268
|
+
// Everything below is keyed by definition *name*, decoded from each `$ref`
|
|
269
|
+
// as a JSON Pointer: a definition called `order/line` is referenced as
|
|
270
|
+
// `#/$defs/order~1line`, and a key built by pasting the name into a pointer
|
|
271
|
+
// would never meet its own references.
|
|
272
|
+
const definitionName = (ref) => {
|
|
273
|
+
const target = typeof ref === "string" ? jsonSchemaDefinitionRef(ref) : null;
|
|
274
|
+
return target && target.key === defsKey ? target.name : null;
|
|
275
|
+
};
|
|
276
|
+
// Every reference to a definition under a node, once per occurrence.
|
|
277
|
+
const eachReference = (node, visit) => {
|
|
278
|
+
if (Array.isArray(node)) {
|
|
279
|
+
node.forEach((item) => eachReference(item, visit));
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
const object = jsonSchemaObject(node);
|
|
283
|
+
if (!object)
|
|
284
|
+
return;
|
|
285
|
+
const name = definitionName(object.$ref);
|
|
286
|
+
if (name !== null)
|
|
287
|
+
visit(name);
|
|
288
|
+
Object.values(object).forEach((value) => eachReference(value, visit));
|
|
289
|
+
};
|
|
290
|
+
const bodies = new Map();
|
|
291
|
+
for (const [name, definition] of Object.entries(definitions)) {
|
|
292
|
+
const body = jsonSchemaObject(definition);
|
|
293
|
+
if (body)
|
|
294
|
+
bodies.set(name, body);
|
|
295
|
+
}
|
|
296
|
+
const uses = new Map();
|
|
297
|
+
eachReference(schema, (name) => uses.set(name, (uses.get(name) ?? 0) + 1));
|
|
298
|
+
const inlinable = new Set();
|
|
299
|
+
for (const [name, body] of bodies) {
|
|
300
|
+
if (isLeafJsonSchema(body) || (uses.get(name) ?? 0) <= 1)
|
|
301
|
+
inlinable.add(name);
|
|
302
|
+
}
|
|
303
|
+
if (!inlinable.size)
|
|
304
|
+
return schema;
|
|
305
|
+
const substitute = (node, stack) => {
|
|
306
|
+
if (Array.isArray(node))
|
|
307
|
+
return node.map((item) => substitute(item, stack));
|
|
308
|
+
const object = jsonSchemaObject(node);
|
|
309
|
+
if (!object)
|
|
310
|
+
return node;
|
|
311
|
+
const name = definitionName(object.$ref);
|
|
312
|
+
if (name !== null && inlinable.has(name) && !stack.has(name)) {
|
|
313
|
+
const { $ref: _ref, ...annotations } = object;
|
|
314
|
+
const deeper = new Set(stack).add(name);
|
|
315
|
+
return {
|
|
316
|
+
...substitute(bodies.get(name), deeper),
|
|
317
|
+
...substitute(annotations, deeper),
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
return Object.fromEntries(Object.entries(object).map(([key, value]) => [key, substitute(value, stack)]));
|
|
321
|
+
};
|
|
322
|
+
const { [defsKey]: _definitions, ...rest } = schema;
|
|
323
|
+
const inlined = substitute(rest, new Set());
|
|
324
|
+
const rewritten = new Map();
|
|
325
|
+
for (const [name, body] of bodies)
|
|
326
|
+
rewritten.set(name, substitute(body, new Set([name])));
|
|
327
|
+
// Only what the root still reaches survives: a definition inlined at its one
|
|
328
|
+
// use, or one nothing ever pointed at, has no reader left.
|
|
329
|
+
const reachable = new Set();
|
|
330
|
+
const frontier = new Set();
|
|
331
|
+
eachReference(inlined, (name) => frontier.add(name));
|
|
332
|
+
while (frontier.size) {
|
|
333
|
+
const name = frontier.values().next().value;
|
|
334
|
+
frontier.delete(name);
|
|
335
|
+
if (reachable.has(name) || !rewritten.has(name))
|
|
336
|
+
continue;
|
|
337
|
+
reachable.add(name);
|
|
338
|
+
eachReference(rewritten.get(name), (onward) => {
|
|
339
|
+
if (!reachable.has(onward))
|
|
340
|
+
frontier.add(onward);
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
const surviving = Object.fromEntries(Object.entries(definitions)
|
|
344
|
+
.filter(([name]) => reachable.has(name))
|
|
345
|
+
.map(([name]) => [name, rewritten.get(name) ?? definitions[name]]));
|
|
346
|
+
return Object.keys(surviving).length ? { ...inlined, [defsKey]: surviving } : inlined;
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* A shape that is finished where it stands: a primitive, an enum, or a union
|
|
350
|
+
* of those. No properties or items to walk into, no reference to chase — which
|
|
351
|
+
* is also what makes inlining one always terminate.
|
|
352
|
+
*/
|
|
353
|
+
function isLeafJsonSchema(schema) {
|
|
354
|
+
if (!schema)
|
|
355
|
+
return false;
|
|
356
|
+
if (schema.$ref !== undefined || schema.properties !== undefined || schema.items !== undefined
|
|
357
|
+
|| schema.additionalProperties !== undefined || schema.$defs !== undefined || schema.definitions !== undefined
|
|
358
|
+
|| schema.type === "object" || schema.type === "array")
|
|
359
|
+
return false;
|
|
360
|
+
for (const key of ["anyOf", "oneOf", "allOf"]) {
|
|
361
|
+
const branches = schema[key];
|
|
362
|
+
if (Array.isArray(branches) && !branches.every((branch) => isLeafJsonSchema(jsonSchemaObject(branch))))
|
|
363
|
+
return false;
|
|
364
|
+
}
|
|
365
|
+
if (schema.not !== undefined && !isLeafJsonSchema(jsonSchemaObject(schema.not)))
|
|
366
|
+
return false;
|
|
367
|
+
return true;
|
|
368
|
+
}
|
|
369
|
+
function flattenRootUnion(schema, root = schema) {
|
|
370
|
+
if (!schema || typeof schema !== "object" || Array.isArray(schema))
|
|
371
|
+
return schema;
|
|
372
|
+
const key = ["anyOf", "oneOf"].find((candidate) => Array.isArray(schema[candidate]));
|
|
373
|
+
if (!key)
|
|
374
|
+
return schema;
|
|
375
|
+
// A branch shape used elsewhere too is lifted into `$defs` and stands here as
|
|
376
|
+
// a bare `$ref`, which carries no `properties` of its own. Merging needs the
|
|
377
|
+
// branch's real shape, so the reference is followed before the check below —
|
|
378
|
+
// the alternative is bailing out and publishing `anyOf` at the root, which is
|
|
379
|
+
// the shape a strict provider refuses.
|
|
380
|
+
const branches = schema[key]
|
|
381
|
+
.map((branch) => resolveJsonSchemaRef(jsonSchemaObject(branch), root) ?? branch)
|
|
382
|
+
.map((branch) => flattenRootUnion(branch, root));
|
|
383
|
+
if (!branches.length || !branches.every((branch) => branch && typeof branch === "object" && branch.properties && typeof branch.properties === "object")) {
|
|
384
|
+
return schema;
|
|
385
|
+
}
|
|
386
|
+
const names = [];
|
|
387
|
+
const variants = new Map();
|
|
388
|
+
for (const branch of branches) {
|
|
389
|
+
for (const [name, child] of Object.entries(branch.properties)) {
|
|
390
|
+
if (!variants.has(name)) {
|
|
391
|
+
variants.set(name, []);
|
|
392
|
+
names.push(name);
|
|
393
|
+
}
|
|
394
|
+
variants.get(name).push(child);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
const properties = {};
|
|
398
|
+
for (const name of names) {
|
|
399
|
+
const shapes = variants.get(name);
|
|
400
|
+
const distinct = [];
|
|
401
|
+
for (const shape of shapes.flatMap(unionBranches)) {
|
|
402
|
+
if (!distinct.some((seen) => jsonSchemaValuesEqual(seen, shape)))
|
|
403
|
+
distinct.push(shape);
|
|
404
|
+
}
|
|
405
|
+
// The discriminator: one string enum per branch, which merges into the set
|
|
406
|
+
// of values the model may choose between rather than an opaque union.
|
|
407
|
+
const merged = distinct.length === 1
|
|
408
|
+
? distinct[0]
|
|
409
|
+
: distinct.every((shape) => shape.type === "string" && Array.isArray(shape.enum))
|
|
410
|
+
? { ...distinct[0], type: "string", enum: [...new Set(distinct.flatMap((shape) => shape.enum))] }
|
|
411
|
+
: { anyOf: distinct };
|
|
412
|
+
// A property carried by only some branches, or optional within its own, is
|
|
413
|
+
// absent on the calls that take another shape.
|
|
414
|
+
const universal = shapes.length === branches.length
|
|
415
|
+
&& branches.every((branch) => Array.isArray(branch.required) && branch.required.includes(name));
|
|
416
|
+
properties[name] = universal || permitsNull(merged, root) ? merged : nullableJsonSchema(merged, root);
|
|
417
|
+
}
|
|
418
|
+
const { anyOf: _anyOf, oneOf: _oneOf, ...rest } = schema;
|
|
419
|
+
return { ...rest, type: "object", properties, required: names, additionalProperties: false };
|
|
420
|
+
}
|
|
421
|
+
/** The same schema, also accepting `null`, without nesting a union in a union. */
|
|
422
|
+
function nullableJsonSchema(schema, root) {
|
|
423
|
+
if (Array.isArray(schema.anyOf)) {
|
|
424
|
+
const branches = schema.anyOf;
|
|
425
|
+
return branches.some((branch) => permitsNull(branch, root))
|
|
426
|
+
? schema
|
|
427
|
+
: { ...schema, anyOf: [...branches, { type: "null" }] };
|
|
428
|
+
}
|
|
429
|
+
// A `$ref` is wrapped rather than followed: the reference is what gets
|
|
430
|
+
// published, and a union around it says the same thing about the value.
|
|
431
|
+
return { anyOf: [schema, { type: "null" }] };
|
|
432
|
+
}
|
|
433
|
+
/** One flat list of branches, so merging a union with a union does not nest one. */
|
|
434
|
+
function unionBranches(schema) {
|
|
435
|
+
return Array.isArray(schema.anyOf) && Object.keys(schema).length === 1
|
|
436
|
+
? schema.anyOf.flatMap(unionBranches)
|
|
437
|
+
: [schema];
|
|
438
|
+
}
|
|
439
|
+
function parseJson(text) {
|
|
440
|
+
try {
|
|
441
|
+
return JSON.parse(text);
|
|
442
|
+
}
|
|
443
|
+
catch {
|
|
444
|
+
return undefined;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
// Keywords that only constrain one JSON type, so a schema carrying one with no
|
|
448
|
+
// `type` of its own is read as that type — the way a reader of the published
|
|
449
|
+
// schema reads `{"pattern": …}` as a string field.
|
|
450
|
+
const JSON_TYPE_IMPLYING_KEYWORDS = {
|
|
451
|
+
minLength: "string", maxLength: "string", pattern: "string", format: "string",
|
|
452
|
+
contentEncoding: "string", contentMediaType: "string",
|
|
453
|
+
minimum: "number", maximum: "number", exclusiveMinimum: "number", exclusiveMaximum: "number", multipleOf: "number",
|
|
454
|
+
properties: "object", additionalProperties: "object", patternProperties: "object", propertyNames: "object",
|
|
455
|
+
required: "object", minProperties: "object", maxProperties: "object", dependentRequired: "object",
|
|
456
|
+
dependentSchemas: "object", unevaluatedProperties: "object",
|
|
457
|
+
items: "array", prefixItems: "array", contains: "array", minItems: "array", maxItems: "array",
|
|
458
|
+
uniqueItems: "array", minContains: "array", maxContains: "array", unevaluatedItems: "array"
|
|
459
|
+
};
|
|
460
|
+
/**
|
|
461
|
+
* Whether a value of the given JSON type could satisfy the schema. Reads the
|
|
462
|
+
* declared `type` — or, without one, the type its type-specific keywords
|
|
463
|
+
* imply — the literal keywords (`enum`, `const`), and every union and `allOf`
|
|
464
|
+
* branch; a schema that names no type at all admits every type. Used to tell
|
|
465
|
+
* an encoding from an answer when a model hands back a string, so where it
|
|
466
|
+
* cannot tell (a `not`, an unresolved `$ref`) it answers that the type is
|
|
467
|
+
* admitted, and the string is left as it arrived.
|
|
468
|
+
*
|
|
469
|
+
* `path` holds the schemas being judged above this one, so a recursive
|
|
470
|
+
* reference ends instead of looping. It is a path, not a visited set: two
|
|
471
|
+
* `allOf` branches reaching the same definition are two judgments of it, and a
|
|
472
|
+
* branch that leads back to a schema already being judged says nothing new
|
|
473
|
+
* about it, so it is left out rather than counted either way.
|
|
474
|
+
*/
|
|
475
|
+
function admitsJsonType(schema, type, root, path = new Set()) {
|
|
476
|
+
const resolved = resolveJsonSchemaRef(jsonSchemaObject(schema), root);
|
|
477
|
+
if (!resolved)
|
|
478
|
+
return true;
|
|
479
|
+
if (isUnconstrainedJsonSchema(resolved, root))
|
|
480
|
+
return true;
|
|
481
|
+
const literalType = (literal) => literal === null ? "null" : Array.isArray(literal) ? "array" : typeof literal === "object" ? "object"
|
|
482
|
+
: typeof literal === "number" ? "number" : typeof literal === "boolean" ? "boolean" : "string";
|
|
483
|
+
if (Array.isArray(resolved.enum))
|
|
484
|
+
return resolved.enum.some((literal) => literalType(literal) === type);
|
|
485
|
+
if (Object.prototype.hasOwnProperty.call(resolved, "const"))
|
|
486
|
+
return literalType(resolved.const) === type;
|
|
487
|
+
const declared = Array.isArray(resolved.type) ? resolved.type : resolved.type !== undefined ? [resolved.type] : [];
|
|
488
|
+
const named = declared.length
|
|
489
|
+
? declared
|
|
490
|
+
: [...new Set(Object.keys(resolved).flatMap((key) => JSON_TYPE_IMPLYING_KEYWORDS[key] ?? []))];
|
|
491
|
+
if (named.length && !named.includes(type)
|
|
492
|
+
&& !(type === "integer" && named.includes("number"))
|
|
493
|
+
&& !(type === "number" && named.includes("integer"))) {
|
|
494
|
+
return false;
|
|
495
|
+
}
|
|
496
|
+
const inner = new Set(path).add(resolved);
|
|
497
|
+
const branches = (key) => (Array.isArray(resolved[key]) ? resolved[key] : []).filter((branch) => {
|
|
498
|
+
const target = resolveJsonSchemaRef(jsonSchemaObject(branch), root);
|
|
499
|
+
return !target || !inner.has(target);
|
|
500
|
+
});
|
|
501
|
+
const admits = (branch) => admitsJsonType(branch, type, root, inner);
|
|
502
|
+
for (const key of ["anyOf", "oneOf"]) {
|
|
503
|
+
const union = branches(key);
|
|
504
|
+
if (union.length && !union.some(admits))
|
|
505
|
+
return false;
|
|
506
|
+
}
|
|
507
|
+
return branches("allOf").every(admits);
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Whether the schema spells this exact string out as one of its values — an
|
|
511
|
+
* `enum` member or a `const`, here or in any union or `allOf` branch. A
|
|
512
|
+
* literal the app named is an answer even when it looks like padding.
|
|
513
|
+
*/
|
|
514
|
+
function namesStringLiteral(schema, literal, root, path = new Set()) {
|
|
515
|
+
const resolved = resolveJsonSchemaRef(jsonSchemaObject(schema), root);
|
|
516
|
+
if (!resolved || path.has(resolved))
|
|
517
|
+
return false;
|
|
518
|
+
if (Array.isArray(resolved.enum) && resolved.enum.includes(literal))
|
|
519
|
+
return true;
|
|
520
|
+
if (resolved.const === literal)
|
|
521
|
+
return true;
|
|
522
|
+
const inner = new Set(path).add(resolved);
|
|
523
|
+
return ["anyOf", "oneOf", "allOf"].some((key) => Array.isArray(resolved[key])
|
|
524
|
+
&& resolved[key].some((branch) => namesStringLiteral(branch, literal, root, inner)));
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Whether a schema accepts `null` outright, by type or through a union branch.
|
|
528
|
+
*
|
|
529
|
+
* With a `root` in hand a reference into that root's `$defs` is followed first,
|
|
530
|
+
* because the whole reason a shape is in `$defs` is that it is used twice, and
|
|
531
|
+
* a nullable shape read as non-nullable is what leaves a property optional in a
|
|
532
|
+
* schema that must have none. `seen` terminates a recursive schema, which is
|
|
533
|
+
* exactly the other thing `$defs` exists for.
|
|
534
|
+
*/
|
|
535
|
+
function permitsNull(schema, root, seen = new Set()) {
|
|
536
|
+
const resolved = resolveJsonSchemaRef(jsonSchemaObject(schema), root);
|
|
537
|
+
if (!resolved || seen.has(resolved))
|
|
538
|
+
return false;
|
|
539
|
+
seen.add(resolved);
|
|
540
|
+
if (resolved.type === "null")
|
|
541
|
+
return true;
|
|
542
|
+
if (Array.isArray(resolved.type) && resolved.type.includes("null"))
|
|
543
|
+
return true;
|
|
544
|
+
for (const key of ["anyOf", "oneOf"]) {
|
|
545
|
+
const branches = resolved[key];
|
|
546
|
+
if (Array.isArray(branches) && branches.some((branch) => permitsNull(branch, root, seen)))
|
|
547
|
+
return true;
|
|
548
|
+
}
|
|
549
|
+
return false;
|
|
550
|
+
}
|
|
551
|
+
function jsonSchemaObject(value) {
|
|
552
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* A `$ref` into the schema's own `$defs`, followed so a judgment about a shape
|
|
556
|
+
* sees the shape rather than the reference. Only root-relative pointers into
|
|
557
|
+
* `$defs`/`definitions` resolve; any other form — a remote `$ref`, a pointer at
|
|
558
|
+
* the document root — is left as it arrived. Nothing here rewrites what is
|
|
559
|
+
* published: resolution informs the judgments, and the reference survives them.
|
|
560
|
+
*/
|
|
561
|
+
function resolveJsonSchemaRef(schema, root, seen = new Set()) {
|
|
562
|
+
if (!schema || typeof schema.$ref !== "string" || !root || seen.has(schema))
|
|
563
|
+
return schema;
|
|
564
|
+
seen.add(schema);
|
|
565
|
+
const reference = jsonSchemaDefinitionRef(schema.$ref);
|
|
566
|
+
const defs = reference ? jsonSchemaObject(root[reference.key]) : null;
|
|
567
|
+
const target = defs && reference ? jsonSchemaObject(defs[reference.name]) : null;
|
|
568
|
+
// A chain of references ends at a shape or at a cycle, and `seen` is what
|
|
569
|
+
// makes the second one end.
|
|
570
|
+
return target ? resolveJsonSchemaRef(target, root, seen) : schema;
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* The definition a root-relative `$ref` names, decoded as RFC 6901 reads a
|
|
574
|
+
* pointer in a URI fragment: percent-decoding first, then `~1` → `/` and
|
|
575
|
+
* `~0` → `~` within each segment. A pointer reaching inside a definition
|
|
576
|
+
* (`#/$defs/a/properties/b`), or anywhere but `$defs`/`definitions`, names no
|
|
577
|
+
* definition.
|
|
578
|
+
*/
|
|
579
|
+
function jsonSchemaDefinitionRef(ref) {
|
|
580
|
+
if (!ref.startsWith("#/"))
|
|
581
|
+
return null;
|
|
582
|
+
let pointer;
|
|
583
|
+
try {
|
|
584
|
+
pointer = decodeURIComponent(ref.slice(1));
|
|
585
|
+
}
|
|
586
|
+
catch {
|
|
587
|
+
return null;
|
|
588
|
+
}
|
|
589
|
+
const segments = pointer.slice(1).split("/").map((segment) => segment.replace(/~1/g, "/").replace(/~0/g, "~"));
|
|
590
|
+
const [key, name] = segments;
|
|
591
|
+
if (segments.length !== 2 || (key !== "$defs" && key !== "definitions") || !name)
|
|
592
|
+
return null;
|
|
593
|
+
return { key, name };
|
|
594
|
+
}
|
|
595
|
+
function jsonSchemaValuesEqual(left, right) {
|
|
596
|
+
if (Object.is(left, right))
|
|
597
|
+
return true;
|
|
598
|
+
try {
|
|
599
|
+
return stableJson(left) === stableJson(right);
|
|
600
|
+
}
|
|
601
|
+
catch {
|
|
602
|
+
return false;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
/* ------------------------------------------------------------------------- *
|
|
606
|
+
* The wire form, and its inverse.
|
|
607
|
+
*
|
|
608
|
+
* `strictJsonSchema` above is the *advertised* form: it removes what the strict
|
|
609
|
+
* subset has no rule for and leaves everything a reader would rely on — an
|
|
610
|
+
* optional property is still optional, an open record is still a record. That
|
|
611
|
+
* form is what an action publishes to documentation, to a manifest, and to any
|
|
612
|
+
* caller asking what it accepts.
|
|
613
|
+
*
|
|
614
|
+
* Two more rewrites are needed before a strict provider will take it, and both
|
|
615
|
+
* change the shape of the *value* the model then sends back:
|
|
616
|
+
*
|
|
617
|
+
* - optionality has to be spelled as a null union with every property
|
|
618
|
+
* required, because the subset has no notion of an omitted key;
|
|
619
|
+
* - a record with open keys, or a value with no shape at all, has no strict
|
|
620
|
+
* form, so it travels as a string holding JSON.
|
|
621
|
+
*
|
|
622
|
+
* Neither is safe on its own: padded nulls and encoded strings reach a handler
|
|
623
|
+
* whose own validator refuses both. So they live here, paired with the inverse
|
|
624
|
+
* that undoes them before anything parses the arguments, and the pair is
|
|
625
|
+
* applied together at the one seam where both halves are in reach — Core's own
|
|
626
|
+
* chat request serialization (`strictChatTools` / `restoreStrictToolCalls` in
|
|
627
|
+
* ai.ts). A custom inference provider that hands tools to a strict API itself
|
|
628
|
+
* reuses these instead of rebuilding them.
|
|
629
|
+
* ------------------------------------------------------------------------- */
|
|
630
|
+
/** Keywords that describe a schema to a reader without constraining any value. */
|
|
631
|
+
const JSON_SCHEMA_ANNOTATION_KEYWORDS = new Set(["title", "description", "$comment", "deprecated", "readOnly", "writeOnly"]);
|
|
632
|
+
/**
|
|
633
|
+
* A schema that constrains nothing — `{}`, from `z.unknown()` or `z.any()`, or
|
|
634
|
+
* one carrying only annotations and validation keywords the subset drops. It
|
|
635
|
+
* reads as harmless and refuses every request, so it travels as JSON in a
|
|
636
|
+
* string and comes back parsed.
|
|
637
|
+
*
|
|
638
|
+
* An unconstrained shape reached through a `$ref` is the same shape, so the
|
|
639
|
+
* reference is followed before the keys are read; without a `root` a `$ref` is
|
|
640
|
+
* its own key and constrains nothing that can be seen from here.
|
|
641
|
+
*/
|
|
642
|
+
function isUnconstrainedJsonSchema(schema, root) {
|
|
643
|
+
const resolved = resolveJsonSchemaRef(schema, root) ?? schema;
|
|
644
|
+
return Object.keys(resolved).every((key) => JSON_SCHEMA_ANNOTATION_KEYWORDS.has(key) || NON_STRUCTURAL_JSON_SCHEMA_KEYWORDS.has(key));
|
|
645
|
+
}
|
|
646
|
+
/** An object whose keys are open — `z.record(...)`, or a hand-written map. */
|
|
647
|
+
function isOpenRecordJsonSchema(schema) {
|
|
648
|
+
return schema.type === "object"
|
|
649
|
+
&& (schema.propertyNames !== undefined || jsonSchemaObject(schema.additionalProperties) !== null);
|
|
650
|
+
}
|
|
651
|
+
function jsonEncodedSchema(schema, subject) {
|
|
652
|
+
const instruction = subject === "object"
|
|
653
|
+
? "Encode this object as a JSON string."
|
|
654
|
+
: "Encode this value as JSON in a string.";
|
|
655
|
+
const description = typeof schema.description === "string" && schema.description
|
|
656
|
+
? `${schema.description} ${instruction}`
|
|
657
|
+
: subject === "object" ? "A JSON object encoded as a string." : "A JSON value encoded as a string.";
|
|
658
|
+
return { type: "string", description };
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* The same schema, also accepting `null`, written the way each shape allows —
|
|
662
|
+
* widening a `type`, extending an existing union, or wrapping in a new one.
|
|
663
|
+
* A one-value `enum` gains `null` alongside so the enum keeps describing the
|
|
664
|
+
* whole set of permitted values rather than contradicting the type.
|
|
665
|
+
*/
|
|
666
|
+
function nullPaddedJsonSchema(schema) {
|
|
667
|
+
const enumClosed = Array.isArray(schema.enum) && !schema.enum.includes(null);
|
|
668
|
+
// An `enum` that lists no `null` refuses one whatever the type says, so a
|
|
669
|
+
// schema that already permits null is only finished if its enum agrees.
|
|
670
|
+
if (permitsNull(schema) && !enumClosed)
|
|
671
|
+
return schema;
|
|
672
|
+
const enumValues = enumClosed ? [...schema.enum, null] : schema.enum;
|
|
673
|
+
if (Object.prototype.hasOwnProperty.call(schema, "const")) {
|
|
674
|
+
return { anyOf: [schema, { type: "null" }] };
|
|
675
|
+
}
|
|
676
|
+
if (typeof schema.type === "string") {
|
|
677
|
+
return { ...schema, type: [schema.type, "null"], ...(enumValues ? { enum: enumValues } : {}) };
|
|
678
|
+
}
|
|
679
|
+
if (Array.isArray(schema.type)) {
|
|
680
|
+
const types = schema.type;
|
|
681
|
+
return {
|
|
682
|
+
...schema,
|
|
683
|
+
type: (types.includes("null") ? types : [...types, "null"]),
|
|
684
|
+
...(enumValues ? { enum: enumValues } : {})
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
if (Array.isArray(schema.anyOf)) {
|
|
688
|
+
const branches = schema.anyOf;
|
|
689
|
+
return {
|
|
690
|
+
...schema,
|
|
691
|
+
anyOf: branches.some((branch) => permitsNull(branch)) ? branches : [...branches, { type: "null" }],
|
|
692
|
+
...(enumValues ? { enum: enumValues } : {})
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
return { anyOf: [schema, { type: "null" }] };
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* The wire form of a tool's parameters or a structured response schema: the
|
|
699
|
+
* advertised strict subset, plus the two rewrites that need
|
|
700
|
+
* `restoreStrictModelValue` on the way back.
|
|
701
|
+
*
|
|
702
|
+
* Applying it twice changes nothing — a padded property already permits null,
|
|
703
|
+
* and an encoded record is a string rather than a record — so a schema that has
|
|
704
|
+
* already been through `strictToolParameters` may pass through here safely.
|
|
705
|
+
*/
|
|
706
|
+
export function strictModelSchema(schema) {
|
|
707
|
+
return padStrictJsonSchema(strictToolParameters(schema));
|
|
708
|
+
}
|
|
709
|
+
function padStrictJsonSchema(value, optional = false) {
|
|
710
|
+
const schema = jsonSchemaObject(value);
|
|
711
|
+
if (!schema)
|
|
712
|
+
return value;
|
|
713
|
+
if (isUnconstrainedJsonSchema(schema)) {
|
|
714
|
+
const encoded = jsonEncodedSchema(schema, "value");
|
|
715
|
+
return optional ? nullPaddedJsonSchema(encoded) : encoded;
|
|
716
|
+
}
|
|
717
|
+
if (isOpenRecordJsonSchema(schema)) {
|
|
718
|
+
const encoded = jsonEncodedSchema(schema, "object");
|
|
719
|
+
return optional ? nullPaddedJsonSchema(encoded) : encoded;
|
|
720
|
+
}
|
|
721
|
+
let next = { ...schema };
|
|
722
|
+
const properties = jsonSchemaObject(schema.properties);
|
|
723
|
+
if (properties) {
|
|
724
|
+
const required = new Set(Array.isArray(schema.required) ? schema.required : []);
|
|
725
|
+
const padded = Object.fromEntries(Object.entries(properties).map(([name, child]) => [name, padStrictJsonSchema(child, !required.has(name))]));
|
|
726
|
+
next = { ...next, properties: padded, required: Object.keys(padded), additionalProperties: false };
|
|
727
|
+
}
|
|
728
|
+
if (schema.items !== undefined)
|
|
729
|
+
next.items = padStrictJsonSchema(schema.items);
|
|
730
|
+
for (const key of ["anyOf", "allOf"]) {
|
|
731
|
+
if (Array.isArray(schema[key])) {
|
|
732
|
+
next[key] = schema[key].map((branch) => padStrictJsonSchema(branch));
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
for (const key of ["$defs", "definitions"]) {
|
|
736
|
+
const defs = jsonSchemaObject(schema[key]);
|
|
737
|
+
if (defs) {
|
|
738
|
+
next[key] = Object.fromEntries(Object.entries(defs).map(([name, child]) => [name, padStrictJsonSchema(child)]));
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
return optional ? nullPaddedJsonSchema(next) : next;
|
|
742
|
+
}
|
|
743
|
+
/**
|
|
744
|
+
* The inverse of `strictModelSchema`, run against the schema as it was *before*
|
|
745
|
+
* the wire form: null padding is stripped where the original left the property
|
|
746
|
+
* optional, JSON-encoded records are parsed back into objects, and a union is
|
|
747
|
+
* followed into the branch the value actually took so the same is done inside
|
|
748
|
+
* it.
|
|
749
|
+
*
|
|
750
|
+
* A value that was never padded passes through unchanged, so running this over
|
|
751
|
+
* arguments from a provider that needed none is safe.
|
|
752
|
+
*/
|
|
753
|
+
export function restoreStrictModelValue(value, schema) {
|
|
754
|
+
return restoreStrictValue(value, schema, schema);
|
|
755
|
+
}
|
|
756
|
+
function restoreStrictValue(value, schemaValue, root) {
|
|
757
|
+
const schema = resolveJsonSchemaRef(jsonSchemaObject(schemaValue), root);
|
|
758
|
+
if (!schema)
|
|
759
|
+
return value;
|
|
760
|
+
if (typeof value === "string" && isUnconstrainedJsonSchema(schema)) {
|
|
761
|
+
try {
|
|
762
|
+
return JSON.parse(value);
|
|
763
|
+
}
|
|
764
|
+
catch {
|
|
765
|
+
return value;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
if (typeof value === "string" && isOpenRecordJsonSchema(schema)) {
|
|
769
|
+
try {
|
|
770
|
+
return jsonSchemaObject(JSON.parse(value)) ?? value;
|
|
771
|
+
}
|
|
772
|
+
catch {
|
|
773
|
+
return value;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
// The strict subset makes every nullable property required, so a model must
|
|
777
|
+
// send *something* for a field it has nothing for — and a weak one sends the
|
|
778
|
+
// four letters "null". On a field that admits null, that is the padding gone
|
|
779
|
+
// wrong, not a value: seen live 2026-09-01 on a nullable text field meant
|
|
780
|
+
// for her verbatim words, where it would have filed "null" as her sentence,
|
|
781
|
+
// and on a nullable permission id, where it would have read as one cited.
|
|
782
|
+
// An app that lists the string "null" itself — an enum member, a const —
|
|
783
|
+
// meant it, so only a field that does not name it is read this way.
|
|
784
|
+
if (value === "null" && permitsNull(schema, root) && !namesStringLiteral(schema, "null", root))
|
|
785
|
+
return null;
|
|
786
|
+
if (typeof value === "string" && !admitsJsonType(schema, "string", root)) {
|
|
787
|
+
// A closed object or an array handed over as a string of JSON. The schema
|
|
788
|
+
// offers no string at this field, so the string is an encoding, not an
|
|
789
|
+
// answer; it is read and restored as the value it encodes. Twenty live
|
|
790
|
+
// calls on 2026-09-01 sent a failure price this way and every one was
|
|
791
|
+
// refused at the door, with the model unable to tell what was wrong.
|
|
792
|
+
const parsed = parseJson(value);
|
|
793
|
+
if ((jsonSchemaObject(parsed) && admitsJsonType(schema, "object", root))
|
|
794
|
+
|| (Array.isArray(parsed) && admitsJsonType(schema, "array", root))) {
|
|
795
|
+
return restoreStrictValue(parsed, schema, root);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
const branch = matchingUnionBranch(schema, value, root);
|
|
799
|
+
if (branch)
|
|
800
|
+
return restoreStrictValue(value, branch, root);
|
|
801
|
+
if (Array.isArray(value))
|
|
802
|
+
return value.map((item) => restoreStrictValue(item, schema.items, root));
|
|
803
|
+
const object = jsonSchemaObject(value);
|
|
804
|
+
const properties = jsonSchemaObject(schema.properties);
|
|
805
|
+
if (!object || !properties)
|
|
806
|
+
return value;
|
|
807
|
+
const required = new Set(Array.isArray(schema.required) ? schema.required.filter((name) => typeof name === "string") : []);
|
|
808
|
+
return Object.fromEntries(Object.entries(object).flatMap(([key, item]) => {
|
|
809
|
+
// The padding, undone: a key the original schema did not require was only
|
|
810
|
+
// sent because the strict subset has no way to leave it out.
|
|
811
|
+
if (item === null && !required.has(key))
|
|
812
|
+
return [];
|
|
813
|
+
return [[key, restoreStrictValue(item, properties[key], root)]];
|
|
814
|
+
}));
|
|
815
|
+
}
|
|
816
|
+
/**
|
|
817
|
+
* The branch of a union a value actually took.
|
|
818
|
+
*
|
|
819
|
+
* A discriminated union is matched by its discriminator first — the literal
|
|
820
|
+
* property that names the shape — because two branches of one union routinely
|
|
821
|
+
* share a type, and picking the wrong one would strip the wrong keys. Anything
|
|
822
|
+
* else falls back to the first branch whose type the value could satisfy.
|
|
823
|
+
*/
|
|
824
|
+
function matchingUnionBranch(schema, value, root) {
|
|
825
|
+
const union = Array.isArray(schema.oneOf) ? schema.oneOf : Array.isArray(schema.anyOf) ? schema.anyOf : [];
|
|
826
|
+
const candidates = union
|
|
827
|
+
.map((candidate) => resolveJsonSchemaRef(jsonSchemaObject(candidate), root))
|
|
828
|
+
.filter((candidate) => Boolean(candidate));
|
|
829
|
+
if (!candidates.length)
|
|
830
|
+
return null;
|
|
831
|
+
const object = jsonSchemaObject(value);
|
|
832
|
+
if (object) {
|
|
833
|
+
const discriminated = candidates.find((candidate) => {
|
|
834
|
+
const properties = jsonSchemaObject(candidate.properties);
|
|
835
|
+
if (!properties)
|
|
836
|
+
return false;
|
|
837
|
+
return Object.entries(properties).some(([key, propertyValue]) => {
|
|
838
|
+
const property = jsonSchemaObject(propertyValue);
|
|
839
|
+
if (!property || object[key] === undefined)
|
|
840
|
+
return false;
|
|
841
|
+
if (Object.prototype.hasOwnProperty.call(property, "const"))
|
|
842
|
+
return Object.is(property.const, object[key]);
|
|
843
|
+
return Array.isArray(property.enum) && property.enum.length === 1 && Object.is(property.enum[0], object[key]);
|
|
844
|
+
});
|
|
845
|
+
});
|
|
846
|
+
if (discriminated)
|
|
847
|
+
return discriminated;
|
|
848
|
+
}
|
|
849
|
+
// Before settling for any object branch, prefer one whose declared
|
|
850
|
+
// properties cover every key the value actually carries. A strict transform
|
|
851
|
+
// that merges a discriminated union hoists the discriminator away from its
|
|
852
|
+
// siblings, so a nested union of bodies reaches here with no discriminator
|
|
853
|
+
// at all — and "first object branch" then restores the value against a
|
|
854
|
+
// stranger's `required`, deleting every explicit null the right branch
|
|
855
|
+
// required. Found live 2026-08-26: a maintenance card's `standingSlot: null`
|
|
856
|
+
// was stripped against a sibling body's schema, making null unsendable and
|
|
857
|
+
// teaching the model that only an invented value could pass.
|
|
858
|
+
if (object) {
|
|
859
|
+
const covering = candidates.find((candidate) => {
|
|
860
|
+
if (candidate.type !== "object" || isOpenRecordJsonSchema(candidate))
|
|
861
|
+
return false;
|
|
862
|
+
const properties = jsonSchemaObject(candidate.properties);
|
|
863
|
+
if (!properties)
|
|
864
|
+
return false;
|
|
865
|
+
return Object.keys(object).every((key) => properties[key] !== undefined);
|
|
866
|
+
});
|
|
867
|
+
if (covering)
|
|
868
|
+
return covering;
|
|
869
|
+
}
|
|
870
|
+
return candidates.find((candidate) => {
|
|
871
|
+
if (candidate.type === "null")
|
|
872
|
+
return value === null;
|
|
873
|
+
if (isOpenRecordJsonSchema(candidate))
|
|
874
|
+
return typeof value === "string" || Boolean(object);
|
|
875
|
+
if (candidate.type === "object")
|
|
876
|
+
return Boolean(object);
|
|
877
|
+
if (candidate.type === "array")
|
|
878
|
+
return Array.isArray(value);
|
|
879
|
+
if (candidate.type === "integer")
|
|
880
|
+
return typeof value === "number";
|
|
881
|
+
return typeof candidate.type === "string" && typeof value === candidate.type;
|
|
882
|
+
}) ?? null;
|
|
883
|
+
}
|
|
884
|
+
//# sourceMappingURL=json-schema.js.map
|