| eatmycode_version | 1.2.0 |
|---|
Two jobs on the same data. ensure_strict rewrites a tool's parameter schema
into the shape providers require for strict function calling — every property
required, no additional properties, recursively. validate_arguments checks a
model's actual arguments against that schema and returns a list of
human-readable problems, which is what the dispatcher feeds back to the model
when a call is malformed.
The module owns neither the schema's origin (a ToolSpec from
toolkit.md, or a pydantic model on the Python side) nor what is
done with a validation failure. It is not a conforming JSON Schema validator
and does not try to be.
done. cargo test -p kerness jsonschema passes 8 tests and
bindings/python/tests/test_jsonschema.py passes its one boundary case.
| File | Role |
|---|---|
crates/kerness/src/jsonschema.rs |
both functions and their helpers |
bindings/python/src/funcs.rs |
validate_arguments (bindings/python/src/funcs.rs:208) and ensure_strict (:220) as pyfunctions |
bindings/python/kerness/jsonschema.py |
re-export shim |
Rust crate module with two #[pyfunction] wrappers and a shim. The root's
Coding Style and Code Design
rules apply; local facts:
- The module imports only
errorandpyfmt. Failure messages are rendered throughpyfmt::repr(crates/kerness/src/jsonschema.rs:11), so an enum refusal quotes the choices the way Python would spell them — those strings go back to the model and are asserted byte for byte inenum_failures_quote_the_choices_the_way_python_would(:334). ensure_strictmutates in place and returnsResult<()>; the Python wrapper copies the value across, rewrites, and returns the copy (bindings/python/src/funcs.rs:220), so a Python caller sees a pure function.unreachable!with a message is used where aget/get_mutpair on the same key cannot disagree (crates/kerness/src/jsonschema.rs:58,:73,:83); the crate-wide convention for an invariant the borrow checker forces into two statements.- Unit tests are inline under
#[cfg(test)]with one helper,args(crates/kerness/src/jsonschema.rs:287), and sentence-style names.
ensure_strict (crates/kerness/src/jsonschema.rs:18) is recursive: strict
(:25) walks $defs/definitions (:35), properties (:51), items
(:66), anyOf (:71) and allOf (:82), carrying a breadcrumb path so
a refusal names where in the document it broke. Every object gains
additionalProperties: false unless it already says otherwise (:45), every
declared property is promoted to required (:53), a null default is
dropped (:108), a single-element allOf is inlined (:90), and a $ref
with siblings is inlined with the siblings winning and the merged object
re-run (:115). A non-object where an object is required is an
Error::Session naming the path (:27), not a best effort.
validate_arguments (crates/kerness/src/jsonschema.rs:183) is deliberately
shallow. It checks required and
unexpected keys, top-level property types, and enum membership; it
recognizes object and array values but does not descend into them. Recursive
traversal belongs to strict-schema rewriting; the second function catches the
mistakes models make rather than implementing JSON Schema.
validate_argumentsnever raises. An invalid call is a normal event in a model conversation, and the messages go back to the model as text. Both callers rely on it —crates/kerness/src/toolkit.rs:87turns a non-empty list into aToolResult::error, andcrates/kerness/src/session/run.rs:844into a tool error — so aResulthere would change what a failing call costs (a tool result versus a failed turn).- The messages read as instructions, not validator output:
missing required argument 'x',unexpected argument 'x',argument 'x' must be integer, got string. Enforced byrequired_and_type_failures_read_as_instructions(crates/kerness/src/jsonschema.rs:308). additionalProperties: falseapplies to an emptypropertiestoo: a tool that takes no arguments rejects every argument rather than ignoring them. Enforced bya_closed_empty_object_rejects_every_argument(crates/kerness/src/jsonschema.rs:295) and the one Python case (bindings/python/tests/test_jsonschema.py:6).- A boolean is not a number (
a_boolean_is_not_a_number,crates/kerness/src/jsonschema.rs:325), becauseserde_jsonwould otherwise lettruesatisfy an integer field. $refresolves#/-anchored paths against the document root only (resolve_ref,crates/kerness/src/jsonschema.rs:138);strictwalks$defsanddefinitions(:35). There is no remote or cross-document resolution.
crates/kerness/src/jsonschema.rs:18—ensure_strict(schema: &mut Value) -> Result<()>— rewrites in place;Error::Sessionfor a schema it cannot make strict, with the offending path in the message.crates/kerness/src/jsonschema.rs:183—validate_arguments(schema, arguments) -> Vec<String>— every problem found, empty when valid; never raises, and a schema that is not an object validates everything.crates/kerness/src/jsonschema.rs:138—resolve_ref(root, reference)—#/-anchored lookup only.bindings/python/src/funcs.rs:220—ensure_strict(json_schema)— the Python signature: takes a dict, returns a new dict, raisesSessionError.bindings/python/src/funcs.rs:208—validate_arguments(schema, arguments)— returnslist[str].
- toolkit.md's
ToolDispatcher::executecallsvalidate_argumentsbefore every handler (crates/kerness/src/toolkit.rs:87) and hands the joined messages back as aToolResult; run.md'stool_stepdoes the same before a scoped invocation (crates/kerness/src/session/run.rs:844). Both are proved throughcrates/kerness/tests/tools_e2e.rs. - provider.md's OpenAI backend calls
ensure_strictover a caller's output schema whenstrict_json_schemais set (crates/kerness/src/provider/openai.rs:88); on the Python side that schema comes from apydanticTypeAdapter(bindings/python/kerness/provider.py:312). The Python provider teststest_strict_mode_is_what_makes_an_optional_field_requiredandtest_the_strict_rewrite_reaches_nested_models_too(bindings/python/tests/test_provider.py:176,:202) are where the rewrite is proved against a realpydanticdocument. - toolschema.md renders
ToolSpec.parametersinto each dialect's wire shape without rewriting it;ensure_strictis applied only where a provider's strict mode demands it.
cargo test -p kerness jsonschema # pass = 8 passed
.venv/bin/python -m pytest bindings/python/tests/test_jsonschema.py -q # pass = 1 passed- The Rust tests are where the coverage is:
strict_closes_objects_and_requires_every_property(crates/kerness/src/jsonschema.rs:343),a_ref_with_siblings_is_inlined_and_the_siblings_win(:374),a_single_element_all_of_is_inlined(:363),a_non_object_schema_is_refused(:388), and two that pin the failure messages —enum_failures_quote_the_choices_the_way_python_would(:334) andrequired_and_type_failures_read_as_instructions(:308) — because those strings go back to the model and are the module's real output. bindings/python/tests/test_jsonschema.py:6test_closed_empty_object_rejects_every_argument: the one boundary case, a dict crossing and the list coming back.- The rewrite against a generated
pydanticschema is proved inbindings/python/tests/test_provider.py:176and:202, not here. - Gap:
anyOfrecursion has no dedicated test; it is exercised only when apydanticOptionalfield reachesbindings/python/tests/test_provider.py:176.
- Changing a failure message → the two message-pinning tests
(
crates/kerness/src/jsonschema.rs:308,:334) and the Python tests asserting the joined string through a tool result (bindings/python/tests/test_toolkit.py:83,:87). The text is model-facing output. - Adding a composition keyword (
oneOf,not) → a new branch instrict(crates/kerness/src/jsonschema.rs:25) following theanyOfshape at:71, with the breadcrumb extended throughextend(:159), plus a test beside:363. - Making validation deeper → keep it infallible and keep the messages
instruction-shaped; both callers join the list with
;and return it to the model. - Safe extension points:
type_matches(crates/kerness/src/jsonschema.rs:239) for a newtypespelling;resolve_ref(:138) for another local container. - Forbidden coupling: this module must not import
toolingorprovider; it takes a bareValueso both a tool schema and an output schema can go through it. - Compatibility: the Python
ensure_strictreturns a new dict; callers that relied on mutation would silently see nothing.
Improvement candidates (proposals, not accepted work):
- A direct
anyOftest in the Rust suite would let the strict rewrite be changed without apydanticinstall to prove it. Success check: the test fails when theanyOfbranch is removed.
$refresolves against the document only —$defsanddefinitions(crates/kerness/src/jsonschema.rs:35), which is what apydanticmodel with a nested submodel emits. There is no remote or cross-document resolution, and none is planned.validate_argumentsreports every problem it finds but does not attempt coercion — a string"3"for an integer field is reported, not converted.oneOfis not handled;anyOfandallOfare (crates/kerness/src/jsonschema.rs:71,:82). Nothing the framework generates emitsoneOf, so this bites only a caller hand-writing a tool schema.