Ask
158
@schema_drift_lu ·

model passes a comma string where my tool schema says string[], zod to json schema tool-schema

Tool takes labels: string[]. Maybe one call in six arrives as "labels": "urgent,billing" instead of ["urgent","billing"], and my Zod parse throws, and the agent then spends two more steps apologising and trying again.

The schema comes out of zod-to-json-schema. Looking at the emitted JSON I see the labels property is a $ref into definitions because the same Zod object is reused in three tools, and one field is an anyOf because it is .optional().

Is the $ref the problem, is it the anyOf, or do models just do this?

10 answers Share
Report

Answering anonymously — a moderator will review it first.

  • @onebag_ozzy · last mo. · 3 replies

    Start with the $ref. Emit the schema inlined and see if the failure rate moves. Providers vary in how thoroughly they resolve $ref and definitions in tool parameters, and a parameter the model cannot see the type of is a parameter it guesses at. Most zod converters have an option for this - zod-to-json-schema takes $refStrategy: 'none', which duplicates the shared object into each tool instead of pointing at it. Your schemas get bigger and uglier and the model stops improvising.

    After that, flatten the anyOf. .optional() producing a union of a type and null is usually harmless, but anyOf at the top level of a parameter is where things get vague. If you have anything like z.union([z.string(), z.array(z.string())]) anywhere, delete the string branch - offer a model two ways to send something and it will use the one you did not want.

    And put an example in the description. labels: string[] - e.g. ["urgent","billing"] is worth more than any amount of schema pedantry.

    139
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
    • @onebag_ozzy · last mo.

      Tool schemas are prompt text with angle brackets. DRY is a source-code virtue and it does not survive contact with the thing reading them.

      55
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
    • @schema_drift_lu · last mo.

      Inlining took it from about 1 in 6 to 1 in 40 on a 200-call replay. The description example got the rest. Slightly deflating that the fix was "make the schema more repetitive".

      44
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
  • @crimp_not_solder · last mo. · 3 replies

    Also just accept both and normalise. You are validating at the boundary anyway:

    const labels = z.preprocess(
      v => typeof v === 'string' ? v.split(',').map(s => s.trim()) : v,
      z.array(z.string())
    )
    

    This is not giving up. A retry costs a full round trip, two steps of context and a visibly confused agent; a coercion costs four lines. Fix the schema too, but stop making the user wait for a round trip over a comma.

    97
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
    • @adjunct_ora · last mo.

      Coerce, but count. If you silently normalise you lose the only signal that tells you the schema is confusing, and six months later nobody knows whether it is 1 in 6 or 1 in 600. Increment a metric in the preprocess branch and you get the ergonomics and the visibility.

      40
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
    • @crimp_not_solder · last mo.

      Agreed and I should have said so. A coercion without a counter is a bug you have agreed not to look at.

      45
      Share
      Reply

      Answering anonymously — a moderator will review it first.

      Report
  • @adjunct_ora · last mo.

    If your provider supports a strict or constrained decoding mode for tool arguments, turn it on and this class of bug largely stops existing, because the sampler cannot emit a string where the grammar demands an array. The catch is that strict modes usually support a subset of JSON Schema - no $ref, limited unions, sometimes every property required - so you may end up making the same simplifications anyway. Which is the actual lesson: the schema shapes that models handle well and the ones constrained decoding supports are almost the same list.

    71
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
  • @torque_spec · last mo.

    Whatever you change, do not evaluate it on vibes. Save 200 real tool calls from your logs, replay them through the new schema, and count. Every one of these threads has somebody who fixed it with a description tweak and somebody else for whom that changed nothing, and the difference is always that neither of them measured.

    46
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
  • @duckweed_dev · last mo.

    Long shot from a day I lost: check whether the field name itself is steering it. We had tags next to a filter string in the same tool, and a lot of the malformed calls looked like the model had blended the two parameters. Renamed one and the confusion dropped noticeably. No idea how to predict that in advance, but it is worth eyeballing the neighbours of a parameter that keeps getting mangled.

    34
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report
  • @pivot_pilot · last mo.

    "urgent,billing" is technically a string of labels. It is not wrong, it is just not helpful, which describes a lot of my week.

    20
    Share
    Reply

    Answering anonymously — a moderator will review it first.

    Report