Skip to content

feat(agent): support Standard Schema validators - #107

Open
LukasParke wants to merge 13 commits into
mainfrom
agent/agent-standard-schema
Open

feat(agent): support Standard Schema validators#107
LukasParke wants to merge 13 commits into
mainfrom
agent/agent-standard-schema

Conversation

@LukasParke

@LukasParke LukasParke commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds Standard Schema v1 support to @openrouter/agent, allowing tool input, output, event, context, shared-context, check, and custom-hook schemas to use validators such as Valibot, ArkType, and Effect Schema without requiring Zod schemas.

What changed

  • added a small Standard Schema adapter around schema['~standard'].validate(value) with sync and async validation support
  • preserved Zod v4 as the detected fast path using the existing z4.parse, safeParse, and z4.toJSONSchema behavior
  • widened tool, agent-tool, shared-context, check-schema, and custom-hook schema types while preserving Zod inference
  • normalized Standard Schema issues into the existing tool validation error shape
  • added inputJsonSchema as the explicit wire-schema escape hatch for non-Zod input validators
  • kept recursive ~-key sanitization for generated and caller-supplied JSON Schema
  • documented Standard Schema usage and added a minor changeset

Design decisions

JSON Schema strategy

Input JSON Schema conversion now uses a three-tier chain:

  1. Zod keeps the existing z4.toJSONSchema(..., { target: 'draft-7' }) fast path, including Zod versions older than 4.2.
  2. Other validators may expose the StandardJSONSchemaV1 companion trait; the agent calls schema['~standard'].jsonSchema.input({ target: 'draft-07' }) and falls through if conversion throws.
  3. inputJsonSchema remains the deterministic explicit override and fallback. Explicit caller intent wins over the trait.

This adds no runtime dependency: StandardJSONSchemaV1 ships in the existing @standard-schema/spec dependency. Zod 4.2+, ArkType 2.1.28+, Zod Mini, VineJS, and Sury implement the trait natively; Valibot supports it via toStandardJsonSchema() from @valibot/to-json-schema.

Only input schemas need conversion because output, event, context, shared-context, check, and custom-hook schemas remain local validators. Generated and supplied schemas retain recursive ~-key sanitization.

SDK boundary

The agent converts every client tool to the SDK's existing function-tool wire shape before making a request. The boundary remains the already-untyped parameters: Record<string, unknown> JSON Schema handoff, so this PR does not depend on the parallel @openrouter/sdk Standard Schema work. No SDK schema types are exposed or cast into the agent's public validator surface.

Context mutation

Initial context validation supports any synchronous Standard Schema validator. ctx.setContext() and ctx.setSharedContext() remain synchronous APIs, so they reject validators whose ~standard.validate returns a Promise. Zod keeps its existing per-field partial-update behavior; generic Standard Schema context updates validate the merged context object.

Test coverage

  • Zod regression: validation and JSON Schema generation
  • Valibot input transforms and output validation
  • Valibot event and context validation
  • Standard Schema issue-path mapping into existing errors
  • StandardJSONSchemaV1 conversion, throw fallback, explicit override, and ~ metadata sanitization
  • missing inputJsonSchema failure for non-Zod tools
  • async Standard Schema validation through tool execution
  • compile-time inference for Standard Schema input/output/event/context and unchanged Zod inference
  • full monorepo build, typecheck, lint, and unit tests

Verification:

  • pnpm build
  • pnpm typecheck
  • pnpm lint
  • pnpm test — 918 agent tests and 168 MCP tests passed

API example

import { tool } from '@openrouter/agent';
import { toStandardJsonSchema } from '@valibot/to-json-schema';
import * as v from 'valibot';

// Trait path: toStandardJsonSchema exposes StandardJSONSchemaV1, so no
// inputJsonSchema is needed.
const search = tool({
  name: 'search',
  inputSchema: toStandardJsonSchema(v.object({ query: v.string() })),
  outputSchema: v.object({ results: v.array(v.string()) }),
  execute: async ({ query }) => ({ results: await searchWeb(query) }),
});

// Escape hatch: validation-only Standard Schema inputs supply the
// provider-facing JSON Schema explicitly (always wins when present,
// including for Zod schemas that z.toJSONSchema cannot convert).
const lookup = tool({
  name: 'lookup',
  inputSchema: v.object({ id: v.pipe(v.string(), v.transform(Number)) }),
  inputJsonSchema: {
    type: 'object',
    properties: { id: { type: 'string' } },
    required: ['id'],
  },
  execute: async ({ id }) => db.get(id), // id is the transformed number
});

@LukasParke
LukasParke marked this pull request as ready for review August 11, 2026 23:02
devin-ai-integration[bot]

This comment was marked as resolved.

- reject thenable-returning validators (not just instanceof Promise) in
  validateSchemaSync so async validators can't silently pass setContext
- restore Zod parity for Standard Schema context updates: filter unknown
  keys and store the validator's (possibly transformed) output values
- reword async-validator error; fix stale isVoidSchema doc; use the
  ObjectSchema alias for sharedContextSchema
Importing ObjectSchema from ./schema.js added a 16th internal edge to
model-result.ts, tripping sentrux's no_god_files gate. Revert to the
external type imports (unresolved, uncounted) to stay at fan-out 15.
devin-ai-integration[bot]

This comment was marked as resolved.

…and error propagation

- convertSchemaToJsonSchema: explicit inputJsonSchema now wins for Zod
  too (z4.toJSONSchema throws on unrepresentable constructs, where the
  escape hatch is the only way through)
- validatePartialAgainstSchema: Object.hasOwn instead of `in` so
  prototype-named keys (constructor, toString) are filtered again
- unifiedExecutionResult: normalize caught values eagerly so
  `throw undefined` / Promise.reject() still surface as tool errors
- isVoidSchema: probe Standard Schema validators with undefined so
  v.void() custom hooks skip result validation like z.void()
devin-ai-integration[bot]

This comment was marked as resolved.

…void probe, changeset example

- validatePartialAgainstSchema: persist raw caller-supplied values (Zod
  parity); validator output is only used to filter unknown keys. Storing
  transformed output poisoned the store for type-changing validators.
- isVoidSchema: a Standard Schema is void only if it accepts undefined
  AND rejects null/string/number/object probes, so v.any()/v.optional()
  keep result validation like their Zod equivalents.
- changeset: add the fenced consumer example required by
  .agents/skills/public-api-examples.
devin-ai-integration[bot]

This comment was marked as resolved.

…alidators

The unified log sink used validateSchemaSync, so a tool declaring an
async Standard Schema event validator threw out of its own body on the
first ctx.log while the same event passed via yield. Zod keeps the sync
throw; non-Zod entries are validated out-of-band and forwarded on
success (invalid ones dropped with a warning).
devin-ai-integration[bot]

This comment was marked as resolved.

Value probing cannot distinguish 'accepts only undefined' from
'undefined | T' (v.optional(v.object(...)) rejects every finite sentinel
set), so the sentinel probe could silently disable result validation for
ordinary optional schemas. Non-Zod result schemas are now always
validated: side-effect-only handlers returning undefined still pass a
v.void() result schema, and handlers returning real values on a void
hook are warned — stricter than Zod, but sound.
devin-ai-integration[bot]

This comment was marked as resolved.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant