Commit 8b0dd11

mo khan <mo@mokhan.ca>
2026-08-31 01:55:45
fix: default items on array-typed tool params missing it
gpt-oss's Jinja chat template throws when rendering an array param with no "items" ({%- if param_spec['items'] -%} isn't tolerant of a missing key), crashing the GGUF provider outright whenever such a tool is offered. Hit via the git tool's args param. The failure was silent: Agent#fetch_response's rescue swallowed the exception into an empty response with no visible error. Fixed at both the specific call site (git.rb now declares items explicitly) and the general one (Tool#initialize defaults items on any array param missing it, symbol- or string-keyed, since tools can be registered at runtime with arbitrary schemas via the eval plugin). Claude-Session: https://claude.ai/code/session_01FpbgyAMtPEkDbo2kx78qR6
Changed files (2)
.elelem
plugins
lib
elelem
.elelem/plugins/git.rb
@@ -5,7 +5,7 @@ Elelem::Plugins.register(:git) do |agent|
 
   agent.toolbox.add("git",
     description: "Run git command",
-    params: { command: { type: "string" }, args: { type: "array" } },
+    params: { command: { type: "string" }, args: { type: "array", items: { type: "string" } } },
     required: ["command"]
   ) do |a|
     cmd = a["command"]
lib/elelem/tool.rb
@@ -7,7 +7,7 @@ module Elelem
     def initialize(name, description:, params: {}, required: [], aliases: [], &fn)
       @name = name
       @description = description
-      @params = params.freeze
+      @params = self.class.with_array_items(params).freeze
       @required = required.freeze
       @aliases = aliases.freeze
       @fn = fn
@@ -15,6 +15,25 @@ module Elelem
       @schema = JSONSchemer.schema(@schema_hash)
     end
 
+    # gpt-oss's chat template throws when rendering an "array" param with no
+    # "items" (Jinja's `if param_spec['items']` isn't tolerant of a missing
+    # key there). Tools can be registered at runtime with arbitrary params
+    # (see the `eval` plugin), so default this here rather than only at each
+    # call site -- one place, covers every caller.
+    def self.with_array_items(params)
+      params.transform_values do |spec|
+        next spec unless spec.is_a?(Hash)
+
+        string_keyed = spec.key?("type")
+        type = string_keyed ? spec["type"] : spec[:type]
+        has_items = spec.key?(:items) || spec.key?("items")
+        next spec unless type == "array" && !has_items
+
+        default = string_keyed ? { "items" => { "type" => "string" } } : { items: { type: "string" } }
+        spec.merge(default)
+      end
+    end
+
     def call(args)
       @fn.call(args)
     end