Signature parsing and validation for SubAgents.
Signatures define the contract between agents and tools:
- Input parameters - What the caller must provide
- Output type - What the callee will return
Signature Format
Full format: (params) -> output
Shorthand: output (equivalent to () -> output)
Types
- Primitives:
:string,:int,:float,:bool,:keyword,:any - Collections:
[:type](list),{field :type}(map),:map(untyped map) - Optional:
:type?(nullable field or parameter)
Examples
iex> {:ok, sig} = Signature.parse("(name :string) -> {greeting :string}")
iex> sig
{:signature, [{"name", :string}], {:map, [{"greeting", :string}]}}
iex> {:ok, sig} = Signature.parse("{count :int}")
iex> sig
{:signature, [], {:map, [{"count", :int}]}}
Summary
Functions
Parse a signature string into internal format.
Format a signature back to string representation.
Check if signature returns a list type.
Convert a signature to JSON Schema format.
Validate data against a signature's return type.
Validate input parameters against a signature.
Types
@type return_type() :: type()
@type signature() :: {:signature, [param()], return_type()}
@type validation_error() :: %{ path: [String.t() | non_neg_integer()], message: String.t() }
Functions
Parse a signature string into internal format.
Returns {:ok, signature()} or {:error, reason}.
Examples
iex> Signature.parse("(id :int) -> {name :string}")
{:ok, {:signature, [{"id", :int}], {:map, [{"name", :string}]}}}
iex> Signature.parse("() -> :string")
{:ok, {:signature, [], :string}}
iex> Signature.parse("{count :int}")
{:ok, {:signature, [], {:map, [{"count", :int}]}}}
iex> Signature.parse("invalid")
{:error, "..."}
Format a signature back to string representation.
Used for rendering in prompts or debugging.
Check if signature returns a list type.
Used to determine if text mode response needs unwrapping.
Convert a signature to JSON Schema format.
Extracts the return type and converts it to a JSON Schema that can be passed to LLM providers for structured output.
Note: Array return types are wrapped in an object with an "items" property
because most LLM providers require an object at the root level. Use
returns_list?/1 to check if unwrapping is needed.
Examples
iex> {:ok, sig} = PtcRunner.SubAgent.Signature.parse("() -> {sentiment :string, score :float}")
iex> PtcRunner.SubAgent.Signature.to_json_schema(sig)
%{
"type" => "object",
"properties" => %{
"sentiment" => %{"type" => "string"},
"score" => %{"type" => "number"}
},
"required" => ["sentiment", "score"],
"additionalProperties" => false
}
iex> {:ok, sig} = PtcRunner.SubAgent.Signature.parse("() -> [:int]")
iex> PtcRunner.SubAgent.Signature.to_json_schema(sig)
%{
"type" => "object",
"properties" => %{
"items" => %{"type" => "array", "items" => %{"type" => "integer"}}
},
"required" => ["items"],
"additionalProperties" => false
}
@spec validate(signature(), term()) :: :ok | {:error, [validation_error()]}
Validate data against a signature's return type.
Returns :ok or {:error, [validation_error()]}.
Examples
iex> {:ok, sig} = Signature.parse("() -> {count :int, items [:string]}")
iex> Signature.validate(sig, %{count: 5, items: ["a", "b"]})
:ok
iex> {:ok, sig} = Signature.parse("() -> :int")
iex> Signature.validate(sig, "not an int")
{:error, [%{path: [], message: "expected int, got string"}]}
@spec validate_input(signature(), map()) :: :ok | {:error, [validation_error()]}
Validate input parameters against a signature.
Returns :ok or {:error, [validation_error()]}.