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:
- 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.
- 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.
Summary
A measure whose argument is an array of plain strings (or numbers, integers, booleans) is
uncallable.
commons:::coerce_arg()applies the enum-onlyvaluesproperty lookup to the array'sitemstype, so validation errors before the measure is ever reached. The model's correct toolcall 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
Reproducible example
No data, no database and no LLM required.
Actual output (verbatim, an unrelated build-version warning trimmed):
Expected: the second call returns
List of 1 / $ x: chr [1:2] "a" "b"as well — an array of plainstrings has no value set to test membership against, so there is nothing to validate.
validate_measure_args()is the functioncall_measure_tool()calls, so this is the live path andnot an artefact of reaching for an internal:
Every non-enum item type fails identically:
Cause
coerce_arg()(commons 0.1.0):type_values()isS7::prop(type, "values"). That property exists onellmer::TypeEnumand not onellmer::TypeBasic, so the array branch throws for any array whoseitemsis a basic type.The sibling helper in the same package guards the identical lookup:
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 ouragent, four measure arguments are
string[](e.g. anindicators: string[]overlay argument on achart measure), and those measures were rejected on every attempt.
Two things make it expensive to diagnose from the outside:
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.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 membershiptest 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 tocharacter. (Today the array branch ends in
return(as.character(value)), so even once themembership test is fixed a
number[]argument would reach the measure as a character vector — asecond, smaller defect on the same branch.)
Happy to open a PR with tests covering
string[],number[]andenum[]if that is useful.