4.1 KiB
Agent Note: Unified JSON-value schema DSL
Status: implemented
English | 中文
Problem
Tool parameters used a small author DSL while subagent/workflow structured output used a separate raw JSON Schema subset and validator. The two vocabularies disagreed about roots, scalar constraints, and validation, so a typed canonical tool-output contract would either duplicate both paths again or accept schemas that some projection could not enforce.
Decision
dsh-tools owns one JSON-value schema vocabulary with two representations. ValueSchemaSpec is the author form for any JSON root; ParameterSchemaSpec is its implicit object-property-map form with per-property required: true. JsonSchemaNode is the raw wire form. Both support string, finite number, integer, boolean, null, array, object, type-correct scalar enum/const, and exact-one oneOf; { type: 'json' } is author-only sugar for an annotation-only unconstrained raw node.
An explicit author object must declare additionalProperties: true | false. The implicit parameter root and raw JSON Schema preserve the standard open default. InferValue<S> and InferArgs<P> derive TypeScript values from the same declarations that valueSchemaSpecToJsonSchema() and parameterSchemaSpecToJsonSchema() compile. assertSupportedJsonSchema() rejects unsupported or misplaced keywords, and validateJsonSchemaValue() enforces the accepted subset against the lossless JsonValue boundary: no undefined, negative zero, non-finite numbers, sparse arrays, cycles, exotic objects, functions, symbols, or other coercive values. Intrinsic plain Object and Array containers remain plain across JavaScript realms; subclasses remain exotic. Author compilation, raw-schema assertion, value validation, schema-to-TypeScript rendering, registry detachment, and dynamic Cordis cross-realm normalization and cloning use explicit work stacks, so valid nesting is limited by available memory rather than the JavaScript call stack.
Object-rooting is a consumer rule rather than a vocabulary restriction. Subagent and workflow caller-defined structured outputs use assertObjectJsonSchema() and ObjectJsonSchema; tool outputs may use any root. Dynamic Cordis registrations rebuild realm-foreign schemas into host-owned JSON, preserve raw-wrapper openness, and require direct-DSL object openness before calling the same compiler.
Alternatives considered
- Keep separate parameter and structured-output schema systems: rejected because every added output construct would require parallel inference, compilation, validation, and code-generation changes with no useful ownership boundary.
- Adopt full JSON Schema or Ajv: rejected because the harness must fail on every construct it cannot project into its generated SDK and validators; accepting a larger language would make enforcement and model guidance dishonest.
- Make every object implicitly open or closed: rejected because either choice hides a consequential author decision. Only the legacy-shaped implicit parameter root and external raw schema retain an intentional default.
- Define
oneOfas first-match: rejected because branch ordering would change validation semantics and allow overlapping branches to hide ambiguous values.
Consequences
- Parameter validation, output validation, schema-to-TypeScript generation, subagent/workflow guards, and dynamic registration share one enforced vocabulary.
- Output declarations can infer object, array, scalar, or null roots; subagent/workflow structured outputs remain object-rooted at their existing seams.
- Explicit object openness and type-correct literal constraints make malformed declarations fail during authoring or registration rather than during a later model call.
- Raw tools may still register broader JSON Schema directly, but unified code generation treats unsupported schemas as unknown instead of pretending to enforce them.
- Runtime and compile-time tests cover every root, exact-one overlap/no-match behavior, raw open defaults, explicit openness, lossy JSON values, inference, and deep nesting across core and dynamic projections.