Skip to content

coerce_arg() cannot validate a non-enum array argument: Can't find property <ellmer::TypeBasic>@values #376

Description

@travisjakel

Summary

A measure whose argument is an array of plain strings (or numbers, integers, booleans) is
uncallable. commons:::coerce_arg() applies the enum-only values property lookup to the array's
items type, so validation errors before the measure is ever reached. The model's correct tool
call is rejected by the harness and comes back to it as Tool calling failed with error Can't find property <ellmer::TypeBasic>@values, which it typically responds to by retrying.

Environment

  • commons 0.1.0 (CRAN)
  • ellmer 0.5.0
  • S7 0.2.2
  • R 4.5.0 (2025-04-11 ucrt), Windows 11

Reproducible example

No data, no database and no LLM required.

library(ellmer)

arr <- function(items) ellmer::tool(
  function(x) x,
  name        = "demo",
  description = "A measure with an array argument.",
  arguments   = list(x = ellmer::type_array(items))
)

# enum[] -- validates fine
str(commons:::validate_measure_args(arr(type_enum(values = c("a", "b"))),
                                    list(x = c("a", "b"))))

# string[] -- e.g. `@param indicators `string[]`` on a chart measure
str(commons:::validate_measure_args(arr(type_string()),
                                    list(x = c("a", "b"))))

Actual output (verbatim, an unrelated build-version warning trimmed):

List of 1
 $ x: chr [1:2] "a" "b"
Error: Can't find property <ellmer::TypeBasic>@values
Execution halted

Expected: the second call returns List of 1 / $ x: chr [1:2] "a" "b" as well — an array of plain
strings has no value set to test membership against, so there is nothing to validate.

validate_measure_args() is the function call_measure_tool() calls, so this is the live path and
not an artefact of reaching for an internal:

call_measure (tool)  ->  commons:::call_measure_tool()
                     ->  commons:::validate_measure_args()
                     ->  commons:::coerce_arg()

Every non-enum item type fails identically:

string[]  -> ERROR: Can't find property <ellmer::TypeBasic>@values
number[]  -> ERROR: Can't find property <ellmer::TypeBasic>@values
integer[] -> ERROR: Can't find property <ellmer::TypeBasic>@values
boolean[] -> ERROR: Can't find property <ellmer::TypeBasic>@values

Cause

coerce_arg() (commons 0.1.0):

kind <- type_kind(type)
if (kind %in% c("enum", "array")) {
  allowed <- if (kind == "enum") {
    type_values(type)
  } else {
    type_values(S7::prop(type, "items"))   # <-- unconditional
  }
  bad <- setdiff(as.character(value), allowed)
  ...
}

type_values() is S7::prop(type, "values"). That property exists on ellmer::TypeEnum and not on
ellmer::TypeBasic, so the array branch throws for any array whose items is a basic type.

The sibling helper in the same package guards the identical lookup:

array_items_label <- function(items) {
  if (identical(type_kind(items), "enum")) {
    paste(type_values(items), collapse = ", ")
  } else {
    type_kind(items)
  }
}

So array_items_label() already encodes the correct rule; coerce_arg() omits it.

Consequence

Any measure with an array-of-basic-type argument is uncallable through call_measure. In our
agent, four measure arguments are string[] (e.g. an indicators: string[] overlay argument on a
chart measure), and those measures were rejected on every attempt.

Two things make it expensive to diagnose from the outside:

  1. The failure is attributed to the model. The tool result the model receives is a generic S7
    property error with no argument name in it, so it reads as a bad call rather than as a harness
    defect. Measured on one item: the model emitted exactly the intended arguments —
    {"ticker":"AAPL","days":730,"indicators":["sma:200"]} — and got the error back.
  2. The retry is unbounded and grows the conversation. The model re-searches the measure pool, gets
    the same roster back, and tries again; in our case the conversation reached 17 near-identical
    search results (~121 KB, 89% of the request body) and then hit the server's context ceiling as
    an HTTP 400. Two different-looking transport 400s upstream of this both trace back to this one
    branch.

An open grammar such as "sma:200" could not be enumerated even in principle, so the membership
test is not merely unavailable here — it is not meaningful.

Suggested fix

Guard the membership test on the item type actually having a value set, mirroring
array_items_label(), and coerce array elements by their item kind rather than always to
character. (Today the array branch ends in return(as.character(value)), so even once the
membership test is fixed a number[] argument would reach the measure as a character vector — a
second, smaller defect on the same branch.)

if (kind == "array") {
  items <- S7::prop(type, "items")
  if (identical(type_kind(items), "enum")) {
    bad <- setdiff(as.character(value), type_values(items))
    if (length(bad)) {
      cli::cli_abort(...)
    }
    return(as.character(value))
  }
  return(switch(
    type_kind(items),
    number  = as.numeric(value),
    integer = as.integer(value),
    boolean = as.logical(value),
    as.character(value)
  ))
}

Happy to open a PR with tests covering string[], number[] and enum[] if that is useful.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions