Coerce values to expected types with warning generation.
This module handles lenient input validation for LLMs, which sometimes produce slightly malformed data (e.g., quoted numbers, missing types).
Elixir to Signature Type Mapping
Primitive Types
| Elixir Type | Signature Type | Notes |
|---|---|---|
String.t() | :string | UTF-8 strings |
binary() | :string | Same as String.t() |
integer() | :int | Whole numbers |
non_neg_integer() | :int | Validation can enforce >= 0 |
pos_integer() | :int | Validation can enforce > 0 |
float() | :float | Decimal numbers |
number() | :float | Accepts int or float |
boolean() | :bool | Boolean values |
atom() | :keyword | Atoms as keywords |
any() | :any | Matches everything |
Collection Types
| Elixir Type | Signature Type | Notes |
|---|---|---|
list(t) | [:t] | Homogeneous lists |
map() | :map | Untyped dictionary |
%{key: type} | {:key :type} | Typed map |
Special Types
| Elixir Type | Signature Type | Rationale |
|---|---|---|
DateTime.t() | :string | ISO 8601 format, LLM-friendly |
| t | nil | :t? | Optional via ? suffix |
Coercion Rules
LLMs sometimes produce slightly malformed data. Input coercion handles common cases:
| From | To | Behavior | Warning |
|---|---|---|---|
"42" | :int | 42 | Yes |
"3.14" | :float | 3.14 | Yes |
"-5" | :int | -5 | Yes |
"true" | :bool | true | Yes |
"false" | :bool | false | Yes |
42 | :float | 42.0 | No (silent widening) |
42.0 | :int | Error | - |
"hello" | :int | Error | - |
:atom | :string | "atom" | Yes |
"atom" | :keyword | :atom | Yes |
Output validation is strict - no coercion applied.
Coercion Modes
| Mode | Input Coercion | Output Validation | Use Case |
|---|---|---|---|
:enabled (default) | Apply with warnings | Strict | Production |
:warn_only | Apply with warnings | Log warnings only | Development |
:strict | No coercion | Strict, reject extra fields | Testing |
:disabled | Skip | Skip | Debugging |
Examples
iex> PtcRunner.SubAgent.Signature.Coercion.coerce("42", :int)
{:ok, 42, ["coerced string \"42\" to integer"]}
iex> PtcRunner.SubAgent.Signature.Coercion.coerce(42, :float)
{:ok, 42.0, []}
iex> PtcRunner.SubAgent.Signature.Coercion.coerce("hello", :int)
{:error, "cannot coerce string \"hello\" to integer"}
iex> PtcRunner.SubAgent.Signature.Coercion.coerce("hello", :keyword)
{:ok, :hello, ["coerced string \"hello\" to keyword"]}
Summary
Types
Functions
@spec coerce(term(), atom() | tuple()) :: coercion_result()
Coerce a value to the expected type.
Returns {:ok, coerced_value, warnings} or {:error, reason}.
Examples
iex> PtcRunner.SubAgent.Signature.Coercion.coerce("42", :int)
{:ok, 42, ["coerced string \"42\" to integer"]}
iex> PtcRunner.SubAgent.Signature.Coercion.coerce(42, :float)
{:ok, 42.0, []}
iex> PtcRunner.SubAgent.Signature.Coercion.coerce("hello", :int)
{:error, "cannot coerce string \"hello\" to integer"}
iex> PtcRunner.SubAgent.Signature.Coercion.coerce(%{"id" => "42", "name" => "Alice"}, {:map, [{"id", :int}, {"name", :string}]})
{:ok, %{"id" => 42, "name" => "Alice"}, ["coerced string \"42\" to integer"]}
@spec coerce(term(), atom() | tuple(), keyword()) :: coercion_result()
Coerce a value to the expected type with options.
Options:
:nested- whether this is a nested coercion (default: false)
Returns {:ok, coerced_value, warnings} or {:error, reason}.